"""Simple TUI utility for finding free Telegram nicknames. The checker is best-effort and uses public web pages from Telegram and Fragment. It handles 429/5xx responses with retries and exponential backoff. """ from __future__ import annotations import argparse import concurrent.futures as futures from dataclasses import dataclass from html import unescape from pathlib import Path import random import re import threading import time from typing import Iterable from urllib import error, parse, request USERNAME_RE = re.compile(r"^[a-z0-9_]{1,32}$") DEFAULT_HEADERS = { "User-Agent": ( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/125.0 Safari/537.36" ), "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.8", "Connection": "close", } @dataclass(frozen=True) class FetchResult: url: str status: int headers: dict[str, str] body: bytes final_url: str @dataclass(frozen=True) class CheckResult: username: str telegram_state: str fragment_state: str telegram_reason: str fragment_reason: str @property def is_free(self) -> bool: return self.telegram_state == "free" and self.fragment_state == "free" class HostThrottle: def __init__(self, min_interval: float = 0.25) -> None: self.min_interval = min_interval self._lock = threading.Lock() self._next_allowed: dict[str, float] = {} def wait(self, host: str) -> None: with self._lock: now = time.monotonic() ready_at = self._next_allowed.get(host, now) delay = max(0.0, ready_at - now) self._next_allowed[host] = max(ready_at, now) + self.min_interval if delay > 0: time.sleep(delay) THROTTLE = HostThrottle() class SafeRedirectHandler(request.HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): parsed = parse.urlparse(newurl) if parsed.scheme and parsed.scheme not in {"http", "https"}: raise error.HTTPError(req.full_url, code, msg, headers, fp) return super().redirect_request(req, fp, code, msg, headers, newurl) OPENER = request.build_opener(SafeRedirectHandler()) def fetch_url(url: str, timeout: float = 12.0, retries: int = 5) -> FetchResult: host = parse.urlparse(url).netloc backoff = 1.0 last_exc: Exception | None = None for attempt in range(retries + 1): THROTTLE.wait(host) req = request.Request(url, headers=DEFAULT_HEADERS, method="GET") try: with OPENER.open(req, timeout=timeout) as resp: body = resp.read() headers = {k: v for k, v in resp.headers.items()} return FetchResult( url=url, status=getattr(resp, "status", resp.getcode()), headers=headers, body=body, final_url=resp.geturl(), ) except error.HTTPError as exc: body = exc.read() if getattr(exc, "fp", None) else b"" headers = {k: v for k, v in (exc.headers.items() if exc.headers else [])} status = exc.code if status == 429 or status >= 500: retry_after = headers.get("Retry-After") sleep_for = _retry_sleep(retry_after, backoff) if attempt < retries: time.sleep(sleep_for) backoff = min(backoff * 2, 30.0) continue return FetchResult( url=url, status=status, headers=headers, body=body, final_url=getattr(exc, "url", url) or url, ) except (error.URLError, TimeoutError, OSError) as exc: last_exc = exc if attempt < retries: time.sleep(backoff + random.uniform(0.0, 0.3)) backoff = min(backoff * 2, 30.0) continue break raise RuntimeError(f"Failed to fetch {url}: {last_exc}") def _retry_sleep(retry_after: str | None, backoff: float) -> float: if retry_after: try: return max(1.0, float(retry_after)) + random.uniform(0.0, 0.5) except ValueError: pass return backoff + random.uniform(0.0, 0.5) def load_usernames(path: Path) -> list[str]: if not path.exists(): raise FileNotFoundError(path) items: list[str] = [] seen: set[str] = set() for raw_line in path.read_text(encoding="utf-8-sig", errors="ignore").splitlines(): line = raw_line.strip() if not line or line.startswith("#"): continue username = normalize_username(line) if username is None: continue if username in seen: continue seen.add(username) items.append(username) return items def normalize_username(value: str) -> str | None: value = value.strip().lstrip("@").lower() if not value: return None if not USERNAME_RE.fullmatch(value): return None return value def estimate_duration(entries: int, workers: int) -> str: if entries <= 0: return "0s" workers = max(workers, 1) estimated_seconds = entries * 1.6 / workers return format_duration(estimated_seconds) def format_duration(seconds: float) -> str: total = max(0, int(round(seconds))) minutes, secs = divmod(total, 60) hours, minutes = divmod(minutes, 60) if hours: return f"{hours}h {minutes}m {secs}s" if minutes: return f"{minutes}m {secs}s" return f"{secs}s" def check_username(username: str, timeout: float = 12.0) -> CheckResult: telegram_url = f"https://t.me/{parse.quote(username)}" fragment_url = f"https://fragment.com/username/{parse.quote(username)}" telegram = fetch_url(telegram_url, timeout=timeout) fragment = fetch_url(fragment_url, timeout=timeout) telegram_state, telegram_reason = classify_telegram(username, telegram) fragment_state, fragment_reason = classify_fragment(username, fragment) return CheckResult( username=username, telegram_state=telegram_state, fragment_state=fragment_state, telegram_reason=telegram_reason, fragment_reason=fragment_reason, ) def classify_telegram(username: str, response: FetchResult) -> tuple[str, str]: text = decode_text(response.body) normalized = collapse_spaces(text.lower()) title = extract_tag_content(text, "title") title_norm = collapse_spaces(title.lower()) if title else "" og_description = extract_meta_content(text, "og:description") og_description_norm = collapse_spaces(og_description.lower()) if og_description else "" free_markers = ( "sorry, this page isn't available", "sorry, this page is unavailable", "this page is unavailable", "page not found", "username is unavailable", "username not available", "doesn't seem to exist", "does not seem to exist", ) if response.status in {404, 410}: return "free", f"HTTP {response.status}" for marker in free_markers: if marker in normalized: return "free", marker if title_norm.startswith(f"telegram: contact @{username}"): return "free", title_norm if title_norm.startswith(f"telegram: view @{username}"): return "taken", title_norm if f"you can contact @{username} right away" in og_description_norm: return "free", og_description_norm if f"you can view and join @{username} right away" in og_description_norm: return "taken", og_description_norm if "view in telegram" in normalized or "preview channel" in normalized: return "taken", "telegram view marker" if "send message" in normalized and f"contact @{username}" not in normalized: return "taken", "telegram send message marker" if response.status == 200: return "unknown", "telegram response is ambiguous" return "unknown", f"HTTP {response.status}" def classify_fragment(username: str, response: FetchResult) -> tuple[str, str]: text = decode_text(response.body) normalized = collapse_spaces(text.lower()) row_html = extract_fragment_row(text, username) row_normalized = collapse_spaces(row_html.lower()) if row_html else "" free_markers = ( "currently not for sale", "not for sale", "coming soon", "address unavailable", ) sale_markers = ( "on auction", "for sale", "sold", "place bid", "buy now", "make an offer", "minimum bid", "highest bid", "auction ends in", "resale", ) if response.status in {404, 410}: return "free", f"HTTP {response.status}" # Prefer the result row for the specific username when present. search_space = row_normalized or normalized for marker in free_markers: if marker in search_space: return "free", marker if row_html and any(marker in row_normalized for marker in sale_markers): return "taken", first_marker(row_normalized, sale_markers) or "sale marker" # Fallback for pages that don't expose a direct row snippet. for marker in sale_markers: if marker in normalized: return "taken", marker if response.status == 200: return "unknown", "no fragment listing detected" if response.status >= 500: return "unknown", f"HTTP {response.status}" return "unknown", f"HTTP {response.status}" def extract_fragment_row(text: str, username: str) -> str: username_escaped = re.escape(username) patterns = ( rf"