Compare commits

..

8 Commits

26 changed files with 950 additions and 93 deletions
+3
View File
@@ -1,3 +1,6 @@
# Telegram Bot Configuration # Telegram Bot Configuration
# Copy this file to .env and fill in your actual bot token. # Copy this file to .env and fill in your actual bot token.
BOT_TOKEN=123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ BOT_TOKEN=123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ
GROUP_CHAT_ID=-100123456789
ADMIN_IDS=123456789,987654321
DISABLE_CAPTCHA=False
+3
View File
@@ -174,3 +174,6 @@ cython_debug/
# PyPI configuration file # PyPI configuration file
.pypirc .pypirc
# Database
bot.db
+10
View File
@@ -0,0 +1,10 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Ignored default folder with query files
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<module external.system.id="pyproject.toml" type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="jdk" jdkName="uv (Frankenstein-bot)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
+6
View File
@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/frankenstein-bot.iml" filepath="$PROJECT_DIR$/.idea/frankenstein-bot.iml" />
</modules>
</component>
</project>
Generated
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
+70 -1
View File
@@ -1,2 +1,71 @@
# Anom-Bot # AWB (Awfully Written Bot)
A Telegram bot designed for secure message forwarding with captcha verification.
## Features
- **Captcha Verification**: Users have to solve an image-based captcha before their messages are forwarded (can be disabled).
- **Support for All Media**: Forwarding works for text, photos, videos, documents, audio, and voice messages.
- **Message Redirection**: Verified messages are automatically redirected to the admins.
- **Anonymized Forwarding**: Messages are forwarded with a custom prefix `Forwarding, ID [MSG_ID]:`, maintaining privacy while allowing admins to track messages.
- **Locale Support**: Built-in support for multiple languages (defaulting to English).
- **Admin Panel**: Authorized admins can access usage statistics and message logs.
## Setup
1. **Clone the repository**:
```bash
git clone <repository_url>
cd AWB
```
2. **Install dependencies**:
This project uses `uv` for dependency management.
```bash
uv sync
```
Or using pip:
```bash
pip install -r requirements.txt
```
3. **Configure environment variables**:
Copy `.env.dist` to `.env` and fill in your details:
```bash
cp .env.dist .env
```
Edit `.env`:
- `BOT_TOKEN`: Your Telegram Bot token from @BotFather.
- `GROUP_CHAT_ID`: The ID of the group where messages should be redirected.
- `ADMIN_IDS`: A comma-separated list of Telegram User IDs who can access admin commands.
- `DISABLE_CAPTCHA`: Set to `True` to disable the captcha verification step (optional, defaults to `False`).
## Usage
### For Users
1. Start the bot with `/start`.
2. Send any message or media.
3. Solve the image captcha by typing the characters shown.
4. Once solved, your message is redirected to the admins.
### For Admins
- `/stats`: View total user count.
- `/logs`: See a list of the 10 most recent redirected messages.
- `/view <ID>`: View detailed information about a specific message by its ID.
- `/ban <USER_ID>`: Block a user from using the bot.
- `/unban <USER_ID>`: Unblock a user.
## Commands
- `/start`: Initialize the bot and receive a welcome message.
- `/id`: Retrieve the ID of the current chat (useful for configuration).
## Adding Locales
The bot supports multiple languages. To add a new locale:
1. Create a new JSON file in the `locales/` directory named after the language code (e.g., `fr.json` for French).
2. Copy the keys from `locales/en.json` and provide the translations for your language.
3. The bot will automatically load the new locale on startup.
+7
View File
@@ -7,6 +7,13 @@ class Settings(BaseSettings):
Application settings loaded from environment variables or a .env file. Application settings loaded from environment variables or a .env file.
""" """
bot_token: SecretStr bot_token: SecretStr
group_chat_id: int
admin_ids: str
disable_captcha: bool = False
@property
def admins(self) -> list[int]:
return [int(x.strip()) for x in self.admin_ids.split(",") if x.strip().isdigit()]
# Tell Pydantic to read from a .env file if it exists, ignoring extra fields # Tell Pydantic to read from a .env file if it exists, ignoring extra fields
model_config = SettingsConfigDict( model_config = SettingsConfigDict(
+3 -3
View File
@@ -1,12 +1,12 @@
from aiogram import Router from aiogram import Router
from . import user, admin from . import user, admin, get_chat_id
def get_handlers_router() -> Router: def get_handlers_router() -> Router:
""" """
Combines all feature routers (user, admin, etc.) into one main router. Combines all feature routers (user, admin, etc.) into one main router.
""" """
router = Router() router = Router()
router.include_router(user.router)
router.include_router(admin.router) router.include_router(admin.router)
router.include_router(get_chat_id.router)
router.include_router(user.router)
return router return router
+70 -8
View File
@@ -1,14 +1,76 @@
from aiogram import Router from aiogram import Router, F
from aiogram.filters import Command from aiogram.filters import Command
from aiogram.types import Message from aiogram.types import Message
from bot.config import settings
from bot.utils.db import db
from bot.utils.i18n import _
router = Router() router = Router()
# Simple middleware-like check for admin
def is_admin(message: Message):
return message.from_user.id in settings.admins
@router.message(Command("admin")) @router.message(Command("stats"), F.from_user.id.in_(settings.admins))
async def cmd_admin(message: Message): async def cmd_stats(message: Message, locale: str):
""" stats = db.get_stats()
Handles /admin command (placeholder). await message.answer(_("stats_title", locale=locale) + "\n" + _("total_users", locale=locale, count=stats["user_count"]))
In production, you'd restrict this using a custom filter or admin ID list.
""" @router.message(Command("logs"), F.from_user.id.in_(settings.admins))
await message.answer("Welcome to the Admin panel. Currently, no admin tasks are configured.") async def cmd_logs(message: Message, locale: str):
logs = db.get_recent_messages()
if not logs:
await message.answer(_("no_logs", locale=locale))
return
response = _("recent_messages", locale=locale) + "\n"
for log in logs:
response += f"ID: {log[0]} | {log[1]}: {log[2][:20]}... ({log[3]})\n"
await message.answer(response)
@router.message(Command("view"), F.from_user.id.in_(settings.admins))
async def cmd_view(message: Message, locale: str):
args = message.text.split()
if len(args) < 2 or not args[1].isdigit():
await message.answer("Usage: /view [msg_id]")
return
msg_id = int(args[1])
log = db.get_message_details(msg_id)
if not log:
await message.answer(_("msg_not_found", locale=locale, id=msg_id))
return
# log: (id, full_name, username, user_id, content_type, timestamp, text)
await message.answer(_("msg_details",
locale=locale,
id=log[0],
full_name=log[1],
username=log[2],
user_id=log[3],
content_type=log[4],
timestamp=log[5],
text=log[6]))
@router.message(Command("ban"), F.from_user.id.in_(settings.admins))
async def cmd_ban(message: Message, locale: str):
args = message.text.split()
if len(args) < 2 or not args[1].isdigit():
await message.answer("Usage: /ban [user_id]")
return
user_id = int(args[1])
db.ban_user(user_id)
await message.answer(_("user_banned", locale=locale, user_id=user_id))
@router.message(Command("unban"), F.from_user.id.in_(settings.admins))
async def cmd_unban(message: Message, locale: str):
args = message.text.split()
if len(args) < 2 or not args[1].isdigit():
await message.answer("Usage: /unban [user_id]")
return
user_id = int(args[1])
db.unban_user(user_id)
await message.answer(_("user_unbanned", locale=locale, user_id=user_id))
+10
View File
@@ -0,0 +1,10 @@
from aiogram import Router
from aiogram.filters import Command
from aiogram.types import Message
from bot.utils.i18n import _
router = Router()
@router.message(Command("id"))
async def cmd_id(message: Message, locale: str):
await message.answer(_("id_command", locale=locale, id=message.chat.id))
+127 -44
View File
@@ -1,56 +1,139 @@
from aiogram import F, Router from aiogram import Router, F, Bot, html
from aiogram.filters import Command, CommandStart from aiogram.filters import CommandStart
from aiogram.types import CallbackQuery, Message from aiogram.types import Message, BufferedInputFile
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from bot.keyboards.inline import get_links_keyboard from bot.config import settings
from bot.keyboards.reply import get_main_menu_keyboard from bot.utils.captcha import generate_captcha
from bot.utils.db import db
from bot.utils.i18n import _
router = Router() router = Router()
class CaptchaStates(StatesGroup):
waiting_for_captcha = State()
@router.message(CommandStart()) @router.message(CommandStart())
async def cmd_start(message: Message): async def cmd_start(message: Message, locale: str):
""" db.log_user(message.from_user)
Handles /start command. Greets user and sends the main reply menu. welcome_key = "welcome_no_captcha" if settings.disable_captcha else "welcome"
""" await message.answer(_(welcome_key, locale=locale))
await message.answer(
f"Hello, {message.from_user.full_name}! Welcome to Frankenstein Bot.\n" @router.message(CaptchaStates.waiting_for_captcha)
f"This bot is bootstrapped with a modular and clean structure.", async def process_captcha(message: Message, state: FSMContext, bot: Bot, locale: str):
reply_markup=get_main_menu_keyboard(), data = await state.get_data()
correct_captcha = data.get("captcha_text")
original_msg_id = data.get("original_msg_id")
if message.text and message.text.upper() == correct_captcha:
await state.clear()
try:
db_msg_id = data.get("db_msg_id", "N/A")
prefix = _("forwarding_prefix", locale=locale, id=db_msg_id)
target_chat_id = settings.group_chat_id
if data.get('has_media'):
new_caption = html.quote(prefix)
if data.get('original_caption'):
new_caption += f"\n{html.quote(data.get('original_caption'))}"
await bot.copy_message(
chat_id=target_chat_id,
from_chat_id=message.chat.id,
message_id=original_msg_id,
caption=new_caption,
parse_mode=None # Use default or specific? Default is HTML.
)
elif data.get('is_text'):
await bot.send_message(
chat_id=target_chat_id,
text=f"{html.quote(prefix)}\n{html.quote(data.get('original_text'))}"
)
else:
# Other types (stickers, etc.)
await bot.copy_message(
chat_id=target_chat_id,
from_chat_id=message.chat.id,
message_id=original_msg_id
)
await bot.send_message(chat_id=target_chat_id, text=html.quote(prefix))
await message.answer(_("captcha_solved", locale=locale))
except Exception as e:
await message.answer(_("captcha_error", locale=locale))
print(f"Error forwarding: {e}")
else:
# Generate new captcha
captcha_text, captcha_img = generate_captcha()
await state.update_data(captcha_text=captcha_text)
await message.answer_photo(
BufferedInputFile(captcha_img, filename="captcha.png"),
caption=_("incorrect_captcha", locale=locale)
) )
@router.message(F.chat.type == "private")
async def handle_any_message(message: Message, state: FSMContext, bot: Bot, locale: str):
# Log everything
db_msg_id = db.log_message(message)
@router.message(Command("help")) has_media = bool(message.photo or message.video or message.document or message.audio or message.voice)
@router.message(F.text == "Help ❓") is_text = bool(message.text)
async def cmd_help(message: Message):
""" if settings.disable_captcha:
Handles /help command or 'Help ❓' reply keyboard button click. try:
""" wait_message = await message.answer(_("processing_message", locale=locale))
await message.answer( prefix = _("forwarding_prefix", locale=locale, id=db_msg_id)
"Here is what I can do:\n" target_chat_id = settings.group_chat_id
"/start - Start the bot\n"
"/help - Get help information\n" if has_media:
"/about - Learn more about the bot", new_caption = html.quote(prefix)
reply_markup=get_links_keyboard(), if message.caption:
new_caption += f"\n{html.quote(message.caption)}"
await bot.copy_message(
chat_id=target_chat_id,
from_chat_id=message.chat.id,
message_id=message.message_id,
caption=new_caption
)
elif is_text:
await bot.send_message(
chat_id=target_chat_id,
text=f"{html.quote(prefix)}\n{html.quote(message.text)}"
)
else:
await bot.copy_message(
chat_id=target_chat_id,
from_chat_id=message.chat.id,
message_id=message.message_id
)
await bot.send_message(chat_id=target_chat_id, text=html.quote(prefix))
await wait_message.edit_text(_("message_processed", locale=locale))
return
except Exception as e:
print(f"Error forwarding without captcha: {e}")
# Fallback to captcha if something goes wrong?
# Or just ignore. Usually, we want to proceed.
pass
# Generate captcha
captcha_text, captcha_img = generate_captcha()
await state.set_state(CaptchaStates.waiting_for_captcha)
await state.update_data(
captcha_text=captcha_text,
db_msg_id=db_msg_id,
original_msg_id=message.message_id,
original_text=message.text,
original_caption=message.caption,
has_media=has_media,
is_text=is_text
) )
await message.answer_photo(
@router.message(Command("about")) BufferedInputFile(captcha_img, filename="captcha.png"),
@router.message(F.text == "About ") caption=_("solve_captcha", locale=locale)
async def cmd_about(message: Message):
"""
Handles /about command or 'About ' reply keyboard button click.
"""
await message.answer(
"Frankenstein Bot is a high-performance Python Telegram bot built on aiogram v3, "
"designed to work seamlessly with systemd process management."
) )
@router.callback_query(F.data == "support_contact")
async def process_support_callback(callback: CallbackQuery):
"""
Processes the inline keyboard callback query 'support_contact'.
"""
await callback.message.answer("Support: Open an issue or contact our administrator.")
await callback.answer()
-13
View File
@@ -1,13 +0,0 @@
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
def get_links_keyboard() -> InlineKeyboardMarkup:
"""
Creates an inline keyboard markup with external links and callback query button.
"""
builder = InlineKeyboardBuilder()
builder.add(InlineKeyboardButton(text="Google", url="https://google.com"))
builder.add(InlineKeyboardButton(text="Contact Support 💬", callback_data="support_contact"))
builder.adjust(1)
return builder.as_markup()
-13
View File
@@ -1,13 +0,0 @@
from aiogram.types import KeyboardButton, ReplyKeyboardMarkup
from aiogram.utils.keyboard import ReplyKeyboardBuilder
def get_main_menu_keyboard() -> ReplyKeyboardMarkup:
"""
Creates the main menu reply keyboard markup.
"""
builder = ReplyKeyboardBuilder()
builder.add(KeyboardButton(text="Help ❓"))
builder.add(KeyboardButton(text="About "))
builder.adjust(2)
return builder.as_markup(resize_keyboard=True)
+32
View File
@@ -24,3 +24,35 @@ class LoggingMiddleware(BaseMiddleware):
except Exception as e: except Exception as e:
logger.error("Error handling event %s: %s", event.__class__.__name__, e, exc_info=True) logger.error("Error handling event %s: %s", event.__class__.__name__, e, exc_info=True)
raise e raise e
class I18nMiddleware(BaseMiddleware):
async def __call__(
self,
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: Dict[str, Any],
) -> Any:
from_user = data.get("event_from_user")
if from_user and from_user.language_code:
data["locale"] = from_user.language_code
else:
data["locale"] = "en"
return await handler(event, data)
class BanMiddleware(BaseMiddleware):
async def __call__(
self,
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: Dict[str, Any],
) -> Any:
from_user = data.get("event_from_user")
if from_user:
from bot.utils.db import db
if db.is_user_banned(from_user.id):
return
return await handler(event, data)
+1 -4
View File
@@ -7,9 +7,6 @@ async def set_bot_commands(bot: Bot):
Sets default commands in the Telegram client's command menu. Sets default commands in the Telegram client's command menu.
""" """
commands = [ commands = [
BotCommand(command="start", description="Start Frankenstein Bot"), BotCommand(command="start", description="Start the bot"),
BotCommand(command="help", description="Show help information"),
BotCommand(command="about", description="About the bot"),
BotCommand(command="admin", description="Admin panel placeholder"),
] ]
await bot.set_my_commands(commands, scope=BotCommandScopeDefault()) await bot.set_my_commands(commands, scope=BotCommandScopeDefault())
+11
View File
@@ -0,0 +1,11 @@
import random
import string
from io import BytesIO
from captcha.image import ImageCaptcha
def generate_captcha():
"""Generates a random captcha and returns (text, image_bytes)"""
captcha_text = ''.join(random.choices(string.ascii_uppercase + string.digits, k=5))
image = ImageCaptcha(width=280, height=90)
data = image.generate(captcha_text)
return captcha_text, data.getvalue()
+123
View File
@@ -0,0 +1,123 @@
import sqlite3
from datetime import datetime
import json
class Database:
def __init__(self, db_path="bot.db"):
self.db_path = db_path
self.init_db()
def get_connection(self):
return sqlite3.connect(self.db_path)
def init_db(self):
with self.get_connection() as conn:
conn.execute("""
CREATE TABLE IF NOT EXISTS users (
user_id INTEGER PRIMARY KEY,
username TEXT,
full_name TEXT,
language_code TEXT,
first_seen TIMESTAMP,
is_banned INTEGER DEFAULT 0
)
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
content_type TEXT,
text TEXT,
json_data TEXT,
timestamp TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(user_id)
)
""")
conn.commit()
def log_user(self, user):
with self.get_connection() as conn:
# Check if user already exists to preserve is_banned status if we were using INSERT OR REPLACE
# But INSERT OR REPLACE would overwrite is_banned if we don't include it.
# Better to use INSERT OR IGNORE and then UPDATE or a more complex query.
conn.execute("""
INSERT INTO users (user_id, username, full_name, language_code, first_seen)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(user_id) DO UPDATE SET
username=excluded.username,
full_name=excluded.full_name,
language_code=excluded.language_code
""", (user.id, user.username, user.full_name, user.language_code, datetime.now()))
conn.commit()
def ban_user(self, user_id):
with self.get_connection() as conn:
conn.execute("UPDATE users SET is_banned = 1 WHERE user_id = ?", (user_id,))
conn.commit()
def unban_user(self, user_id):
with self.get_connection() as conn:
conn.execute("UPDATE users SET is_banned = 0 WHERE user_id = ?", (user_id,))
conn.commit()
def is_user_banned(self, user_id):
with self.get_connection() as conn:
cursor = conn.execute("SELECT is_banned FROM users WHERE user_id = ?", (user_id,))
row = cursor.fetchone()
return bool(row[0]) if row else False
def log_message(self, message):
self.log_user(message.from_user)
with self.get_connection() as conn:
content_type = message.content_type
text = message.text or message.caption or ""
try:
json_data = message.model_dump_json()
except Exception:
# Fallback if model_dump_json fails due to non-serializable types
try:
import json
# Use model_dump to get a dict, then serialize manually
# exclude_none=True can help, and we can handle unknown types
data_dict = message.model_dump(exclude_none=True)
def custom_serializer(obj):
return str(obj)
json_data = json.dumps(data_dict, default=custom_serializer)
except Exception as e:
json_data = json.dumps({"error": f"Failed to serialize message: {str(e)}"})
cursor = conn.execute("""
INSERT INTO messages (user_id, content_type, text, json_data, timestamp)
VALUES (?, ?, ?, ?, ?)
""", (message.from_user.id, content_type, text, json_data, datetime.now()))
conn.commit()
return cursor.lastrowid
def get_stats(self):
with self.get_connection() as conn:
cursor = conn.execute("SELECT COUNT(*) FROM users")
user_count = cursor.fetchone()[0]
return {"user_count": user_count}
def get_recent_messages(self, limit=10):
with self.get_connection() as conn:
cursor = conn.execute("""
SELECT m.id, u.full_name, m.text, m.timestamp
FROM messages m
JOIN users u ON m.user_id = u.user_id
ORDER BY m.timestamp DESC
LIMIT ?
""", (limit,))
return cursor.fetchall()
def get_message_details(self, msg_id):
with self.get_connection() as conn:
cursor = conn.execute("""
SELECT m.id, u.full_name, u.username, u.user_id, m.content_type, m.timestamp, m.text
FROM messages m
JOIN users u ON m.user_id = u.user_id
WHERE m.id = ?
""", (msg_id,))
return cursor.fetchone()
db = Database()
+31
View File
@@ -0,0 +1,31 @@
import json
import os
class I18n:
def __init__(self, default_locale="en"):
self.default_locale = default_locale
self.translations = {}
self.load_translations()
def load_translations(self):
locales_dir = "locales"
if not os.path.exists(locales_dir):
return
for filename in os.listdir(locales_dir):
if filename.endswith(".json"):
locale = filename[:-5]
with open(os.path.join(locales_dir, filename), "r", encoding="utf-8") as f:
self.translations[locale] = json.load(f)
def get(self, key, locale=None, **kwargs):
if locale is None:
locale = self.default_locale
text = self.translations.get(locale, {}).get(key, self.translations.get(self.default_locale, {}).get(key, key))
try:
return text.format(**kwargs)
except (KeyError, IndexError):
return text
i18n = I18n()
_ = i18n.get
+21
View File
@@ -0,0 +1,21 @@
{
"welcome": "Welcome! Send me any message (text, image, video, etc.) and I will redirect it to the admins after you solve a simple captcha.",
"welcome_no_captcha": "Welcome! Send me any message (text, image, video, etc.) and I will redirect it to the admins",
"solve_captcha": "Please type the letters/numbers you see on the image to verify you're human.",
"redirecting": "Redirecting your message...",
"captcha_solved": "Captcha solved! Your message has been redirected to the admins.",
"captcha_error": "Captcha solved, but there was an error redirecting your message.",
"incorrect_captcha": "Incorrect captcha. Please try again with this new image:",
"stats_title": "Bot Stats:",
"total_users": "Total Users: {count}",
"no_logs": "No logs found.",
"recent_messages": "Recent messages:",
"msg_not_found": "Message with ID {id} not found.",
"msg_details": "Message ID: {id}\nUser: {full_name} (@{username}) [ID: {user_id}]\nType: {content_type}\nTimestamp: {timestamp}\n\nContent:\n{text}",
"id_command": "Current Chat ID: <code>{id}</code>",
"processing_message": "Forwarding your message...",
"message_processed": "Forwarded!",
"forwarding_prefix": "Forwarding, ID [{id}]:",
"user_banned": "User {user_id} has been banned.",
"user_unbanned": "User {user_id} has been unbanned."
}
+21
View File
@@ -0,0 +1,21 @@
{
"welcome": "Вітаю! Надішліть мені будь-яке повідомлення (текст, зображення, відео тощо), і я перешлю його до адмінів після того, як ви пройдете просту капчу.",
"welcome_no_captcha": "Вітаю! Надішліть мені будь-яке повідомлення (текст, зображення, відео тощо), і я перешлю його до адмінів.",
"solve_captcha": "Будь ласка, введіть літери/цифри, які ви бачите на зображенні, щоб підтвердити, що ви людина.",
"redirecting": "Перенаправляю ваше повідомлення...",
"captcha_solved": "Капчу пройдено! Ваше повідомлення було переслано до адмінів.",
"captcha_error": "Капчу пройдено, але сталася помилка під час перенаправлення вашого повідомлення.",
"incorrect_captcha": "Неправильна капча. Будь ласка, спробуйте ще раз із цим новим зображенням:",
"stats_title": "Статистика бота:",
"total_users": "Усього користувачів: {count}",
"no_logs": "Логів не знайдено.",
"recent_messages": "Останні повідомлення:",
"msg_not_found": "Повідомлення з ID {id} не знайдено.",
"msg_details": "ID повідомлення: {id}\nКористувач: {full_name} (@{username}) [ID: {user_id}]\nТип: {content_type}\nЧас: {timestamp}\n\nВміст:\n{text}",
"id_command": "Поточний ID чату: <code>{id}</code>",
"processing_message": "Відправляємо ваше повідомлення...",
"message_processed": "Відправлено!",
"forwarding_prefix": "Пересилання, ID [{id}]:",
"user_banned": "Користувач {user_id} заблокований.",
"user_unbanned": "Користувач {user_id} розблокований."
}
+6 -3
View File
@@ -8,7 +8,8 @@ from aiogram.enums import ParseMode
from bot.config import settings from bot.config import settings
from bot.handlers import get_handlers_router from bot.handlers import get_handlers_router
from bot.middlewares import LoggingMiddleware from bot.middlewares import LoggingMiddleware, I18nMiddleware, BanMiddleware
from bot.utils.db import db
from bot.ui_commands import set_bot_commands from bot.ui_commands import set_bot_commands
# Setup logging # Setup logging
@@ -23,7 +24,7 @@ logger = logging.getLogger(__name__)
async def main(): async def main():
logger.info("Initializing Frankenstein Bot...") logger.info("Initializing Bot...")
# Initialize Bot instance with default HTML parsing mode # Initialize Bot instance with default HTML parsing mode
bot = Bot( bot = Bot(
@@ -36,6 +37,8 @@ async def main():
# Register middlewares (e.g. LoggingMiddleware) # Register middlewares (e.g. LoggingMiddleware)
dp.update.outer_middleware(LoggingMiddleware()) dp.update.outer_middleware(LoggingMiddleware())
dp.update.outer_middleware(BanMiddleware())
dp.update.outer_middleware(I18nMiddleware())
# Register routers (handlers) # Register routers (handlers)
dp.include_router(get_handlers_router()) dp.include_router(get_handlers_router())
@@ -73,4 +76,4 @@ if __name__ == "__main__":
try: try:
asyncio.run(main()) asyncio.run(main())
except (KeyboardInterrupt, SystemExit): except (KeyboardInterrupt, SystemExit):
logger.info("Frankenstein Bot execution stopped.") logger.info("Bot execution stopped.")
+1
View File
@@ -6,4 +6,5 @@ dependencies = [
"aiogram>=3.29.1", "aiogram>=3.29.1",
"pydantic>=2.13.4", "pydantic>=2.13.4",
"pydantic-settings>=2.14.2", "pydantic-settings>=2.14.2",
"captcha",
] ]
+302
View File
@@ -0,0 +1,302 @@
# This file was autogenerated by uv via the following command:
# uv export -o requirements.txt
aiofiles==25.1.0 \
--hash=sha256:a8d728f0a29de45dc521f18f07297428d56992a742f0cd2701ba86e44d23d5b2 \
--hash=sha256:abe311e527c862958650f9438e859c1fa7568a141b22abcd015e120e86a85695
# via aiogram
aiogram==3.29.1 \
--hash=sha256:63cd0123e8ab065f78974ad58ff57c1a5c8c35aac1055761859c4a5ad27eaf04 \
--hash=sha256:7ac92c64b71d33eb8e3656de4517a1533a88b61728e5dd00b1e11393b669b521
# via frankenstein-bot
aiohappyeyeballs==2.7.1 \
--hash=sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d \
--hash=sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472
# via aiohttp
aiohttp==3.14.1 \
--hash=sha256:07eabb979d236335fed927e137a928c9adfb7df3b9ec7aa31726f133a62be983 \
--hash=sha256:10ee9c1753a8f706345b22496c79fbddb5be0599e0823f3738b1534058e25340 \
--hash=sha256:1601cc37baf5750ccacae618ec2daf020769581695550e3b654a911f859c563d \
--hash=sha256:20205f7f5ade7aaec9f4b500549bbc071b046453aed72f9c06dcab87896a83e8 \
--hash=sha256:237651caadc3a59badd39319c54642b5299e9cc98a3a194310e55d5bb9f5e397 \
--hash=sha256:2964cbf553df4d7a57348da44d961d871895fc1ee4e8c322b2a95612c7b17fba \
--hash=sha256:2b7edd08e0a5deb1e8564a2fcd8f4561014a3f05252334671bbf55ddd47db0e5 \
--hash=sha256:307f2cff90a764d329e77040603fa032db89c5c24fdad50c4c15334cba744035 \
--hash=sha256:335c0cc3e3545ce98dcb9cfcb836f40c3411f43fa03dab757597d80c89af8a35 \
--hash=sha256:367a9314fdc79dab0fac96e216cb41dd73c85bdca85306ce8999118ba7e0f333 \
--hash=sha256:38e1e7daaea81df51c952e18483f323d878499a1e2bfe564790e0f9701d6f203 \
--hash=sha256:4132e72c608fe9fecb8f409113567605915b83e9bdd3ea56538d2f9cd35002f1 \
--hash=sha256:4691802dda97be727f79d86818acaad7eb8e9252626a1d6b519fedbb92d5e251 \
--hash=sha256:47ddf841cdecc810749921d25606dee45857d12d2ad5ddb7b5bd7eab12e4b365 \
--hash=sha256:4d6e0ac9da31c9c04c84e1c0182ad8d6df35965a85cae29cd71d089621b3ae94 \
--hash=sha256:4fe1f1087cbadb280b5e1bb054a4f00d1423c74d6626c5e48400d871d34ecefe \
--hash=sha256:5e78b522b7a6e27e0b25d19b247b75039ac4c94f99823e3c9e53ae1603a9f7e9 \
--hash=sha256:62a759436b29e677181a9e76bab8b8f689a29cb9c535f45f7c48c9c830d3f8c3 \
--hash=sha256:634e385930fb6d2d479cf3aa66515955863b77a5e3c2b5894ca259a25b308602 \
--hash=sha256:6ffbb2f4ec1ceaff7e07d43922954da26b223d188bf30658e561b98e23089444 \
--hash=sha256:7c106c26852ca1c2047c6b80384f17100b4e439af276f21ef3d4e2f450ae7e15 \
--hash=sha256:819c054312f1af92947e6a55883d1b66feefab11531a7fc45e0fb9b63880b5c2 \
--hash=sha256:8560b4d712474335d08907db7973f71912d3a9a8f1dee992ec06b5d2fe359496 \
--hash=sha256:896e12dfdbbab9d8f7e16d2b28c6769a60126fa92095d1ebf9473d02593a2448 \
--hash=sha256:90d53f1609c29ccc2193945ef732428382a28f78d0456ae4d3daf0d48b74f0f6 \
--hash=sha256:979ed4717f59b8bb12e3963378fa285d93d367e15bcd66c721311826d3c44a6c \
--hash=sha256:99abd37084b82f5830c635fddd0b4993b9742a66eb746dacf433c8590e8f9e3c \
--hash=sha256:9e8f2d660c350b3d0e259c7a7e3d9b7fc8b41210cbcc3d4a7076ff0a5e5c2fdc \
--hash=sha256:a24f677ebe83749039e7bdf862ff0bbb16818ae4193d4ef96505e269375bcce0 \
--hash=sha256:a9875b46d910cff3ea2f5962f9d266b465459fe634e22556ab9bd6fc1192eea0 \
--hash=sha256:ae6be797afdef264e8a84864a85b196ca06045586481b3df8a967322fd2fa844 \
--hash=sha256:af8b4b81a960eeaf1234971ac3cd0ba5901f3cd42eae42a46b4d089a8b492719 \
--hash=sha256:b165790117eea512d7f3fb22f1f6dad3d55a7189571993eb015591c1401276d1 \
--hash=sha256:b6ff7fcee63287ae57b5df3e4f5957ce032122802509246dec1a5bcc55904c95 \
--hash=sha256:c389c482a7e9b9dc3ee2701ac46c4125297a3818875b9c305ddb603c04828fd1 \
--hash=sha256:c83afe0ba876be7e943d2e0ba645809ad441575d2840c895c21ee5de93b9377a \
--hash=sha256:cf4491381b1b57425c315a56a439251b1bdac07b2275f19a8c44bc57744532ec \
--hash=sha256:d03f281ed22579314ba00821ce20115a7c0ac430660b4cc05704a3f818b3e004 \
--hash=sha256:ed09c7eb1c391271c2ed0314a51903e72a3acb653d5ccfc264cdf3ef11f8269d \
--hash=sha256:eeea07c4397bbc57719c4eed8f9c284874d4f175f9b6d57f7a1546b976d455ca \
--hash=sha256:eefd9cc9b6d4a2db5f00a26bc3e4f9acf71926a6ec557cd56c9c6f27c290b665 \
--hash=sha256:fc0cacab7ba4e56f0f81c82a98c09bed2f39c940107b03a34b168bdf7597edd3
# via aiogram
aiosignal==1.4.0 \
--hash=sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e \
--hash=sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7
# via aiohttp
annotated-types==0.7.0 \
--hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \
--hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89
# via pydantic
attrs==26.1.0 \
--hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 \
--hash=sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32
# via aiohttp
certifi==2026.6.17 \
--hash=sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432 \
--hash=sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db
# via aiogram
frozenlist==1.8.0 \
--hash=sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686 \
--hash=sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd \
--hash=sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7 \
--hash=sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d \
--hash=sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79 \
--hash=sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f \
--hash=sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7 \
--hash=sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef \
--hash=sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe \
--hash=sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930 \
--hash=sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37 \
--hash=sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128 \
--hash=sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2 \
--hash=sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f \
--hash=sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df \
--hash=sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c \
--hash=sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0 \
--hash=sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad \
--hash=sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30 \
--hash=sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c \
--hash=sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a \
--hash=sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8 \
--hash=sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed \
--hash=sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24 \
--hash=sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e \
--hash=sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e \
--hash=sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8 \
--hash=sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806 \
--hash=sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a \
--hash=sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2 \
--hash=sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0 \
--hash=sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b \
--hash=sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d \
--hash=sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a
# via
# aiohttp
# aiosignal
idna==3.18 \
--hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \
--hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848
# via yarl
magic-filter==1.0.12 \
--hash=sha256:4751d0b579a5045d1dc250625c4c508c18c3def5ea6afaf3957cb4530d03f7f9 \
--hash=sha256:e5929e544f310c2b1f154318db8c5cdf544dd658efa998172acd2e4ba0f6c6a6
# via aiogram
multidict==6.7.1 \
--hash=sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581 \
--hash=sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1 \
--hash=sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c \
--hash=sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262 \
--hash=sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d \
--hash=sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601 \
--hash=sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362 \
--hash=sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2 \
--hash=sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb \
--hash=sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d \
--hash=sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5 \
--hash=sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd \
--hash=sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5 \
--hash=sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37 \
--hash=sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56 \
--hash=sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f \
--hash=sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889 \
--hash=sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048 \
--hash=sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59 \
--hash=sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709 \
--hash=sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c \
--hash=sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee \
--hash=sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609 \
--hash=sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1 \
--hash=sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31 \
--hash=sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7 \
--hash=sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9 \
--hash=sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4 \
--hash=sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2 \
--hash=sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c \
--hash=sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2 \
--hash=sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a \
--hash=sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e \
--hash=sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489 \
--hash=sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d \
--hash=sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b \
--hash=sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d \
--hash=sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0
# via
# aiohttp
# yarl
propcache==0.5.2 \
--hash=sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427 \
--hash=sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0 \
--hash=sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917 \
--hash=sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3 \
--hash=sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa \
--hash=sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821 \
--hash=sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56 \
--hash=sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999 \
--hash=sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb \
--hash=sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4 \
--hash=sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03 \
--hash=sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5 \
--hash=sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979 \
--hash=sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5 \
--hash=sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078 \
--hash=sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837 \
--hash=sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c \
--hash=sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8 \
--hash=sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe \
--hash=sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d \
--hash=sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539 \
--hash=sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285 \
--hash=sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e \
--hash=sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe \
--hash=sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39 \
--hash=sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2 \
--hash=sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0 \
--hash=sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335 \
--hash=sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80 \
--hash=sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370 \
--hash=sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4 \
--hash=sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825 \
--hash=sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366 \
--hash=sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702 \
--hash=sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e \
--hash=sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6
# via
# aiohttp
# yarl
pydantic==2.13.4 \
--hash=sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba \
--hash=sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6
# via
# aiogram
# frankenstein-bot
# pydantic-settings
pydantic-core==2.46.4 \
--hash=sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e \
--hash=sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb \
--hash=sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5 \
--hash=sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462 \
--hash=sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b \
--hash=sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519 \
--hash=sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6 \
--hash=sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565 \
--hash=sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898 \
--hash=sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb \
--hash=sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596 \
--hash=sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d \
--hash=sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd \
--hash=sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1 \
--hash=sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3 \
--hash=sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4 \
--hash=sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b \
--hash=sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac \
--hash=sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4 \
--hash=sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914 \
--hash=sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424 \
--hash=sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0 \
--hash=sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e \
--hash=sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a \
--hash=sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c \
--hash=sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000 \
--hash=sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28 \
--hash=sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9 \
--hash=sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02 \
--hash=sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7 \
--hash=sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff
# via pydantic
pydantic-settings==2.14.2 \
--hash=sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440 \
--hash=sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f
# via frankenstein-bot
python-dotenv==1.2.2 \
--hash=sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a \
--hash=sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3
# via pydantic-settings
typing-extensions==4.16.0 \
--hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \
--hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5
# via
# aiogram
# pydantic
# pydantic-core
# typing-inspection
typing-inspection==0.4.2 \
--hash=sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7 \
--hash=sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464
# via
# pydantic
# pydantic-settings
yarl==1.24.2 \
--hash=sha256:0063adad533e57171b79db3943b229d40dfafeeee579767f96541f106bac5f1b \
--hash=sha256:081c2bf54efe03774d0311172bc04fedf9ca01e644d4cd8c805688e527209bdc \
--hash=sha256:0c3063e5c0a8e8e62fae6c2596fa01da1561e4cd1da6fec5789f5cf99a8aefd8 \
--hash=sha256:221ce1dd921ac4f603957f17d7c18c5cc0797fbb52f156941f92e04605d1d67b \
--hash=sha256:2783d9226db8797636cd6896e4de81feed252d1db72265686c9558d97a4d94b9 \
--hash=sha256:297a2fe352ecf858b30a98f87948746ec16f001d279f84aebdbd3bd965e2f1bd \
--hash=sha256:2a263e76b97bc42bdcd7c5f4953dec1f7cd62a1112fa7f869e57255229390d67 \
--hash=sha256:2d07d21d0bc4b17558e8de0b02fbfdf1e347d3bb3699edd00bb92e7c57925420 \
--hash=sha256:310fc687f7b2044ec54e372c8cbe923bb88f5c37bded0d3079e5791c2fc3cf50 \
--hash=sha256:4da31a5512ed1729ca8d8aacde3f7faeb8843cde3165d6bcf7f88f74f17bb8aa \
--hash=sha256:4fb1ac3fc5fecd8ae7453ea237e4d22b49befa70266dfe1629924245c21a0c7f \
--hash=sha256:533ded4dceb5f1f3da7906244f4e82cf46cfd40d84c69a1faf5ac506aa65ecbe \
--hash=sha256:5f3224db28173a00d7afacdee07045cc4673dfab2b15492c7ae10deddbece761 \
--hash=sha256:68cf6eacd6028ef1142bc4b48376b81566385ca6f9e7dde3b0fa91be08ffcb57 \
--hash=sha256:73e68edf6dfd5f73f9ca127d84e2a6f9213c65bdffb736bda19524c0564fcd14 \
--hash=sha256:7b3a85525f6e7eeabcfdd372862b21ee1915db1b498a04e8bf0e389b607ff0bd \
--hash=sha256:822519b64cf0b474f1a0aaef1dc621438ea46bb77c94df97a5b4d213a7d8a8b1 \
--hash=sha256:84f9670b89f34db07f81e53aee83e0b938a3412329d51c8f922488be7fcc4024 \
--hash=sha256:86746bef442aa479107fe28132e1277237f9c24c2f00b0b0cf22b3ee0904f2bb \
--hash=sha256:8cec2a38d70edc10e0e856ceda886af5327a017ccbde8e1de1bd44d300357543 \
--hash=sha256:904065e6e85b1fa54d0d87438bd58c14c0bad97aad654ad1077fd9d87e8478ed \
--hash=sha256:9ac374123c6fd7abf64d1fec93962b0bd4ee2c19751755a762a72dd96c0378f8 \
--hash=sha256:a296ca617f2d25fbceafb962b88750d627e5984e75732c712154d058ae8d79a3 \
--hash=sha256:a46d1ab4ba4d32e6dc80daf8a28ce0bd83d08df52fbc32f3e288663427734535 \
--hash=sha256:a4f4d6cd615823bfc7fb7e9b5987c3f41666371d870d51058f77e2680fbe9630 \
--hash=sha256:a7624b1ca46ca5d7b864ef0d2f8efe3091454085ee1855b4e992314529972215 \
--hash=sha256:abb2759733d63a28b4956500a5dd57140f26486c92b2caedfb964ab7d9b79dbf \
--hash=sha256:afb00d7fd8e0f285ca29a44cc50df2d622ff2f7a6d933fa641577b5f9d5f3db0 \
--hash=sha256:b6067060d9dc594899ba83e6db6c48c68d1e494a6dab158156ed86977ca7bcb1 \
--hash=sha256:c557165320d6244ebe3a02431b2a201a20080e02f41f0cfa0ccc47a183765da8 \
--hash=sha256:e434a45ce2e7a947f951fc5a8944c8cc080b7e59f9c50ae80fd39107cf88126d \
--hash=sha256:e51b2cf5ec89a8b8470177641ed62a3ba22d74e1e898e06ad53aa77972487208 \
--hash=sha256:e7484b9361ed222ee1ca5b4337aa4cbdcc4618ce5aff57d9ef1582fd95893fc0 \
--hash=sha256:ee8e3fb34513e8dc082b586ef4910c98335d43a6fab688cd44d4851bacfce3e8 \
--hash=sha256:f9312b3c02d9b3d23840f67952913c9c8721d7f1b7db305289faefa878f364c2 \
--hash=sha256:fecd17873a096036c1c87ab3486f1aef7f269ada7f23f7f856f93b1cc7744f14
# via aiohttp
Generated
+64
View File
@@ -125,6 +125,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" },
] ]
[[package]]
name = "captcha"
version = "0.7.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pillow" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b4/65/8e186bb798f33ba390eab897c995b0fcee92bc030e0f40cb8ea01f34dd07/captcha-0.7.1.tar.gz", hash = "sha256:a1b462bcc633a64d8db5efa7754548a877c698d98f87716c620a707364cabd6b", size = 226561, upload-time = "2025-03-01T05:00:13.395Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/da/ff/3f0982ecd37c2d6a7266c22e7ea2e47d0773fe449984184c5316459d2776/captcha-0.7.1-py3-none-any.whl", hash = "sha256:8b73b5aba841ad1e5bdb856205bf5f09560b728ee890eb9dae42901219c8c599", size = 147606, upload-time = "2025-03-01T05:00:10.433Z" },
]
[[package]] [[package]]
name = "certifi" name = "certifi"
version = "2026.6.17" version = "2026.6.17"
@@ -140,6 +152,7 @@ version = "0.1.0"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "aiogram" }, { name = "aiogram" },
{ name = "captcha" },
{ name = "pydantic" }, { name = "pydantic" },
{ name = "pydantic-settings" }, { name = "pydantic-settings" },
] ]
@@ -147,6 +160,7 @@ dependencies = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "aiogram", specifier = ">=3.29.1" }, { name = "aiogram", specifier = ">=3.29.1" },
{ name = "captcha" },
{ name = "pydantic", specifier = ">=2.13.4" }, { name = "pydantic", specifier = ">=2.13.4" },
{ name = "pydantic-settings", specifier = ">=2.14.2" }, { name = "pydantic-settings", specifier = ">=2.14.2" },
] ]
@@ -255,6 +269,56 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" },
] ]
[[package]]
name = "pillow"
version = "12.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736, upload-time = "2026-07-01T11:54:51.156Z" },
{ url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435, upload-time = "2026-07-01T11:54:53.414Z" },
{ url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262, upload-time = "2026-07-01T11:54:55.739Z" },
{ url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344, upload-time = "2026-07-01T11:54:57.657Z" },
{ url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131, upload-time = "2026-07-01T11:54:59.713Z" },
{ url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757, upload-time = "2026-07-01T11:55:01.778Z" },
{ url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962, upload-time = "2026-07-01T11:55:03.93Z" },
{ url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171, upload-time = "2026-07-01T11:55:05.989Z" },
{ url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116, upload-time = "2026-07-01T11:55:08.131Z" },
{ url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209, upload-time = "2026-07-01T11:55:10.408Z" },
{ url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707, upload-time = "2026-07-01T11:55:12.745Z" },
{ url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995, upload-time = "2026-07-01T11:55:14.736Z" },
{ url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503, upload-time = "2026-07-01T11:55:17.076Z" },
{ url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956, upload-time = "2026-07-01T11:55:19.448Z" },
{ url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855, upload-time = "2026-07-01T11:55:21.613Z" },
{ url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642, upload-time = "2026-07-01T11:55:24.006Z" },
{ url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281, upload-time = "2026-07-01T11:55:26.252Z" },
{ url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716, upload-time = "2026-07-01T11:55:28.318Z" },
{ url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125, upload-time = "2026-07-01T11:55:30.956Z" },
{ url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939, upload-time = "2026-07-01T11:55:34.044Z" },
{ url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506, upload-time = "2026-07-01T11:55:35.988Z" },
{ url = "https://files.pythonhosted.org/packages/5d/dc/8fdce34ec725a33c81c6ba122b904d6b9024e50ea9ac7bede62fab54506c/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139", size = 4162063, upload-time = "2026-07-01T11:55:37.941Z" },
{ url = "https://files.pythonhosted.org/packages/76/66/2044b9a63d3b84ff048228dfcb7cd9bf0df983e8470971bf7d4c57b693de/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402", size = 4255549, upload-time = "2026-07-01T11:55:40.022Z" },
{ url = "https://files.pythonhosted.org/packages/52/7e/1f67e6f4ece6b582ee4b539decbcc9f848dc245a93ed8cd7338bafef72f1/pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c", size = 3696331, upload-time = "2026-07-01T11:55:41.98Z" },
{ url = "https://files.pythonhosted.org/packages/12/40/d306fc2c8e4d45d7f175c77edca7063be7b86fe7fe6e68f4353bf71d808c/pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f", size = 5350370, upload-time = "2026-07-01T11:55:44.028Z" },
{ url = "https://files.pythonhosted.org/packages/dd/44/668fb1437e8ce420f62d6106eb66e44a5971602a4d794615bdf79315d82d/pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701", size = 4780147, upload-time = "2026-07-01T11:55:46.073Z" },
{ url = "https://files.pythonhosted.org/packages/0c/08/93fa2e70e30a2d81547e481b6ee2bb9522117221fb1e0ce4b5df70967677/pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace", size = 6273659, upload-time = "2026-07-01T11:55:48.264Z" },
{ url = "https://files.pythonhosted.org/packages/f8/6d/043e96ff814fc31a33077e4cba86082167db520c93632afdf2042febbb0c/pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4", size = 6947439, upload-time = "2026-07-01T11:55:50.503Z" },
{ url = "https://files.pythonhosted.org/packages/af/92/ba71d2ee2ac0edf3fa33bd9d5ee9ee080da70b1766f3ca3934f9938ddac9/pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39", size = 6353577, upload-time = "2026-07-01T11:55:52.697Z" },
{ url = "https://files.pythonhosted.org/packages/0f/ce/e63064e2122923ff687c8ad792d0d736a7b3920a56a46982e81a7fdd25d6/pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71", size = 7060394, upload-time = "2026-07-01T11:55:55.149Z" },
{ url = "https://files.pythonhosted.org/packages/54/76/a09cc3ccc8d773a7283d34c38bec1708f9e3cc932093cbc4c5e71ac4060b/pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827", size = 6467375, upload-time = "2026-07-01T11:55:57.769Z" },
{ url = "https://files.pythonhosted.org/packages/3e/03/1846c49ba3b1d5550392a4bbd06d6fb4578e1cd91a803198b5c90f5f7d53/pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5", size = 7237048, upload-time = "2026-07-01T11:55:59.975Z" },
{ url = "https://files.pythonhosted.org/packages/fb/bb/89f35dcc79610423f9f195504d7def7f0d1416a711541b42867e25fe3412/pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658", size = 2566006, upload-time = "2026-07-01T11:56:02.143Z" },
{ url = "https://files.pythonhosted.org/packages/30/88/707027ba09942dfa2c28759b5c222d769290a41c6d20ea60ec250801941f/pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf", size = 5352509, upload-time = "2026-07-01T11:56:04.2Z" },
{ url = "https://files.pythonhosted.org/packages/b0/6d/00352fa25332c2569cd387851f568cc5a4b75a9adbfb37ac4fbce4c02eec/pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64", size = 4783167, upload-time = "2026-07-01T11:56:06.631Z" },
{ url = "https://files.pythonhosted.org/packages/13/4f/9e049dfa21af7c22427275720e2490267ba8138120add5c4c574deb69782/pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e", size = 6329237, upload-time = "2026-07-01T11:56:08.868Z" },
{ url = "https://files.pythonhosted.org/packages/36/16/cf6eeaae8d0fce8dd390a33437cf68c5d5bd73834a2bc6e2f14efda0ab45/pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777", size = 6997047, upload-time = "2026-07-01T11:56:11.379Z" },
{ url = "https://files.pythonhosted.org/packages/1e/69/dbf769bdd55f48bf5733cac28edc6364ffaa072ec9ba336266e4fe66be55/pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1", size = 6400440, upload-time = "2026-07-01T11:56:13.908Z" },
{ url = "https://files.pythonhosted.org/packages/a0/e1/ffc9cfc2eea0d178da8018e18e959301ad9d6bc9f3edb7181e748a474b97/pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9", size = 7105895, upload-time = "2026-07-01T11:56:16.575Z" },
{ url = "https://files.pythonhosted.org/packages/18/f0/a5595c1e8c3ae44b9828cb2f0fa8155e5095ef04d6327b8f61cf44a3df85/pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8", size = 6474384, upload-time = "2026-07-01T11:56:18.855Z" },
{ url = "https://files.pythonhosted.org/packages/e4/04/62bcd9f844984c5938d3b05264a61d797a29d3e0812341a8204af70bbdee/pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418", size = 7243537, upload-time = "2026-07-01T11:56:21.214Z" },
{ url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491, upload-time = "2026-07-01T11:56:23.506Z" },
]
[[package]] [[package]]
name = "propcache" name = "propcache"
version = "0.5.2" version = "0.5.2"