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"