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())