make the bot.

This commit is contained in:
2026-07-02 20:51:08 +03:00
parent 382c345961
commit 330cbcecd3
22 changed files with 315 additions and 97 deletions
+2
View File
@@ -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
+3
View File
@@ -174,3 +174,6 @@ cython_debug/
# PyPI configuration file
.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>
+67 -1
View File
@@ -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 <repository_url>
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 <ID>`: 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.
+6
View File
@@ -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(
+3 -3
View File
@@ -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
-14
View File
@@ -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.")
+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))
+91 -47
View File
@@ -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()
-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)
+16
View File
@@ -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)
+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.
"""
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())
+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()
+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
+15
View File
@@ -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: <code>{id}</code>"
}
+15
View File
@@ -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 чату: <code>{id}</code>"
}
+3 -1
View File
@@ -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())