#!/usr/bin/env python3 """GetTGAPI v1 example: resumable writes and explicit result deletion. Python 3.10+, pip install requests. No real keys are included. The private work directory contains application metadata and, on success, credentials. """ import argparse import getpass import json import os from pathlib import Path import random import time import uuid import requests BASE = "https://gettgapi.com/api/open/v1" def private_write(path, value): tmp = path.with_name(path.name + "." + uuid.uuid4().hex) fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) with os.fdopen(fd, "w", encoding="utf-8") as stream: json.dump(value, stream, ensure_ascii=False, indent=2) stream.flush() os.fsync(stream.fileno()) os.replace(tmp, path) # On POSIX persist the directory entry before acknowledging/deleting remotely. if os.name == "posix": directory = os.open(path.parent, os.O_RDONLY) try: os.fsync(directory) finally: os.close(directory) class Api: def __init__(self, key): self.session = requests.Session() self.session.trust_env = False self.session.headers.update({"Authorization": "Bearer " + key, "Accept": "application/json"}) def call(self, method, path, body=None, key=None): headers = {"Idempotency-Key": key} if key else {} for attempt in range(6): try: response = self.session.request(method, BASE + path, json=body, headers=headers, timeout=(10, 40), allow_redirects=False) except requests.RequestException: # Do not log exception objects: they may retain request headers. if attempt == 5: raise RuntimeError("连接未确认,请稍后以同一目录重试") from None time.sleep(min(60, 2 ** attempt) + random.random()) continue if response.status_code in {408, 429, 500, 502, 503, 504} and attempt < 5: try: wait = max(1, min(3600, int(response.headers.get("Retry-After", "30")))) except ValueError: wait = 30 time.sleep(wait + random.random()) continue try: value = response.json() except ValueError: raise RuntimeError("响应未确认,请稍后以同一目录重试") from None if not response.ok: error = value.get("error", {}) raise RuntimeError(f"{error.get('code', 'REQUEST_FAILED')} · request_id={value.get('request_id', '')}") return value["data"] raise RuntimeError("重试未确认,请保留目录后稍后继续") def main(): parser = argparse.ArgumentParser() parser.add_argument("--work-dir", required=True, type=Path) args = parser.parse_args() folder = args.work_dir.resolve() folder.mkdir(mode=0o700, parents=True, exist_ok=True) if os.name == "posix": folder.chmod(0o700) state_path, result_path = folder / "state.json", folder / "result.json" api = Api(getpass.getpass("授权码(不回显):").strip()) state = json.loads(state_path.read_text()) if state_path.exists() else {} if not state: print("可用次数:", api.call("GET", "/balance")["available"]) if input("确认有权使用此号码?输入 yes:").strip() != "yes": return state = {"create_key": str(uuid.uuid4()), "input": {"phone": input("含国家区号的号码:"), "title": input("应用名称(回车使用默认):") or "My Telegram App", "short_name": input("应用短名(5–32 位字母数字下划线,字母开头):"), "consent": True}} private_write(state_path, state) if not state.get("id"): application = api.call("POST", "/applications", state["input"], state["create_key"]) state["id"] = application["id"] private_write(state_path, state) path = "/applications/" + state["id"] # Pending code is intentionally not stored. On uncertain submit, query first on resume. deadline = time.monotonic() + 3600 while time.monotonic() < deadline: application = api.call("GET", path) status = application["status"] print("状态:", status, "申请:", state["id"]) if status == "succeeded": if not result_path.exists(): private_write(result_path, api.call("GET", path + "/result")) # Read back the local file before offering deletion. saved = json.loads(result_path.read_text()) if not saved.get("api_id") or len(saved.get("api_hash", "")) != 32: raise RuntimeError("本地结果不完整,不会删除平台结果") print("结果已保存到受限文件:", result_path) if input("已确认保存,要清除平台临时副本吗?输入 delete:").strip() == "delete": state.setdefault("delete_key", str(uuid.uuid4())) private_write(state_path, state) receipt = api.call("POST", path + "/result/deletion", {"confirm": True}, state["delete_key"]) private_write(folder / "deletion.json", receipt) print("已清除平台临时副本。") return if status in {"failed", "cancelled", "duplicate", "deleted", "review_required"}: print("本轮已停止。请根据文档处理;需要人工协助时提供申请编号。") return if status == "waiting_code": challenge = application.get("challenge") if not challenge: print("验证已过期,请按文档取消或联系人工。") return pending = state.get("code_attempt", {}) if pending.get("challenge_id") != challenge["id"]: pending = {"challenge_id": challenge["id"], "key": str(uuid.uuid4())} state["code_attempt"] = pending private_write(state_path, state) code = getpass.getpass("本轮验证码(恢复未确认请求时输入原验证码):") api.call("POST", path + "/code", {"challenge_id": challenge["id"], "code": code}, pending["key"]) code = None time.sleep(max(5, application.get("poll_after_seconds", 5))) print("等待已达一小时,保留此目录,稍后重新运行继续查询。") if __name__ == "__main__": try: main() except (RuntimeError, KeyboardInterrupt) as error: print(str(error) or "已停止,请保留工作目录后恢复。")