Files
Free-Telegram-Finder/telegram_checker.py
T
2026-07-14 08:59:05 +05:00

109 lines
4.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import asyncio
import random
import re
import aiohttp
from config import (
HEADERS,
TG_JITTER,
TG_MAX_HEAD_BYTES,
TG_MIN_INTERVAL,
TG_REQUEST_TIMEOUT,
TG_RETRIES,
)
from rate_limiter import RateLimiter
OG_TITLE_RE = re.compile(rb'property=["\']og:title["\']\s+content=["\']([^"\']*)["\']')
OG_IMAGE_RE = re.compile(rb'property=["\']og:image["\']\s+content=["\']([^"\']*)["\']')
OG_DESC_RE = re.compile(
rb'property=["\']og:description["\']\s+content=["\']([^"\']*)["\']'
)
# один общий лимитер на всё приложение: t.me не должен видеть больше одного
# запроса раз в TG_MIN_INTERVAL секунд, сколько бы задач ни ждало своей очереди
_rate_limiter = RateLimiter(min_interval=TG_MIN_INTERVAL, jitter=TG_JITTER)
async def _fetch_head(session, url: str, max_bytes: int = TG_MAX_HEAD_BYTES) -> bytes:
"""Читает страницу кусками и останавливается сразу после </head> -
сам текст страницы не нужен, только og-теги в шапке."""
timeout = aiohttp.ClientTimeout(total=TG_REQUEST_TIMEOUT)
async with session.get(url, timeout=timeout, headers=HEADERS) as resp:
if resp.status != 200:
raise aiohttp.ClientResponseError(
resp.request_info, resp.history, status=resp.status
)
buf = b""
async for chunk in resp.content.iter_chunked(1024):
buf += chunk
if b"</head>" in buf or len(buf) >= max_bytes:
break
return buf
def _extract(pattern, html: bytes) -> str:
match = pattern.search(html)
return match.group(1).decode() if match else ""
def _looks_unregistered(
username: str, title: str, image: str, description: str
) -> bool:
"""Для несуществующих/удалённых юзернеймов телега всегда рисует одну и
ту же болванку: заголовок "Telegram: Contact @username", дефолтный
логотип и пустое описание. Если все три совпали - юзернейм свободен."""
expected_title = f"telegram: contact @{username}".lower()
return (
title.lower() == expected_title
and image == "https://telegram.org/img/t_logo_2x.png"
and description == ""
)
async def check_telegram(session, tg_url: str) -> str | None:
"""None - юзернейм занят или проверить не вышло. Строка - похоже свободен."""
username = tg_url.rstrip("/").split("/")[-1]
if not username:
print(f"Кривая ссылка на тг: {tg_url}")
return None
html = None
for attempt in range(1, TG_RETRIES + 1):
await _rate_limiter.wait()
try:
html = await _fetch_head(session, f"https://t.me/{username}")
break
except (aiohttp.ClientError, asyncio.TimeoutError) as e:
backoff = 2**attempt + random.uniform(0, 1)
print(
f" @{username}: попытка {attempt}/{TG_RETRIES} не удалась ({e}), жду {backoff:.1f}с"
)
await asyncio.sleep(backoff)
if html is None:
print(
f" @{username}: не смог получить страницу за {TG_RETRIES} попыток, пропускаю"
)
return None
title = _extract(OG_TITLE_RE, html)
image = _extract(OG_IMAGE_RE, html)
description = _extract(OG_DESC_RE, html)
print(f" @{username}: title={title!r} image={image!r} desc={description!r}")
if not title and not image and not description:
# пустые og-теги почти всегда значат, что нас притормозили,
# а не что страница правда пустая - за "свободно" это не считаем
print(f" @{username}: пустой ответ, похоже попали под лимит - пропускаю")
return None
if _looks_unregistered(username, title, image, description):
print(f" @{username}: похоже свободен")
return username
print(f" @{username}: занят")
return None