diff --git a/.env.dist b/.env.dist index 6893ee2..e0d8a07 100644 --- a/.env.dist +++ b/.env.dist @@ -1,3 +1,5 @@ # Telegram Bot Configuration # Copy this file to .env and fill in your actual bot token. BOT_TOKEN=123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ +GROUP_CHAT_ID=-100123456789 +ADMIN_IDS=123456789,987654321 diff --git a/.gitignore b/.gitignore index 36b13f1..1c5c138 100644 --- a/.gitignore +++ b/.gitignore @@ -174,3 +174,6 @@ cython_debug/ # PyPI configuration file .pypirc + +# Database +bot.db diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..30cf57e --- /dev/null +++ b/.idea/.gitignore @@ -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 diff --git a/.idea/frankenstein-bot.iml b/.idea/frankenstein-bot.iml new file mode 100644 index 0000000..b9bd68f --- /dev/null +++ b/.idea/frankenstein-bot.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..3c208d1 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 05a29b7..60001e4 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,68 @@ -# Anom-Bot +# Frankenstein Bot + +A Telegram bot designed for secure message forwarding with captcha verification. + +## Features + +- **Captcha Verification**: Users must solve an image-based captcha before their messages are forwarded. +- **Support for All Media**: Forwarding works for text, photos, videos, documents, audio, and voice messages. +- **Message Redirection**: Verified messages are automatically redirected to a configured group chat. +- **Anonymized Forwarding**: Messages are forwarded with a custom prefix `Forwarding, ID [MSG_ID]:`, maintaining privacy while allowing for message tracking. +- **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 + cd Frankenstein-bot + ``` + +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. + +## 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 destination group. + +### For Admins +- `/stats`: View total user count. +- `/logs`: See a list of the 10 most recent redirected messages. +- `/view `: View detailed information about a specific message by its ID. + +## 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. + diff --git a/bot/config.py b/bot/config.py index 9e8a8a2..a3462d3 100644 --- a/bot/config.py +++ b/bot/config.py @@ -7,6 +7,12 @@ class Settings(BaseSettings): Application settings loaded from environment variables or a .env file. """ bot_token: SecretStr + group_chat_id: int + admin_ids: str + + @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 model_config = SettingsConfigDict( diff --git a/bot/handlers/__init__.py b/bot/handlers/__init__.py index 762f87f..9bc6fcb 100644 --- a/bot/handlers/__init__.py +++ b/bot/handlers/__init__.py @@ -1,12 +1,12 @@ from aiogram import Router -from . import user, admin - +from . import user, admin, get_chat_id def get_handlers_router() -> Router: """ Combines all feature routers (user, admin, etc.) into one main router. """ router = Router() - router.include_router(user.router) router.include_router(admin.router) + router.include_router(get_chat_id.router) + router.include_router(user.router) return router diff --git a/bot/handlers/admin.py b/bot/handlers/admin.py deleted file mode 100644 index 3a442cc..0000000 --- a/bot/handlers/admin.py +++ /dev/null @@ -1,14 +0,0 @@ -from aiogram import Router -from aiogram.filters import Command -from aiogram.types import Message - -router = Router() - - -@router.message(Command("admin")) -async def cmd_admin(message: Message): - """ - Handles /admin command (placeholder). - In production, you'd restrict this using a custom filter or admin ID list. - """ - await message.answer("Welcome to the Admin panel. Currently, no admin tasks are configured.") diff --git a/bot/handlers/get_chat_id.py b/bot/handlers/get_chat_id.py new file mode 100644 index 0000000..da9a1a9 --- /dev/null +++ b/bot/handlers/get_chat_id.py @@ -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)) diff --git a/bot/handlers/user.py b/bot/handlers/user.py index 88b693a..56cc052 100644 --- a/bot/handlers/user.py +++ b/bot/handlers/user.py @@ -1,56 +1,100 @@ -from aiogram import F, Router -from aiogram.filters import Command, CommandStart -from aiogram.types import CallbackQuery, Message +from aiogram import Router, F, Bot +from aiogram.filters import CommandStart +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.keyboards.reply import get_main_menu_keyboard +from bot.config import settings +from bot.utils.captcha import generate_captcha +from bot.utils.db import db +from bot.utils.i18n import _ router = Router() +class CaptchaStates(StatesGroup): + waiting_for_captcha = State() @router.message(CommandStart()) -async def cmd_start(message: Message): - """ - Handles /start command. Greets user and sends the main reply menu. - """ - await message.answer( - f"Hello, {message.from_user.full_name}! Welcome to Frankenstein Bot.\n" - f"This bot is bootstrapped with a modular and clean structure.", - reply_markup=get_main_menu_keyboard(), +async def cmd_start(message: Message, locale: str): + db.log_user(message.from_user) + await message.answer(_("welcome", locale=locale)) + +@router.message(CaptchaStates.waiting_for_captcha) +async def process_captcha(message: Message, state: FSMContext, bot: Bot, locale: str): + 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 = f"Forwarding, ID [{db_msg_id}]:" + target_chat_id = settings.group_chat_id + + if data.get('has_media'): + new_caption = prefix + if data.get('original_caption'): + new_caption += f"\n{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 + ) + elif data.get('is_text'): + await bot.send_message( + chat_id=target_chat_id, + text=f"{prefix}\n{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=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, locale: str): + # Log everything + db_msg_id = db.log_message(message) + + # Generate captcha + captcha_text, captcha_img = generate_captcha() + + has_media = bool(message.photo or message.video or message.document or message.audio or message.voice) + is_text = bool(message.text) + + 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 ) - - -@router.message(Command("help")) -@router.message(F.text == "Help ❓") -async def cmd_help(message: Message): - """ - Handles /help command or 'Help ❓' reply keyboard button click. - """ - await message.answer( - "Here is what I can do:\n" - "/start - Start the bot\n" - "/help - Get help information\n" - "/about - Learn more about the bot", - reply_markup=get_links_keyboard(), + + await message.answer_photo( + BufferedInputFile(captcha_img, filename="captcha.png"), + caption=_("solve_captcha", locale=locale) ) - - -@router.message(Command("about")) -@router.message(F.text == "About ℹ️") -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() diff --git a/bot/keyboards/inline.py b/bot/keyboards/inline.py deleted file mode 100644 index 3252a00..0000000 --- a/bot/keyboards/inline.py +++ /dev/null @@ -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() diff --git a/bot/keyboards/reply.py b/bot/keyboards/reply.py deleted file mode 100644 index e83a5c0..0000000 --- a/bot/keyboards/reply.py +++ /dev/null @@ -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) diff --git a/bot/middlewares/__init__.py b/bot/middlewares/__init__.py index 1803504..5aadfda 100644 --- a/bot/middlewares/__init__.py +++ b/bot/middlewares/__init__.py @@ -24,3 +24,19 @@ class LoggingMiddleware(BaseMiddleware): except Exception as e: logger.error("Error handling event %s: %s", event.__class__.__name__, e, exc_info=True) 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) diff --git a/bot/ui_commands.py b/bot/ui_commands.py index 7973de8..ee45cf7 100644 --- a/bot/ui_commands.py +++ b/bot/ui_commands.py @@ -7,9 +7,6 @@ async def set_bot_commands(bot: Bot): Sets default commands in the Telegram client's command menu. """ commands = [ - BotCommand(command="start", description="Start Frankenstein Bot"), - BotCommand(command="help", description="Show help information"), - BotCommand(command="about", description="About the bot"), - BotCommand(command="admin", description="Admin panel placeholder"), + BotCommand(command="start", description="Start the bot"), ] await bot.set_my_commands(commands, scope=BotCommandScopeDefault()) diff --git a/bot/utils/captcha.py b/bot/utils/captcha.py new file mode 100644 index 0000000..8a0d53f --- /dev/null +++ b/bot/utils/captcha.py @@ -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() diff --git a/bot/utils/i18n.py b/bot/utils/i18n.py new file mode 100644 index 0000000..6c6ed8b --- /dev/null +++ b/bot/utils/i18n.py @@ -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 diff --git a/locales/en.json b/locales/en.json new file mode 100644 index 0000000..e198036 --- /dev/null +++ b/locales/en.json @@ -0,0 +1,15 @@ +{ + "welcome": "Welcome! Send me any message (text, image, video, etc.) and I will redirect it after you solve a simple captcha.", + "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.", + "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: {id}" +} diff --git a/locales/uk.json b/locales/uk.json new file mode 100644 index 0000000..93f99ce --- /dev/null +++ b/locales/uk.json @@ -0,0 +1,15 @@ +{ + "welcome": "Вітаю! Надішліть мені будь-яке повідомлення (текст, зображення, відео тощо), і я перенаправлю його після того, як ви пройдете просту капчу.", + "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 чату: {id}" +} \ No newline at end of file diff --git a/main.py b/main.py index 5102be1..5e322ce 100644 --- a/main.py +++ b/main.py @@ -8,7 +8,8 @@ from aiogram.enums import ParseMode from bot.config import settings from bot.handlers import get_handlers_router -from bot.middlewares import LoggingMiddleware +from bot.middlewares import LoggingMiddleware, I18nMiddleware +from bot.utils.db import db from bot.ui_commands import set_bot_commands # Setup logging @@ -36,6 +37,7 @@ async def main(): # Register middlewares (e.g. LoggingMiddleware) dp.update.outer_middleware(LoggingMiddleware()) + dp.update.outer_middleware(I18nMiddleware()) # Register routers (handlers) dp.include_router(get_handlers_router())