From 919a9fff83af5e712a1bd4f1feb41febf887fbd0 Mon Sep 17 00:00:00 2001 From: dela Date: Tue, 6 Jan 2026 12:06:12 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90bot=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- checker/.env.example | 2 +- checker/pyproject.toml | 1 + checker/scripts/gencard.py | 154 +++++++++++++++++ checker/scripts/setup_with_uv.sh | 0 checker/scripts/start_bot.sh | 11 ++ checker/src/checker/bot/__init__.py | 3 + checker/src/checker/bot/handlers.py | 221 +++++++++++++++++++++++++ checker/src/checker/bot/main.py | 45 +++++ checker/src/checker/cards/__init__.py | 9 +- checker/src/checker/cards/generator.py | 114 +++++++++++++ checker/uv.lock | 87 ++++++++++ 11 files changed, 643 insertions(+), 4 deletions(-) create mode 100644 checker/scripts/gencard.py mode change 100644 => 100755 checker/scripts/setup_with_uv.sh create mode 100755 checker/scripts/start_bot.sh create mode 100644 checker/src/checker/bot/__init__.py create mode 100644 checker/src/checker/bot/handlers.py create mode 100644 checker/src/checker/bot/main.py create mode 100644 checker/src/checker/cards/generator.py diff --git a/checker/.env.example b/checker/.env.example index 64aa8cf..21ec75e 100644 --- a/checker/.env.example +++ b/checker/.env.example @@ -2,7 +2,7 @@ # 复制此文件为 .env 并填写实际值 # Telegram 通知配置(可选) -TELEGRAM_TOKEN= +TELEGRAM_TOKEN=8388442374:AAF5v5jZIMn2kvGmWMBfPtPUYuHdC5T59Y8 TELEGRAM_CHAT_ID= # 线程配置 diff --git a/checker/pyproject.toml b/checker/pyproject.toml index c4ff029..aff9e96 100644 --- a/checker/pyproject.toml +++ b/checker/pyproject.toml @@ -28,6 +28,7 @@ dependencies = [ "pycryptodome>=3.23.0", "v-jstools>=0.0.8", "python-telegram-bot>=20.0", + "dotenv>=0.9.9", ] [project.optional-dependencies] diff --git a/checker/scripts/gencard.py b/checker/scripts/gencard.py new file mode 100644 index 0000000..62d3ca0 --- /dev/null +++ b/checker/scripts/gencard.py @@ -0,0 +1,154 @@ +import random +import datetime +import re + +class CardGenerator: + def __init__(self): + self.current_year = datetime.datetime.now().year + + def luhn_checksum(self, card_number_str): + """计算 Luhn 校验和""" + digits = [int(d) for d in card_number_str] + checksum = 0 + is_second = False + for digit in reversed(digits): + if is_second: + digit *= 2 + if digit > 9: + digit -= 9 + checksum += digit + is_second = not is_second + return checksum + + def calculate_check_digit(self, card_number_without_check): + """计算最后一位校验位""" + temp_number = card_number_without_check + "0" + checksum = self.luhn_checksum(temp_number) + if checksum % 10 == 0: + return "0" + else: + return str(10 - (checksum % 10)) + + def generate_card_number(self, pattern): + """ + 核心生成逻辑:支持任意位置的 'x' + """ + # 1. 确定卡号长度标准 (Amex 15位,其他 16位) + # 如果 pattern 已经很长,就用 pattern 的长度,否则根据卡头判断 + target_len = 16 + if pattern.startswith(('34', '37')): + target_len = 15 + + # 2. 补全 pattern 到目标长度 (减去最后一位校验位) + # 如果输入的 pattern 比如是 "547958" (长度6),需要补 x 到 15位 (留一位给校验) + # 如果输入已经包含了 x 且长度足够,则保留原样 + + working_pattern = pattern.lower() + + # 移除可能存在的非数字/x字符 + working_pattern = re.sub(r'[^0-9x]', '', working_pattern) + + # 自动填充 x + if len(working_pattern) < target_len: + working_pattern += 'x' * (target_len - len(working_pattern)) + + # 截断到 倒数第二位 (最后一位是校验位) + # 注意:如果用户输入了完整的 16位 pattern 且最后一位不是 x,我们会覆盖它, + # 因为 Luhn 算法要求最后一位必须是计算出来的。 + # 除非用户就是想校验?但在生成逻辑里,我们通常假设最后一位是计算的。 + base_pattern = working_pattern[:target_len-1] + + generated_prefix = "" + for char in base_pattern: + if char == 'x': + generated_prefix += str(random.randint(0, 9)) + else: + generated_prefix += char + + # 计算校验位 + check_digit = self.calculate_check_digit(generated_prefix) + return generated_prefix + check_digit + + def parse_date_cvv(self, card_num, m_req, y_req, c_req): + """ + 处理日期和CVV的占位符逻辑 + """ + # --- Month --- + if m_req in ['(m)', 'rnd', '', None]: + month = str(random.randint(1, 12)).zfill(2) + else: + month = m_req.strip() # 使用用户指定的静态值,如 '05' + + # --- Year --- + if y_req in ['(y)', 'rnd', '', None]: + year = str(self.current_year + random.randint(2, 5)) + else: + year = y_req.strip() # 使用用户指定的静态值,如 '2023' 或 '23' + + # --- CVV --- + if c_req in ['(cvv)', 'rnd', '', None]: + if card_num.startswith(('34', '37')): + cvv = str(random.randint(1102, 9999)) + else: + cvv = str(random.randint(112, 999)) + else: + cvv = c_req.strip() + + return month, year, cvv + + def process_line(self, raw_input, count=1): + """ + 智能解析输入字符串并生成 + 支持格式: + - BIN only: 547958 + - Pattern: 460723xxxxxxxxxx + - Complex: 5319719xxx5xxx87 + - Full: 434256|11|2022|212 + - Templates: 434256|(m)|(y)|(cvv) + """ + # 默认值 + parts = raw_input.split('|') + + # 提取各个部分,没有的就设为 None,交给 parse_date_cvv 处理默认逻辑 + pattern_part = parts[0] if len(parts) > 0 else "" + month_part = parts[1] if len(parts) > 1 else None + year_part = parts[2] if len(parts) > 2 else None + cvv_part = parts[3] if len(parts) > 3 else None + + results = [] + for _ in range(count): + # 1. 生成卡号 + card_num = self.generate_card_number(pattern_part) + + # 2. 处理附属信息 + m, y, c = self.parse_date_cvv(card_num, month_part, year_part, cvv_part) + + results.append(f"{card_num}|{m}|{y}|{c}") + + return results + +# --- 测试与演示 --- + +if __name__ == "__main__": + gen = CardGenerator() + + # 你列出的所有测试用例 + test_cases = [ + "547958", # 仅 BIN + "460723xxxxxxxxxx", # 带图案 + "5319719xxx5xxx87", # 专业用户 (混合 pattern) + "434256|11|2022|212", # 你自己的固定模式 + "559917|11|2022|(cvv)", # 随机 CVV + "434256|(m)|(y)|(cvv)", # 全随机占位符 + "434256240669|(m)|(y)|(cvv)", # 短卡号 (没有最后4位) -> 会自动补全 + "53673712123xxxxx|05|2023|", # 指定长年份 + "53673712123xxxxx|05|23|" # 指定短年份 + ] + + print(f"{'INPUT FORMAT':<35} | {'GENERATED OUTPUT'}") + print("-" * 80) + + for case in test_cases: + # 每个案例生成 1 条作为演示 + output = gen.process_line(case, count=1)[0] + print(f"{case:<35} -> {output}") diff --git a/checker/scripts/setup_with_uv.sh b/checker/scripts/setup_with_uv.sh old mode 100644 new mode 100755 diff --git a/checker/scripts/start_bot.sh b/checker/scripts/start_bot.sh new file mode 100755 index 0000000..fa03d94 --- /dev/null +++ b/checker/scripts/start_bot.sh @@ -0,0 +1,11 @@ +#!/bin/bash +# 启动 Telegram Bot + +# 检查是否安装了 python-telegram-bot +if ! python -c "import telegram" &> /dev/null; then + echo "📦 安装 Bot 依赖..." + uv pip install "python-telegram-bot>=20.0" +fi + +echo "🚀 启动 Checker Bot..." +python -m checker.bot.main diff --git a/checker/src/checker/bot/__init__.py b/checker/src/checker/bot/__init__.py new file mode 100644 index 0000000..21db616 --- /dev/null +++ b/checker/src/checker/bot/__init__.py @@ -0,0 +1,3 @@ +from .main import main + +__all__ = ["main"] diff --git a/checker/src/checker/bot/handlers.py b/checker/src/checker/bot/handlers.py new file mode 100644 index 0000000..f95d2da --- /dev/null +++ b/checker/src/checker/bot/handlers.py @@ -0,0 +1,221 @@ +import logging +import asyncio +import re +import requests +from concurrent.futures import ThreadPoolExecutor +from telegram import Update +from telegram.ext import ContextTypes +from telegram.constants import ParseMode + +from ..models import Card, CheckStatus +from ..checkers import StripeChecker +from ..cards import generator + +logger = logging.getLogger(__name__) + +# 全局线程池,避免每个请求都创建 +# 可以根据服务器性能调整 max_workers +executor = ThreadPoolExecutor(max_workers=5) + +async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """响应 /start 命令""" + user = update.effective_user + await update.message.reply_html( + f"Hi {user.mention_html()}! 👋\n\n" + "我是 Gemini Checker Bot。\n\n" + "可用命令:\n" + "1. /chk - 检测卡片\n" + " /chk cc|mm|yy|cvv\n\n" + "2. /gen - 生成卡片\n" + " /gen 10 536737xxxx (生成10张)\n" + " /gen 10 536737xxxx /chk (生成并检测)" + ) + +async def gen_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """响应 /gen 命令""" + # 格式: /gen [/chk] + if not context.args or len(context.args) < 2: + await update.message.reply_html( + "❌ 格式错误\n\n" + "用法:\n" + "/gen 10 536737xxxxxxxxxx\n" + "/gen 10 536737xxxxxxxxxx /chk" + ) + return + + try: + count = int(context.args[0]) + pattern = context.args[1] + except ValueError: + await update.message.reply_text("❌ 数量必须是数字。") + return + + # 检查是否有 /chk + do_check = False + if len(context.args) > 2 and '/chk' in context.args[2:]: + do_check = True + + # 限制生成数量 + if count > 1000: + await update.message.reply_text("❌ 一次最多生成 1000 张卡片。") + return + + # 1. 生成卡片 + try: + cards_list = generator.process_line(pattern, count=count) + except Exception as e: + await update.message.reply_text(f"❌ 生成失败: {e}") + return + + # 2. 如果不需要检测,直接返回文本 + if not do_check: + response_text = "\n".join(cards_list) + # 避免消息过长 (Telegram 限制 4096 字符) + if len(response_text) > 4000: + # 如果太长,保存为文件发送 (简化处理,这里只截断) + response_text = response_text[:4000] + "\n... (truncated)" + + await update.message.reply_text( + f"✅ 生成了 {count} 张卡片:\n\n{response_text}", + parse_mode=ParseMode.HTML + ) + return + + # 3. 如果需要检测,复用检测逻辑 + # 将生成的卡片列表拼接成字符串,传给 process_input_text + input_text = "\n".join(cards_list) + + await update.message.reply_text( + f"✅ 已生成 {count} 张卡片,即将开始检测..." + ) + + await process_input_text(update, context, input_text) + + +async def chk_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """响应 /chk 命令""" + text = update.message.text + # 移除命令部分 (例如 /chk 或 /chk@botname) + # 使用 regex 替换开头的命令 + content = re.sub(r'^/\w+(@\w+)?\s*', '', text, count=1, flags=re.MULTILINE) + + if not content.strip(): + await update.message.reply_text("❌ 请在命令后提供卡片列表。") + return + + await process_input_text(update, context, content) + +async def process_input_text(update: Update, context: ContextTypes.DEFAULT_TYPE, text: str) -> None: + """统一处理输入文本""" + # 1. 解析卡片 + cards = [] + lines = text.splitlines() + for line in lines: + line = line.strip() + if not line: + continue + card = Card.parse(line) + if card: + cards.append(card) + + if not cards: + # 如果是直接消息,可能用户只是发了聊天内容,不是卡片 + # 如果是 /chk 命令进来的,已经在 chk_command 判空了,但这里是解析后的 + # 我们只在明确是命令或者看起来像卡片时才报错 + # 为简单起见,如果解析不到卡片,提示格式错误 + await update.message.reply_text("❌ 未找到有效卡片格式。请检查格式: cc|mm|yy|cvv") + return + + # 2. 发送开始消息 + status_msg = await update.message.reply_text( + f"🔍 收到 {len(cards)} 张卡片,开始检测..." + ) + + # 3. 初始化检测器 + # 注意: 这里不传入 telegram token,因为我们要自己控制回复 + checker = StripeChecker( + timeout=20, + max_retries=3 + ) + + stats = { + "live": 0, + "dead": 0, + "unknown": 0, + "total": len(cards), + "checked": 0 + } + + # 4. 创建并发任务 + tasks = [] + # 如果卡片太多,可能需要分批处理,这里简单起见直接全部提交 + for card in cards: + tasks.append(process_card(card, checker, update, stats)) + + # 等待所有任务完成 + await asyncio.gather(*tasks) + + # 5. 最终报告 + report = ( + f"🏁 检测完成\n\n" + f"Total: {stats['total']}\n" + f"Live: {stats['live']} ✅\n" + f"Dead: {stats['dead']} ❌\n" + f"Unknown: {stats['unknown']} ⚠️" + ) + await status_msg.edit_text(report, parse_mode=ParseMode.HTML) + +async def process_card( + card: Card, + checker: StripeChecker, + update: Update, + stats: dict +): + """异步处理单张卡片""" + loop = asyncio.get_running_loop() + + # 每个任务独立的 Session + session = requests.Session() + + try: + # 在线程池中运行阻塞的检测逻辑 + result = await loop.run_in_executor( + executor, + checker.check, + card, + session, + None # 暂时不使用代理,后续可扩展 + ) + + stats["checked"] += 1 + + if result.status == CheckStatus.LIVE: + stats["live"] += 1 + await send_live_alert(update, result) + elif result.status == CheckStatus.DEAD: + stats["dead"] += 1 + else: + stats["unknown"] += 1 + + except Exception as e: + logger.error(f"Error checking card {card.formatted}: {e}") + stats["unknown"] += 1 + finally: + session.close() + +async def send_live_alert(update: Update, result): + """发送存活卡片通知""" + card = result.card + bin_info = result.bin_info + + msg = ( + f"Approved ✅\n\n" + f"CC: {card.formatted}\n" + f"Status: {result.message}\n" + f"Info: {bin_info.brand} - {bin_info.card_type}\n" + f"Country: {bin_info.country} {bin_info.country_flag}\n" + f"Bank: {bin_info.bank}" + ) + # 使用 reply_html 回复原始消息 + await update.message.reply_html(msg) + diff --git a/checker/src/checker/bot/main.py b/checker/src/checker/bot/main.py new file mode 100644 index 0000000..7fec377 --- /dev/null +++ b/checker/src/checker/bot/main.py @@ -0,0 +1,45 @@ +import os +import logging +from telegram.ext import Application, CommandHandler, MessageHandler, filters +from dotenv import load_dotenv + +from .handlers import start, chk_command, gen_command + +# 加载 .env +load_dotenv() + +# 配置日志 +logging.basicConfig( + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + level=logging.INFO +) +logger = logging.getLogger(__name__) + +def main(): + """启动 Telegram Bot""" + token = os.getenv("TELEGRAM_TOKEN") + if not token: + logger.error("❌ 错误: 未在环境变量中找到 TELEGRAM_TOKEN") + print("请在 .env 文件中配置 TELEGRAM_TOKEN") + return + + # 检查是否配置了限制用户 + # 这里简单实现:如果有配置 ALLOWED_IDS,则在 handler 中过滤 + # 目前暂未在 handlers.py 中实现过滤逻辑,默认允许所有知道 bot 的人使用 + + print("🚀 正在启动 Checker Bot...") + + application = Application.builder().token(token).build() + + # 注册处理器 + application.add_handler(CommandHandler("start", start)) + application.add_handler(CommandHandler("chk", chk_command)) + application.add_handler(CommandHandler("gen", gen_command)) + + print("✅ Bot 已启动,正在等待消息...") + + # 运行 polling + application.run_polling() + +if __name__ == "__main__": + main() diff --git a/checker/src/checker/cards/__init__.py b/checker/src/checker/cards/__init__.py index 096df55..9103723 100644 --- a/checker/src/checker/cards/__init__.py +++ b/checker/src/checker/cards/__init__.py @@ -1,9 +1,12 @@ """卡片处理模块""" from .parser import parse_card_file, deduplicate_cards from .bin_lookup import lookup_bin +from .generator import generator, CardGenerator __all__ = [ - 'parse_card_file', - 'deduplicate_cards', - 'lookup_bin', + "parse_card_file", + "deduplicate_cards", + "lookup_bin", + "generator", + "CardGenerator" ] diff --git a/checker/src/checker/cards/generator.py b/checker/src/checker/cards/generator.py new file mode 100644 index 0000000..0ed838c --- /dev/null +++ b/checker/src/checker/cards/generator.py @@ -0,0 +1,114 @@ +import random +import datetime +import re +from typing import List, Tuple, Optional + +class CardGenerator: + """信用卡生成器""" + + def __init__(self): + self.current_year = datetime.datetime.now().year + + def luhn_checksum(self, card_number_str: str) -> int: + """计算 Luhn 校验和""" + digits = [int(d) for d in card_number_str] + checksum = 0 + is_second = False + for digit in reversed(digits): + if is_second: + digit *= 2 + if digit > 9: + digit -= 9 + checksum += digit + is_second = not is_second + return checksum + + def calculate_check_digit(self, card_number_without_check: str) -> str: + """计算最后一位校验位""" + temp_number = card_number_without_check + "0" + checksum = self.luhn_checksum(temp_number) + if checksum % 10 == 0: + return "0" + else: + return str(10 - (checksum % 10)) + + def generate_card_number(self, pattern: str) -> str: + """ + 核心生成逻辑:支持任意位置的 'x' + """ + # 1. 确定卡号长度标准 (Amex 15位,其他 16位) + target_len = 16 + if pattern.startswith(('34', '37')): + target_len = 15 + + working_pattern = pattern.lower() + + # 移除可能存在的非数字/x字符 + working_pattern = re.sub(r'[^0-9x]', '', working_pattern) + + # 自动填充 x + if len(working_pattern) < target_len: + working_pattern += 'x' * (target_len - len(working_pattern)) + + # 截断到 倒数第二位 (最后一位是校验位) + base_pattern = working_pattern[:target_len-1] + + generated_prefix = "" + for char in base_pattern: + if char == 'x': + generated_prefix += str(random.randint(0, 9)) + else: + generated_prefix += char + + # 计算校验位 + check_digit = self.calculate_check_digit(generated_prefix) + return generated_prefix + check_digit + + def parse_date_cvv(self, card_num: str, m_req: Optional[str], y_req: Optional[str], c_req: Optional[str]) -> Tuple[str, str, str]: + """ + 处理日期和CVV的占位符逻辑 + """ + # --- Month --- + if m_req in ['(m)', 'rnd', '', None]: + month = str(random.randint(1, 12)).zfill(2) + else: + month = m_req.strip() + + # --- Year --- + if y_req in ['(y)', 'rnd', '', None]: + year = str(self.current_year + random.randint(2, 5)) + else: + year = y_req.strip() + + # --- CVV --- + if c_req in ['(cvv)', 'rnd', '', None]: + if card_num.startswith(('34', '37')): + cvv = str(random.randint(1102, 9999)) + else: + cvv = str(random.randint(112, 999)) + else: + cvv = c_req.strip() + + return month, year, cvv + + def process_line(self, raw_input: str, count: int = 1) -> List[str]: + """ + 生成指定数量的卡片 + """ + parts = raw_input.split('|') + + pattern_part = parts[0] if len(parts) > 0 else "" + month_part = parts[1] if len(parts) > 1 else None + year_part = parts[2] if len(parts) > 2 else None + cvv_part = parts[3] if len(parts) > 3 else None + + results = [] + for _ in range(count): + card_num = self.generate_card_number(pattern_part) + m, y, c = self.parse_date_cvv(card_num, month_part, year_part, cvv_part) + results.append(f"{card_num}|{m}|{y}|{c}") + + return results + +# 全局实例 +generator = CardGenerator() diff --git a/checker/uv.lock b/checker/uv.lock index 95c2bbd..fc86dd3 100644 --- a/checker/uv.lock +++ b/checker/uv.lock @@ -2,6 +2,20 @@ version = 1 revision = 3 requires-python = ">=3.10" +[[package]] +name = "anyio" +version = "4.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", size = 228266, upload-time = "2025-11-28T23:37:38.911Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362, upload-time = "2025-11-28T23:36:57.897Z" }, +] + [[package]] name = "black" version = "25.12.0" @@ -150,9 +164,11 @@ version = "0.2.0" source = { editable = "." } dependencies = [ { name = "colorama" }, + { name = "dotenv" }, { name = "faker" }, { name = "pycryptodome" }, { name = "pyfiglet" }, + { name = "python-telegram-bot" }, { name = "requests" }, { name = "urllib3" }, { name = "v-jstools" }, @@ -171,12 +187,14 @@ dev = [ requires-dist = [ { name = "black", marker = "extra == 'dev'", specifier = ">=23.0.0" }, { name = "colorama", specifier = ">=0.4.6" }, + { name = "dotenv", specifier = ">=0.9.9" }, { name = "faker", specifier = ">=20.0.0" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.7.0" }, { name = "pycryptodome", specifier = ">=3.23.0" }, { name = "pyfiglet", specifier = ">=1.0.2" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.4.0" }, { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.1.0" }, + { name = "python-telegram-bot", specifier = ">=20.0" }, { name = "requests", specifier = ">=2.31.0" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.1.0" }, { name = "urllib3", specifier = ">=2.0.0" }, @@ -309,6 +327,17 @@ toml = [ { name = "tomli", marker = "python_full_version <= '3.11'" }, ] +[[package]] +name = "dotenv" +version = "0.9.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dotenv" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/b7/545d2c10c1fc15e48653c91efde329a790f2eecfbbf2bd16003b5db2bab0/dotenv-0.9.9-py2.py3-none-any.whl", hash = "sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9", size = 1892, upload-time = "2025-02-19T22:15:01.647Z" }, +] + [[package]] name = "exceptiongroup" version = "1.3.1" @@ -333,6 +362,43 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/eb/5a/26cdb1b10a55ac6eb11a738cea14865fa753606c4897d7be0f5dc230df00/faker-39.0.0-py3-none-any.whl", hash = "sha256:c72f1fca8f1a24b8da10fcaa45739135a19772218ddd61b86b7ea1b8c790dce7", size = 1980775, upload-time = "2025-12-17T19:19:02.926Z" }, ] +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + [[package]] name = "idna" version = "3.11" @@ -600,6 +666,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, ] +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + +[[package]] +name = "python-telegram-bot" +version = "22.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/6b/400f88e5c29a270c1c519a3ca8ad0babc650ec63dbfbd1b73babf625ed54/python_telegram_bot-22.5.tar.gz", hash = "sha256:82d4efd891d04132f308f0369f5b5929e0b96957901f58bcef43911c5f6f92f8", size = 1488269, upload-time = "2025-09-27T13:50:27.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/c3/340c7520095a8c79455fcf699cbb207225e5b36490d2b9ee557c16a7b21b/python_telegram_bot-22.5-py3-none-any.whl", hash = "sha256:4b7cd365344a7dce54312cc4520d7fa898b44d1a0e5f8c74b5bd9b540d035d16", size = 730976, upload-time = "2025-09-27T13:50:25.93Z" }, +] + [[package]] name = "pytokens" version = "0.3.0"