#!/usr/bin/env python3 """Badger Uploader one-time Google sign-in (installed-app flow, PKCE). yt_auth.py url -> prints the sign-in link, remembers the PKCE verifier yt_auth.py code -> takes the http://localhost/?code=... address the browser ended on, stores the refresh token as ~/.config/argus/youtube/token_.json (600) One token per CHANNEL: a Brand Account channel is chosen on Google's consent screen.""" import base64, hashlib, json, os, secrets, sys, urllib.parse, urllib.request D = os.path.expanduser('~/.config/argus/youtube') C = json.load(open(f'{D}/client_secret.json'))['installed'] SCOPES = 'https://www.googleapis.com/auth/youtube.upload https://www.googleapis.com/auth/youtube' REDIRECT = 'http://localhost' def w600(p, obj): fd = os.open(p, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) with os.fdopen(fd, 'w') as f: json.dump(obj, f, indent=1) cmd, name = sys.argv[1], sys.argv[2] if cmd == 'url': ver = secrets.token_urlsafe(64); state = secrets.token_urlsafe(16) ch = base64.urlsafe_b64encode(hashlib.sha256(ver.encode()).digest()).rstrip(b'=').decode() w600(f'{D}/pending_{name}.json', {'verifier': ver, 'state': state}) print(C['auth_uri'] + '?' + urllib.parse.urlencode({'client_id': C['client_id'], 'redirect_uri': REDIRECT, 'response_type': 'code', 'scope': SCOPES, 'access_type': 'offline', 'prompt': 'consent select_account', 'code_challenge': ch, 'code_challenge_method': 'S256', 'state': state})) elif cmd == 'code': p = json.load(open(f'{D}/pending_{name}.json')) q = urllib.parse.parse_qs(urllib.parse.urlparse(sys.argv[3]).query) if q.get('state', [''])[0] != p['state']: sys.exit('state mismatch: use the link from the latest url step') body = urllib.parse.urlencode({'code': q['code'][0], 'client_id': C['client_id'], 'client_secret': C['client_secret'], 'redirect_uri': REDIRECT, 'grant_type': 'authorization_code', 'code_verifier': p['verifier']}).encode() tok = json.load(urllib.request.urlopen(C['token_uri'], body)) if 'refresh_token' not in tok: sys.exit('no refresh token returned: ' + json.dumps(tok)[:300]) w600(f'{D}/token_{name}.json', {'refresh_token': tok['refresh_token'], 'scope': tok.get('scope')}) os.remove(f'{D}/pending_{name}.json') req = urllib.request.Request('https://www.googleapis.com/youtube/v3/channels?part=snippet&mine=true', headers={'Authorization': 'Bearer ' + tok['access_token']}) for it in json.load(urllib.request.urlopen(req)).get('items', []): print('signed in to channel:', it['snippet']['title'], it['id'])