"""One-time local OAuth helper; Python 3.10+, standard library only. This file is delivered by the user's own assistant server. No credentials are saved on the computer or sent to Telegram. OpenAI tokens travel over HTTPS to that server, which validates the signed identity before encrypted storage. """ import base64 import hashlib import hmac import http.server import json import secrets import threading import time import urllib.error import urllib.parse import urllib.request import webbrowser SERVER = 'https://ilya-akto-bot-6830c6.apps.dada-tuda.ru' REDIRECT = "http://127.0.0.1:1455/auth/callback" def post(url, data, form=False): body = urllib.parse.urlencode(data).encode() if form else json.dumps(data).encode() request = urllib.request.Request(url, data=body, headers={ "Content-Type": "application/x-www-form-urlencoded" if form else "application/json", }) with urllib.request.urlopen(request, timeout=40) as response: return json.load(response) def main(): if not SERVER.startswith('https://'): raise RuntimeError('Скачайте файл со страницы подключения вашего помощника.') print('Подключение подписки ChatGPT к Личному помощнику Ильи.') print('Пароль вводится только на сайте OpenAI. В этот файл и Telegram он не передаётся.') code = input('Вставьте одноразовый код из команды /connect в Telegram: ').strip() verifier = secrets.token_urlsafe(64) challenge = base64.urlsafe_b64encode(hashlib.sha256(verifier.encode()).digest()).rstrip(b'=').decode() received = {} event = threading.Event() expected_state = '' class Handler(http.server.BaseHTTPRequestHandler): def log_message(self, *args): pass # Never log authorization codes or callback queries. def do_GET(self): path = urllib.parse.urlparse(self.path) params = urllib.parse.parse_qs(path.query) state = params.get('state', [''])[0] valid = path.path == '/auth/callback' and hmac.compare_digest(state, expected_state) self.send_response(200 if valid else 400) self.send_header('Content-Type', 'text/html; charset=utf-8') self.send_header('Cache-Control', 'no-store') self.end_headers() if not valid: self.wfile.write('Некорректный возврат. Продолжите вход в окне программы.'.encode()) return received.update({key: values[0] for key, values in params.items()}) self.wfile.write('
Вернитесь к программе подключения.
'.encode()) event.set() listener = http.server.HTTPServer(('127.0.0.1', 1455), Handler) thread = threading.Thread(target=listener.serve_forever, daemon=True) thread.start() try: setup = post(SERVER + '/auth/start', {'code': code, 'challenge': challenge, 'redirect_uri': REDIRECT}) expected_state = setup['params']['state'] auth_url = setup['authorize_url'] + '?' + urllib.parse.urlencode(setup['params']) print('Открываю официальный сайт OpenAI. Разрешите помощнику использовать лимиты подписки.') webbrowser.open(auth_url) # New-registration URL contains PKCE challenge/state, no OAuth tokens. print('Если браузер не открылся, откройте эту ссылку на этом компьютере:\n' + auth_url) if not event.wait(900): raise RuntimeError('Время входа истекло. Получите новый код через /connect.') if received.get('error'): raise RuntimeError('Вход отменён или недоступен. Подписка не подключена.') if not hmac.compare_digest(received.get('state', ''), expected_state): raise RuntimeError('Не удалось проверить возврат после входа.') previous = setup['params']['client_id'] issued = received.get('client_id') or (previous if previous != 'dynamic_agent_client' else '') if not issued or issued == 'dynamic_agent_client' or (previous != 'dynamic_agent_client' and issued != previous): raise RuntimeError('OpenAI не завершил регистрацию помощника.') tokens = post('https://auth.openai.com/api/accounts/oauth/token', { 'grant_type': 'authorization_code', 'client_id': issued, 'code': received['code'], 'code_verifier': verifier, 'redirect_uri': REDIRECT, 'resource': 'https://api.openai.com/v1', }, form=True) tokens['client_id'] = issued result = post(SERVER + '/auth/import', {'code': code, 'credentials': tokens}) if not result.get('ok'): raise RuntimeError('Сервер не подтвердил подключение.') print('\nГотово! В Telegram отправьте /status, затем обычное сообщение.') finally: listener.shutdown() listener.server_close() if __name__ == '__main__': try: main() except urllib.error.HTTPError as error: # No response bodies or credentials in console output. print(f'Не удалось завершить вход (HTTP {error.code}). Получите новый код через /connect.') except Exception as error: if isinstance(error, RuntimeError): print(str(error)) else: print('Не удалось завершить вход. Проверьте интернет и освободите локальный порт 1455.') input('\nНажмите Enter, чтобы закрыть окно.')