API کے ذریعے PowerShell سے گوگل صارفین بنانا

ہیلو!

یہ مضمون G Suite صارفین کو جوڑ توڑ کرنے کے لیے Google API کے ساتھ PowerShell تعامل کے نفاذ کی وضاحت کرے گا۔

ہم پوری تنظیم میں متعدد داخلی اور کلاؤڈ خدمات استعمال کرتے ہیں۔ زیادہ تر حصے کے لیے، ان میں اجازت گوگل یا ایکٹو ڈائرکٹری تک آتی ہے، جس کے درمیان ہم ایک نقل برقرار نہیں رکھ سکتے؛ اس کے مطابق، جب کوئی نیا ملازم چلا جاتا ہے، تو آپ کو ان دونوں سسٹمز میں اکاؤنٹ بنانے/ فعال کرنے کی ضرورت ہوتی ہے۔ اس عمل کو خودکار بنانے کے لیے، ہم نے ایک اسکرپٹ لکھنے کا فیصلہ کیا جو معلومات اکٹھا کرے اور اسے دونوں سروسز کو بھیجے۔

اجازت

تقاضے تیار کرتے وقت، ہم نے اجازت کے لیے حقیقی انسانی منتظمین کو استعمال کرنے کا فیصلہ کیا؛ یہ حادثاتی یا جان بوجھ کر بڑی تبدیلیوں کی صورت میں اعمال کے تجزیہ کو آسان بناتا ہے۔

Google APIs توثیق اور اجازت کے لیے OAuth 2.0 پروٹوکول استعمال کرتے ہیں۔ استعمال کے معاملات اور مزید تفصیلی وضاحتیں یہاں مل سکتی ہیں: Google APIs تک رسائی کے لیے OAuth 2.0 استعمال کرنا.

میں نے اس اسکرپٹ کا انتخاب کیا جو ڈیسک ٹاپ ایپلی کیشنز میں اجازت کے لیے استعمال ہوتا ہے۔ سروس اکاؤنٹ استعمال کرنے کا آپشن بھی ہے، جس میں صارف کی جانب سے غیر ضروری نقل و حرکت کی ضرورت نہیں ہے۔

نیچے دی گئی تصویر گوگل پیج سے منتخب منظر نامے کی منصوبہ بندی کی وضاحت ہے۔

API کے ذریعے PowerShell سے گوگل صارفین بنانا

  1. سب سے پہلے، ہم صارف کو Google اکاؤنٹ کے تصدیقی صفحہ پر بھیجتے ہیں، GET پیرامیٹرز کی وضاحت کرتے ہوئے:
    • درخواست کی شناخت
    • جن علاقوں تک ایپلیکیشن کو رسائی کی ضرورت ہے۔
    • وہ پتہ جس پر صارف کو طریقہ کار مکمل کرنے کے بعد ری ڈائریکٹ کیا جائے گا۔
    • جس طرح سے ہم ٹوکن کو اپ ڈیٹ کریں گے۔
    • سیکیورٹی کوڈ
    • تصدیقی کوڈ ٹرانسمیشن فارمیٹ

  2. اجازت مکمل ہونے کے بعد، صارف کو پہلی درخواست میں بیان کردہ صفحہ پر بھیج دیا جائے گا، جس میں GET پیرامیٹرز کے ذریعے پاس کردہ غلطی یا اجازت نامے کے کوڈ کے ساتھ
  3. درخواست (اسکرپٹ) کو یہ پیرامیٹرز حاصل کرنے کی ضرورت ہوگی اور اگر کوڈ موصول ہو جائے تو ٹوکن حاصل کرنے کے لیے درج ذیل درخواست کریں
  4. اگر درخواست درست ہے تو، گوگل API واپس کرتا ہے:
    • رسائی ٹوکن جس کے ساتھ ہم درخواستیں کر سکتے ہیں۔
    • اس ٹوکن کی میعاد کی مدت
    • رسائی ٹوکن کو ریفریش کرنے کے لیے ریفریش ٹوکن درکار ہے۔

پہلے آپ کو گوگل API کنسول پر جانے کی ضرورت ہے: اسناد - گوگل API کنسول، مطلوبہ ایپلیکیشن منتخب کریں اور اسناد کے سیکشن میں ایک کلائنٹ OAuth شناخت کنندہ بنائیں۔ وہاں (یا بعد میں، تخلیق کردہ شناخت کنندہ کی خصوصیات میں) آپ کو ان پتے کی وضاحت کرنے کی ضرورت ہے جن پر ری ڈائریکشن کی اجازت ہے۔ ہمارے معاملے میں، یہ مختلف بندرگاہوں کے ساتھ متعدد لوکل ہوسٹ اندراجات ہوں گے (نیچے دیکھیں)۔

اسکرپٹ الگورتھم کو پڑھنے کے لیے اسے مزید آسان بنانے کے لیے، آپ ایک علیحدہ فنکشن میں پہلے مراحل کو ڈسپلے کر سکتے ہیں جو ایپلیکیشن کے لیے رسائی اور ریفریش ٹوکنز کو واپس کرے گا:

$client_secret = 'Our Client Secret'
$client_id = 'Our Client ID'
function Get-GoogleAuthToken {
  if (-not [System.Net.HttpListener]::IsSupported) {
    "HttpListener is not supported."
    exit 1
  }
  $codeverifier = -join ((65..90) + (97..122) + (48..57) + 45 + 46 + 95 + 126 |Get-Random -Count 60| % {[char]$_})
  $hasher = new-object System.Security.Cryptography.SHA256Managed
  $hashByteArray = $hasher.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($codeverifier))
  $base64 = ((([System.Convert]::ToBase64String($hashByteArray)).replace('=','')).replace('+','-')).replace('/','_')
  $ports = @(10600,15084,39700,42847,65387,32079)
  $port = $ports[(get-random -Minimum 0 -maximum 5)]
  Write-Host "Start browser..."
  Start-Process "https://accounts.google.com/o/oauth2/v2/auth?code_challenge_method=S256&code_challenge=$base64&access_type=offline&client_id=$client_id&redirect_uri=http://localhost:$port&response_type=code&scope=https://www.googleapis.com/auth/admin.directory.user https://www.googleapis.com/auth/admin.directory.group"
  $listener = New-Object System.Net.HttpListener
  $listener.Prefixes.Add("http://localhost:"+$port+'/')
  try {$listener.Start()} catch {
    "Unable to start listener."
    exit 1
  }
  while (($code -eq $null)) {
    $context = $listener.GetContext()
    Write-Host "Connection accepted" -f 'mag'
    $url = $context.Request.RawUrl
    $code = $url.split('?')[1].split('=')[1].split('&')[0]
    if ($url.split('?')[1].split('=')[0] -eq 'error') {
      Write-Host "Error!"$code -f 'red'
      $buffer = [System.Text.Encoding]::UTF8.GetBytes("Error!"+$code)
      $context.Response.ContentLength64 = $buffer.Length
      $context.Response.OutputStream.Write($buffer, 0, $buffer.Length)
      $context.Response.OutputStream.Close()
      $listener.Stop()
      exit 1
    }
    $buffer = [System.Text.Encoding]::UTF8.GetBytes("Now you can close this browser tab.")
    $context.Response.ContentLength64 = $buffer.Length
    $context.Response.OutputStream.Write($buffer, 0, $buffer.Length)
    $context.Response.OutputStream.Close()
    $listener.Stop()
  }
  Return Invoke-RestMethod -Method Post -Uri "https://www.googleapis.com/oauth2/v4/token" -Body @{
    code = $code
    client_id = $client_id
    client_secret = $client_secret
    redirect_uri = 'http://localhost:'+$port
    grant_type = 'authorization_code'
    code_verifier   = $codeverifier
  }
  $code = $null

ہم OAuth کلائنٹ شناخت کنندہ خصوصیات میں حاصل کردہ کلائنٹ آئی ڈی اور کلائنٹ سیکریٹ سیٹ کرتے ہیں، اور کوڈ کی تصدیق کنندہ 43 سے 128 حروف پر مشتمل ایک تار ہے جسے غیر محفوظ شدہ حروف سے تصادفی طور پر تیار کیا جانا چاہیے: [AZ] / [az] / [0-9 ] /"-"/"۔ / "_" / "~"۔

اس کے بعد یہ کوڈ دوبارہ منتقل کیا جائے گا۔ یہ اس خطرے کو ختم کرتا ہے جس میں حملہ آور صارف کی اجازت کے بعد ری ڈائریکٹ کے طور پر واپس آنے والے ردعمل کو روک سکتا ہے۔
آپ موجودہ درخواست میں ایک کوڈ تصدیق کنندہ کو واضح متن میں بھیج سکتے ہیں (جو اسے بے معنی بنا دیتا ہے - یہ صرف ان سسٹمز کے لیے موزوں ہے جو SHA256 کو سپورٹ نہیں کرتے ہیں)، یا SHA256 الگورتھم کا استعمال کرتے ہوئے ایک ہیش بنا کر، جسے BASE64Url میں انکوڈ ہونا چاہیے (مختلف بیس 64 سے دو ٹیبل حروف سے) اور کریکٹر لائن کے اختتام کو ہٹانا: =۔

اس کے بعد، اجازت کے بعد جواب حاصل کرنے کے لیے ہمیں مقامی مشین پر HTTP کو سننا شروع کرنا ہوگا، جسے ری ڈائریکٹ کے طور پر واپس کیا جائے گا۔

انتظامی کام ایک خاص سرور پر انجام پاتے ہیں، ہم اس امکان کو رد نہیں کر سکتے کہ ایک ہی وقت میں متعدد منتظمین اسکرپٹ کو چلائیں گے، اس لیے یہ موجودہ صارف کے لیے تصادفی طور پر ایک بندرگاہ کا انتخاب کرے گا، لیکن میں نے پہلے سے طے شدہ بندرگاہوں کی وضاحت کی ہے کیونکہ انہیں API کنسول میں قابل اعتماد کے طور پر بھی شامل کیا جانا چاہیے۔

رسائی_ٹائپ = آف لائن اس کا مطلب یہ ہے کہ ایپلیکیشن براؤزر کے ساتھ صارف کی بات چیت کے بغیر اپنے طور پر ایک میعاد ختم ہونے والے ٹوکن کو اپ ڈیٹ کر سکتی ہے،
ریسپانس_ٹائپ = کوڈ فارمیٹ سیٹ کرتا ہے کہ کوڈ کو کیسے واپس کیا جائے گا (پرانے اجازت کے طریقہ کار کا حوالہ، جب صارف نے براؤزر سے کوڈ کو اسکرپٹ میں کاپی کیا)
گنجائش رسائی کی گنجائش اور قسم کی نشاندہی کرتا ہے۔ انہیں خالی جگہوں یا %20 (یو آر ایل انکوڈنگ کے مطابق) سے الگ کیا جانا چاہیے۔ اقسام کے ساتھ رسائی والے علاقوں کی فہرست یہاں دیکھی جا سکتی ہے: Google APIs کے لیے OAuth 2.0 اسکوپس.

اجازت کا کوڈ حاصل کرنے کے بعد، ایپلیکیشن براؤزر کو ایک قریبی پیغام واپس کرے گی، پورٹ پر سننا بند کر دے گی اور ٹوکن حاصل کرنے کے لیے POST کی درخواست بھیجے گی۔ ہم اس میں کنسول API سے پہلے سے مخصوص آئی ڈی اور راز کی نشاندہی کرتے ہیں، وہ پتہ جس پر صارف کو ری ڈائریکٹ کیا جائے گا اور پروٹوکول کی تفصیلات کے مطابق grant_type دیا جائے گا۔

جواب میں، ہمیں ایک رسائی ٹوکن، سیکنڈوں میں اس کی میعاد کی مدت، اور ایک ریفریش ٹوکن ملے گا، جس کے ساتھ ہم رسائی ٹوکن کو اپ ڈیٹ کر سکتے ہیں۔

ایپلیکیشن کو ٹوکنز کو محفوظ جگہ پر محفوظ کرنا چاہیے جس میں طویل شیلف لائف ہے، لہذا جب تک ہم موصول ہونے والی رسائی کو منسوخ نہیں کرتے، ایپلیکیشن ریفریش ٹوکن واپس نہیں کرے گی۔ آخر میں، میں نے ٹوکن کو منسوخ کرنے کی درخواست شامل کی؛ اگر درخواست کامیابی کے ساتھ مکمل نہیں ہوئی اور ریفریش ٹوکن واپس نہیں کیا گیا، تو یہ طریقہ کار دوبارہ شروع کر دے گا (ہم نے ٹرمینل پر مقامی طور پر ٹوکنز کو ذخیرہ کرنا غیر محفوظ سمجھا، اور ہم ایسا نہیں کرتے۔ خفیہ نگاری کے ساتھ چیزوں کو پیچیدہ نہیں کرنا چاہتے یا براؤزر کو کثرت سے کھولنا نہیں چاہتے ہیں)۔

do {
  $token_result = Get-GoogleAuthToken
  $token = $token_result.access_token
  if ($token_result.refresh_token -eq $null) {
    Write-Host ("Session is not destroyed. Revoking token...")
    Invoke-WebRequest -Uri ("https://accounts.google.com/o/oauth2/revoke?token="+$token)
  }
} while ($token_result.refresh_token -eq $null)
$refresh_token = $token_result.refresh_token
$minute = ([int]("{0:mm}" -f ([timespan]::fromseconds($token_result.expires_in))))+((Get-date).Minute)-2
if ($minute -lt 0) {$minute += 60}
elseif ($minute -gt 59) {$minute -=60}
$token_expire = @{
  hour = ([int]("{0:hh}" -f ([timespan]::fromseconds($token_result.expires_in))))+((Get-date).Hour)
  minute = $minute
}

جیسا کہ آپ نے پہلے ہی دیکھا ہے، ٹوکن کو کالعدم کرتے وقت Invoke-WebRequest استعمال کیا جاتا ہے۔ Invoke-RestMethod کے برعکس، یہ موصول ہونے والے ڈیٹا کو قابل استعمال فارمیٹ میں واپس نہیں کرتا اور درخواست کی حیثیت کو ظاہر کرتا ہے۔

اگلا، اسکرپٹ آپ سے لاگ ان + ای میل تیار کرتے ہوئے صارف کا پہلا اور آخری نام درج کرنے کو کہتا ہے۔

درخواستوں

اگلی درخواستیں یہ ہوں گی - سب سے پہلے، آپ کو یہ چیک کرنے کی ضرورت ہے کہ آیا ایک ہی لاگ ان والا صارف پہلے سے موجود ہے تاکہ نیا بنانے یا موجودہ کو فعال کرنے کا فیصلہ حاصل کیا جا سکے۔

میں نے سوئچ کا استعمال کرتے ہوئے تمام درخواستوں کو سلیکشن کے ساتھ ایک فنکشن کی شکل میں نافذ کرنے کا فیصلہ کیا:

function GoogleQuery {
  param (
    $type,
    $query
  )
  switch ($type) {
    "SearchAccount" {
      Return Invoke-RestMethod -Method Get -Uri "https://www.googleapis.com/admin/directory/v1/users" -Headers @{Authorization = "Bearer "+(Get-GoogleToken)} -Body @{
        domain = 'rocketguys.com'
        query  = "email:$query"
      }
    }
    "UpdateAccount" {
      $body = @{
        name  = @{
          givenName = $query['givenName']
          familyName = $query['familyName']
        }
        suspended = 'false'
        password = $query['password']
        changePasswordAtNextLogin = 'true'
        phones = @(@{
          primary = 'true'
          value = $query['phone']
          type = "mobile"
        })
        orgUnitPath = $query['orgunit']
      }
      Return Invoke-RestMethod -Method Put -Uri ("https://www.googleapis.com/admin/directory/v1/users/"+$query['email']) -Headers @{Authorization = "Bearer "+(Get-GoogleToken)} -Body (ConvertTo-Json $body) -ContentType 'application/json; charset=utf-8'
    }
    
    "CreateAccount" {
      $body = @{
        primaryEmail = $query['email']
        name  = @{
          givenName = $query['givenName']
          familyName = $query['familyName']
        }
        suspended = 'false'
        password = $query['password']
        changePasswordAtNextLogin = 'true'
        phones = @(@{
          primary = 'true'
          value = $query['phone']
          type = "mobile"
        })
        orgUnitPath = $query['orgunit']
      }
      Return Invoke-RestMethod -Method Post -Uri "https://www.googleapis.com/admin/directory/v1/users" -Headers @{Authorization = "Bearer "+(Get-GoogleToken)} -Body (ConvertTo-Json $body) -ContentType 'application/json; charset=utf-8'
    }
    "AddMember" {
      $body = @{
        userKey = $query['email']
      }
      $ifrequest = Invoke-RestMethod -Method Get -Uri "https://www.googleapis.com/admin/directory/v1/groups" -Headers @{Authorization = "Bearer "+(Get-GoogleToken)} -Body $body
      $array = @()
      foreach ($group in $ifrequest.groups) {$array += $group.email}
      if ($array -notcontains $query['groupkey']) {
        $body = @{
          email = $query['email']
          role = "MEMBER"
        }
        Return Invoke-RestMethod -Method Post -Uri ("https://www.googleapis.com/admin/directory/v1/groups/"+$query['groupkey']+"/members") -Headers @{Authorization = "Bearer "+(Get-GoogleToken)} -Body (ConvertTo-Json $body) -ContentType 'application/json; charset=utf-8'
      } else {
        Return ($query['email']+" now is a member of "+$query['groupkey'])
      }
    }
  }
}

ہر درخواست میں، آپ کو ایک اتھورائزیشن ہیڈر بھیجنے کی ضرورت ہے جس میں ٹوکن کی قسم اور خود رسائی ٹوکن ہو۔ فی الحال، ٹوکن کی قسم ہمیشہ بیئرر ہوتی ہے۔ کیونکہ ہمیں یہ چیک کرنے کی ضرورت ہے کہ ٹوکن کی میعاد ختم نہیں ہوئی ہے اور اسے جاری ہونے کے ایک گھنٹہ بعد اپ ڈیٹ کرنا ہے، میں نے ایک دوسرے فنکشن کے لیے درخواست کی وضاحت کی ہے جو ایک رسائی ٹوکن واپس کرتا ہے۔ پہلا ایکسیس ٹوکن وصول کرتے وقت کوڈ کا وہی ٹکڑا اسکرپٹ کے شروع میں ہوتا ہے:

function Get-GoogleToken {
  if (((Get-date).Hour -gt $token_expire.hour) -or (((Get-date).Hour -ge $token_expire.hour) -and ((Get-date).Minute -gt $token_expire.minute))) {
  Write-Host "Token Expired. Refreshing..."
    $request = (Invoke-RestMethod -Method Post -Uri "https://www.googleapis.com/oauth2/v4/token" -ContentType 'application/x-www-form-urlencoded' -Body @{
      client_id = $client_id
      client_secret = $client_secret
      refresh_token = $refresh_token
      grant_type = 'refresh_token'
    })
    $token = $request.access_token
    $minute = ([int]("{0:mm}" -f ([timespan]::fromseconds($request.expires_in))))+((Get-date).Minute)-2
    if ($minute -lt 0) {$minute += 60}
    elseif ($minute -gt 59) {$minute -=60}
    $script:token_expire = @{
      hour = ([int]("{0:hh}" -f ([timespan]::fromseconds($request.expires_in))))+((Get-date).Hour)
      minute = $minute
    }
  }
  return $token
}

وجود کے لیے لاگ ان کی جانچ کرنا:

function Check_Google {
  $query = (GoogleQuery 'SearchAccount' $username)
  if ($query.users -ne $null) {
    $user = $query.users[0]
    Write-Host $user.name.fullName' - '$user.PrimaryEmail' - suspended: '$user.Suspended
    $GAresult = $user
  }
  if ($GAresult) {
      $return = $GAresult
  } else {$return = 'gg'}
  return $return
}

ای میل:$ استفسار کی درخواست API سے بالکل اسی ای میل والے صارف کو تلاش کرنے کو کہے گی، بشمول عرفی نام۔ آپ وائلڈ کارڈ بھی استعمال کر سکتے ہیں: =، :، :{PREFIX}*.

ڈیٹا حاصل کرنے کے لیے، GET درخواست کا طریقہ استعمال کریں، ڈیٹا داخل کرنے کے لیے (اکاؤنٹ بنانا یا کسی گروپ میں ممبر شامل کرنا) - POST، موجودہ ڈیٹا کو اپ ڈیٹ کرنے کے لیے - PUT، ریکارڈ کو حذف کرنے کے لیے (مثال کے طور پر، کسی گروپ سے ممبر) - حذف کریں۔

اسکرپٹ فون نمبر (ایک غیر تصدیق شدہ سٹرنگ) اور علاقائی تقسیم گروپ میں شامل کرنے کے لیے بھی کہے گا۔ یہ فیصلہ کرتا ہے کہ منتخب ایکٹو ڈائریکٹری OU کی بنیاد پر صارف کو کون سی تنظیمی اکائی ہونی چاہیے اور پاس ورڈ کے ساتھ آتا ہے:

do {
  $phone = Read-Host "Телефон в формате +7хххххххх"
} while (-not $phone)
do {
    $moscow = Read-Host "В Московский офис? (y/n) "
} while (-not (($moscow -eq 'y') -or ($moscow -eq 'n')))
$orgunit = '/'
if ($OU -like "*OU=Delivery,OU=Users,OU=ROOT,DC=rocket,DC=local") {
    Write-host "Будет создана в /Team delivery"
    $orgunit = "/Team delivery"
}
$Password =  -join ( 48..57 + 65..90 + 97..122 | Get-Random -Count 12 | % {[char]$_})+"*Ba"

اور پھر وہ اکاؤنٹ میں ہیرا پھیری کرنا شروع کر دیتا ہے:

$query = @{
  email = $email
  givenName = $firstname
  familyName = $lastname
  password = $password
  phone = $phone
  orgunit = $orgunit
}
if ($GMailExist) {
  Write-Host "Запускаем изменение аккаунта" -f mag
  (GoogleQuery 'UpdateAccount' $query) | fl
  write-host "Не забудь проверить группы у включенного $Username в Google."
} else {
  Write-Host "Запускаем создание аккаунта" -f mag
  (GoogleQuery 'CreateAccount' $query) | fl
}
if ($moscow -eq "y"){
  write-host "Добавляем в группу moscowoffice"
  $query = @{
    groupkey = '[email protected]'
    email = $email
  }
  (GoogleQuery 'AddMember' $query) | fl
}

اکاؤنٹ کو اپ ڈیٹ کرنے اور بنانے کے فنکشنز میں ایک ہی ترکیب ہوتی ہے؛ تمام اضافی فیلڈز کی ضرورت نہیں ہوتی؛ فون نمبرز والے سیکشن میں، آپ کو ایک صف کی وضاحت کرنے کی ضرورت ہوتی ہے جس میں نمبر اور اس کی قسم کے ساتھ ایک اندراج تک ہو سکتا ہے۔

کسی صارف کو کسی گروپ میں شامل کرتے وقت غلطی نہ ہونے کے لیے، ہم پہلے خود صارف سے گروپ ممبران یا کمپوزیشن کی فہرست حاصل کر کے چیک کر سکتے ہیں کہ آیا وہ پہلے سے ہی اس گروپ کا ممبر ہے۔

کسی مخصوص صارف کی گروپ کی رکنیت کے بارے میں استفسار کرنا تکراری نہیں ہوگا اور صرف براہ راست رکنیت دکھائے گا۔ پیرنٹ گروپ میں ایک صارف کو شامل کرنا جس کے پاس پہلے سے ہی چائلڈ گروپ ہے جس کا صارف ممبر ہے کامیاب ہوگا۔

حاصل يہ ہوا

باقی صرف صارف کو نئے اکاؤنٹ کا پاس ورڈ بھیجنا ہے۔ ہم یہ ایس ایم ایس کے ذریعے کرتے ہیں، اور ہدایات کے ساتھ عام معلومات بھیجتے ہیں اور ذاتی ای میل پر لاگ ان کرتے ہیں، جو کہ ایک فون نمبر کے ساتھ، محکمہ بھرتی کی طرف سے فراہم کیا گیا تھا۔ متبادل کے طور پر، آپ پیسے بچا سکتے ہیں اور خفیہ ٹیلی گرام چیٹ پر اپنا پاس ورڈ بھیج سکتے ہیں، جسے دوسرا عنصر بھی سمجھا جا سکتا ہے (میک بکس مستثنیٰ ہوں گے)۔

آخر تک پڑھنے کا شکریہ۔ مجھے آرٹیکل لکھنے کے انداز کو بہتر بنانے کے لیے تجاویز دیکھ کر خوشی ہوگی اور میں چاہتا ہوں کہ سکرپٹ لکھتے وقت آپ کم غلطیاں پکڑیں ​​=)

ان لنکس کی فہرست جو موضوعی طور پر مفید ہو سکتے ہیں یا محض سوالات کے جوابات دے سکتے ہیں:

ماخذ: www.habr.com

نیا تبصرہ شامل کریں