Initial commit

This commit is contained in:
root
2025-09-15 00:47:01 +02:00
commit a50fa92542
8 changed files with 1200 additions and 0 deletions

207
.gitignore vendored Normal file
View File

@@ -0,0 +1,207 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[codz]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py.cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
#poetry.toml
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
#pdm.lock
#pdm.toml
.pdm-python
.pdm-build/
# pixi
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
#pixi.lock
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
# in the .venv directory. It is recommended not to include this directory in version control.
.pixi
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.envrc
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Abstra
# Abstra is an AI-powered process automation framework.
# Ignore directories containing user credentials, local state, and settings.
# Learn more at https://abstra.io/docs
.abstra/
# Visual Studio Code
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
# and can be added to the global gitignore or merged into this file. However, if you prefer,
# you could uncomment the following to ignore the entire vscode folder
# .vscode/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
# Cursor
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
# refer to https://docs.cursor.com/context/ignore-files
.cursorignore
.cursorindexingignore
# Marimo
marimo/_static/
marimo/_lsp/
__marimo__/

15
README.md Normal file
View File

@@ -0,0 +1,15 @@
# Connex
A Telegram bot that eases sharing VPN configurations with users
## Key Features
- Admin panel for managing users and configurations.
- Ability to add and edit tutorials for users to help them understand how to use client applications.
## Tech Stack
- **Programming language:** Python
- **Database:** SQLite
## Dependencies
- **aiogram** — for interacting with the Telegram API.
- **aiosqlite3** — for database operations.
- **asyncio**

373
handlers/admin_handlers.py Normal file
View File

@@ -0,0 +1,373 @@
# handlers/admin_handlers.py
import sqlite3
import asyncio
from aiogram import Router, F, types, Bot
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.enums import ContentType
from aiogram.exceptions import TelegramAPIError
from keyboards import (
get_main_keyboard_by_role, get_users_keyboard, user_management_keyboard,
get_users_for_configs_keyboard, get_user_configs_management_keyboard,
get_tutorials_admin_keyboard, get_skip_media_keyboard, get_confirm_send_keyboard
)
from localization import get_text
router = Router()
# --- Helper function to get admin's language ---
def get_admin_lang(user_id: int) -> str:
conn = sqlite3.connect('bot.db')
cursor = conn.cursor()
cursor.execute("SELECT language_code FROM users WHERE telegram_id = ?", (user_id,))
result = cursor.fetchone()
conn.close()
return result[0] if result else 'en'
# --- FSM States ---
class AdminStates(StatesGroup):
add_user_id = State()
add_config_type = State()
add_config_data = State()
add_tutorial_title = State()
add_tutorial_text = State()
add_tutorial_media = State()
mass_send_message = State()
mass_send_confirm = State()
# --- Main Menu Handler ---
@router.callback_query(F.data == "admin_menu")
async def process_admin_menu(callback: types.CallbackQuery, state: FSMContext):
await state.clear() # Clear any active state
lang = get_admin_lang(callback.from_user.id)
await callback.message.edit_text(
get_text('welcome_admin', lang),
reply_markup=get_main_keyboard_by_role(is_admin=True, lang=lang)
)
await callback.answer()
# --- User Management Section ---
@router.callback_query(F.data.startswith("admin_users_page_"))
async def process_users_list(callback: types.CallbackQuery):
page = int(callback.data.split("_")[-1])
lang = get_admin_lang(callback.from_user.id)
await callback.message.edit_text(
get_text('users_list', lang),
reply_markup=get_users_keyboard(page, lang)
)
await callback.answer()
@router.callback_query(F.data.startswith("manage_user_"))
async def process_manage_user(callback: types.CallbackQuery):
user_id = int(callback.data.split("_")[-1])
lang = get_admin_lang(callback.from_user.id)
conn = sqlite3.connect('bot.db')
cursor = conn.cursor()
cursor.execute("SELECT username FROM users WHERE telegram_id = ?", (user_id,))
user = cursor.fetchone()
conn.close()
username = user[0] if user and user[0] else "N/A"
text = get_text('manage_user_title', lang).format(user_id=user_id, username=username)
await callback.message.edit_text(
text,
reply_markup=user_management_keyboard(user_id, lang),
parse_mode="Markdown"
)
await callback.answer()
@router.callback_query(F.data.startswith("delete_user_"))
async def process_delete_user(callback: types.CallbackQuery):
user_id = int(callback.data.split("_")[-1])
lang = get_admin_lang(callback.from_user.id)
conn = sqlite3.connect('bot.db')
cursor = conn.cursor()
cursor.execute("DELETE FROM users WHERE telegram_id = ?", (user_id,))
# Configs will be deleted automatically due to "ON DELETE CASCADE" in the new DB schema
conn.commit()
conn.close()
await callback.answer(get_text('user_deleted_ok', lang))
await callback.message.edit_text(
get_text('users_list', lang),
reply_markup=get_users_keyboard(0, lang)
)
@router.callback_query(F.data == "add_user")
async def process_add_user_start(callback: types.CallbackQuery, state: FSMContext):
lang = get_admin_lang(callback.from_user.id)
await callback.message.edit_text(get_text('ask_for_user_id', lang))
await state.set_state(AdminStates.add_user_id)
await callback.answer()
@router.message(AdminStates.add_user_id)
async def process_add_user_id(message: types.Message, state: FSMContext):
lang = get_admin_lang(message.from_user.id)
try:
user_id = int(message.text)
conn = sqlite3.connect('bot.db')
cursor = conn.cursor()
cursor.execute("SELECT telegram_id FROM users WHERE telegram_id = ?", (user_id,))
if cursor.fetchone():
await message.answer(get_text('user_already_exists', lang))
else:
# Add user with default language 'en'
cursor.execute("INSERT INTO users (telegram_id, language_code) VALUES (?, ?)", (user_id, 'en'))
conn.commit()
await message.answer(get_text('user_added_ok', lang))
conn.close()
await state.clear()
await message.answer(
get_text('users_list', lang),
reply_markup=get_users_keyboard(0, lang)
)
except (ValueError, TypeError):
await message.answer(get_text('invalid_id_format', lang))
# --- Config Management Section ---
# (This section is also refactored to use the lang parameter)
@router.callback_query(F.data.startswith("admin_configs_page_"))
async def process_config_users_list(callback: types.CallbackQuery):
page = int(callback.data.split("_")[-1])
lang = get_admin_lang(callback.from_user.id)
await callback.message.edit_text(
get_text('choose_user_for_config', lang),
reply_markup=get_users_for_configs_keyboard(page, lang)
)
await callback.answer()
@router.callback_query(F.data.startswith("user_configs_manage_"))
async def process_user_configs_manage(callback: types.CallbackQuery):
user_id = int(callback.data.split("_")[-1])
lang = get_admin_lang(callback.from_user.id)
await callback.message.edit_text(
get_text('user_configs_title', lang),
reply_markup=get_user_configs_management_keyboard(user_id, lang)
)
await callback.answer()
@router.callback_query(F.data.startswith("delete_config:"))
async def process_delete_config(callback: types.CallbackQuery):
_, config_id, user_id = callback.data.split(":")
config_id, user_id = int(config_id), int(user_id)
lang = get_admin_lang(callback.from_user.id)
conn = sqlite3.connect('bot.db')
cursor = conn.cursor()
cursor.execute("DELETE FROM configs WHERE id = ?", (config_id,))
conn.commit()
conn.close()
await callback.answer(get_text('config_deleted_ok', lang))
await callback.message.edit_text(
get_text('user_configs_title', lang),
reply_markup=get_user_configs_management_keyboard(user_id, lang)
)
# ... (Add Config FSM flow refactored for localization)
@router.callback_query(F.data.startswith("add_config_"))
async def process_add_config_start(callback: types.CallbackQuery, state: FSMContext):
user_id = int(callback.data.split("_")[-1])
lang = get_admin_lang(callback.from_user.id)
await state.update_data(current_user_id=user_id)
await state.set_state(AdminStates.add_config_type)
await callback.message.edit_text(get_text('add_config_step1', lang))
await callback.answer()
@router.message(AdminStates.add_config_type)
async def process_add_config_type(message: types.Message, state: FSMContext):
lang = get_admin_lang(message.from_user.id)
await state.update_data(config_type=message.text)
await state.set_state(AdminStates.add_config_data)
await message.answer(get_text('add_config_step2', lang))
@router.message(AdminStates.add_config_data, F.content_type.in_({ContentType.TEXT, ContentType.DOCUMENT}))
async def process_add_config_data(message: types.Message, state: FSMContext):
lang = get_admin_lang(message.from_user.id)
data = await state.get_data()
user_id = data['current_user_id']
# ... (logic for handling file/text remains the same)
if message.document:
file_id = message.document.file_id
file_name = message.document.file_name
config_type = f"file:{file_name}"
config_data = file_id
else:
config_type = data['config_type']
config_data = message.text
conn = sqlite3.connect('bot.db')
cursor = conn.cursor()
cursor.execute("INSERT INTO configs (user_id, config_type, config_data) VALUES (?, ?, ?)",
(user_id, config_type, config_data))
conn.commit()
conn.close()
await state.clear()
await message.answer(get_text('config_added_ok', lang))
await message.answer(
get_text('user_configs_title', lang),
reply_markup=get_user_configs_management_keyboard(user_id, lang)
)
# --- Tutorial Management Section ---
@router.callback_query(F.data == "admin_tutorials_menu")
async def process_tutorials_menu(callback: types.CallbackQuery):
lang = get_admin_lang(callback.from_user.id)
await callback.message.edit_text(
get_text('tutorials_menu_title', lang),
reply_markup=get_tutorials_admin_keyboard(lang)
)
await callback.answer()
# ... (rest of the tutorial management refactored similarly)
@router.callback_query(F.data.startswith("delete_tutorial_"))
async def process_delete_tutorial(callback: types.CallbackQuery):
tutorial_id = int(callback.data.split("_")[-1])
lang = get_admin_lang(callback.from_user.id)
conn = sqlite3.connect('bot.db')
cursor = conn.cursor()
cursor.execute("DELETE FROM tutorials WHERE id = ?", (tutorial_id,))
conn.commit()
conn.close()
await callback.answer(get_text('tutorial_deleted_ok', lang))
await callback.message.edit_text(
get_text('tutorials_menu_title', lang),
reply_markup=get_tutorials_admin_keyboard(lang)
)
@router.callback_query(F.data == "add_tutorial")
async def process_add_tutorial_start(callback: types.CallbackQuery, state: FSMContext):
lang = get_admin_lang(callback.from_user.id)
await state.set_state(AdminStates.add_tutorial_title)
await callback.message.edit_text(get_text('add_tutorial_step1', lang))
await callback.answer()
@router.message(AdminStates.add_tutorial_title)
async def process_add_tutorial_title(message: types.Message, state: FSMContext):
lang = get_admin_lang(message.from_user.id)
await state.update_data(title=message.text)
await state.set_state(AdminStates.add_tutorial_text)
await message.answer(get_text('add_tutorial_step2', lang))
@router.message(AdminStates.add_tutorial_text)
async def process_add_tutorial_text(message: types.Message, state: FSMContext):
lang = get_admin_lang(message.from_user.id)
await state.update_data(text=message.text)
await state.set_state(AdminStates.add_tutorial_media)
await message.answer(
get_text('add_tutorial_step3', lang),
reply_markup=get_skip_media_keyboard(lang)
)
@router.callback_query(F.data == "skip_media", AdminStates.add_tutorial_media)
async def process_skip_media(callback: types.CallbackQuery, state: FSMContext):
lang = get_admin_lang(callback.from_user.id)
data = await state.get_data()
# ... (DB logic is the same)
conn = sqlite3.connect('bot.db')
cursor = conn.cursor()
cursor.execute("INSERT INTO tutorials (title, content_text) VALUES (?, ?)", (data['title'], data['text']))
conn.commit()
conn.close()
await state.clear()
await callback.message.edit_text(get_text('tutorial_added_ok_no_media', lang))
await callback.message.answer(
get_text('tutorials_menu_title', lang),
reply_markup=get_tutorials_admin_keyboard(lang)
)
await callback.answer()
@router.message(AdminStates.add_tutorial_media, F.content_type.in_({ContentType.PHOTO, ContentType.VIDEO}))
async def process_add_tutorial_media(message: types.Message, state: FSMContext):
lang = get_admin_lang(message.from_user.id)
# ... (DB and file_id logic is the same)
file_id = ""
if message.photo:
file_id = message.photo[-1].file_id
elif message.video:
file_id = message.video.file_id
data = await state.get_data()
conn = sqlite3.connect('bot.db')
cursor = conn.cursor()
cursor.execute("INSERT INTO tutorials (title, content_text, file_id) VALUES (?, ?, ?)",
(data['title'], data['text'], file_id))
conn.commit()
conn.close()
await state.clear()
await message.answer(get_text('tutorial_added_ok_with_media', lang))
await message.answer(
get_text('tutorials_menu_title', lang),
reply_markup=get_tutorials_admin_keyboard(lang)
)
# --- Mass Messaging Section ---
@router.callback_query(F.data == "mass_send_start")
async def process_mass_send_start(callback: types.CallbackQuery, state: FSMContext):
lang = get_admin_lang(callback.from_user.id)
await state.set_state(AdminStates.mass_send_message)
await callback.message.edit_text(get_text('mass_send_ask_message', lang))
await callback.answer()
@router.message(AdminStates.mass_send_message)
async def process_mass_send_message(message: types.Message, state: FSMContext):
lang = get_admin_lang(message.from_user.id)
await state.update_data(message_to_send=message)
await state.set_state(AdminStates.mass_send_confirm)
await message.answer(
get_text('mass_send_confirm_message', lang),
reply_markup=get_confirm_send_keyboard(lang)
)
@router.callback_query(F.data == "send_cancelled", AdminStates.mass_send_confirm)
async def process_send_cancelled(callback: types.CallbackQuery, state: FSMContext):
lang = get_admin_lang(callback.from_user.id)
await state.clear()
await callback.message.edit_text(
get_text('mass_send_cancelled', lang),
reply_markup=get_main_keyboard_by_role(is_admin=True, lang=lang)
)
await callback.answer()
@router.callback_query(F.data == "send_confirmed", AdminStates.mass_send_confirm)
async def process_send_confirmed(callback: types.CallbackQuery, state: FSMContext, bot: Bot):
lang = get_admin_lang(callback.from_user.id)
data = await state.get_data()
message_to_send = data['message_to_send']
await state.clear()
await callback.message.edit_text(get_text('mass_send_started', lang), reply_markup=None)
conn = sqlite3.connect('bot.db')
cursor = conn.cursor()
cursor.execute("SELECT telegram_id FROM users WHERE is_admin = 0")
user_ids = cursor.fetchall()
conn.close()
success_count = 0
fail_count = 0
for (user_id,) in user_ids:
try:
await message_to_send.copy_to(chat_id=user_id)
success_count += 1
except TelegramAPIError as e:
print(f"Failed to send to {user_id}: {e}")
fail_count += 1
await asyncio.sleep(0.1)
result_text = get_text('mass_send_finished', lang).format(
success_count=success_count,
fail_count=fail_count
)
await callback.message.answer(
result_text,
reply_markup=get_main_keyboard_by_role(is_admin=True, lang=lang)
)

View File

@@ -0,0 +1,48 @@
# handlers/settings_handlers.py
import sqlite3
from aiogram import Router, F, types
from keyboards import get_language_choice_keyboard, get_main_keyboard_by_role
from localization import get_text
router = Router()
@router.callback_query(F.data == "settings")
async def process_settings(callback: types.CallbackQuery):
# Получаем язык пользователя для корректного отображения меню
conn = sqlite3.connect('bot.db')
cursor = conn.cursor()
cursor.execute("SELECT language_code FROM users WHERE telegram_id = ?", (callback.from_user.id,))
lang = cursor.fetchone()[0]
conn.close()
await callback.message.edit_text(
get_text('choose_language', lang),
reply_markup=get_language_choice_keyboard()
)
await callback.answer()
@router.callback_query(F.data.startswith("set_lang_"))
async def process_set_language(callback: types.CallbackQuery):
lang_code = callback.data.split("_")[-1]
user_id = callback.from_user.id
conn = sqlite3.connect('bot.db')
cursor = conn.cursor()
cursor.execute("UPDATE users SET language_code = ? WHERE telegram_id = ?", (lang_code, user_id))
cursor.execute("SELECT is_admin FROM users WHERE telegram_id = ?", (user_id,))
is_admin = cursor.fetchone()[0]
conn.commit()
conn.close()
await callback.message.edit_text(get_text('language_changed', lang_code))
# Показываем главное меню на новом языке
main_menu_text = get_text('welcome_admin' if is_admin else 'welcome', lang_code)
await callback.message.answer(
main_menu_text,
reply_markup=get_main_keyboard_by_role(is_admin, lang_code)
)
await callback.answer()

98
handlers/user_handlers.py Normal file
View File

@@ -0,0 +1,98 @@
# handlers/user_handlers.py
import sqlite3
from aiogram import Router, F, types, Bot
from keyboards import get_main_keyboard_by_role, get_tutorials_user_keyboard
from localization import get_text
router = Router()
# Вспомогательная функция для получения языка пользователя
def get_user_lang(user_id: int) -> str:
conn = sqlite3.connect('bot.db')
cursor = conn.cursor()
cursor.execute("SELECT language_code FROM users WHERE telegram_id = ?", (user_id,))
result = cursor.fetchone()
conn.close()
return result[0] if result else 'en'
# Новый обработчик для кнопки "Назад в меню" из раздела помощи
@router.callback_query(F.data == "user_main_menu")
async def process_back_to_main_menu(callback: types.CallbackQuery):
lang = get_user_lang(callback.from_user.id)
await callback.message.edit_text(
get_text('welcome', lang),
reply_markup=get_main_keyboard_by_role(is_admin=False, lang=lang)
)
await callback.answer()
@router.callback_query(F.data == "user_configs")
async def process_user_configs(callback: types.CallbackQuery, bot: Bot):
user_id = callback.from_user.id
lang = get_user_lang(user_id)
conn = sqlite3.connect('bot.db')
cursor = conn.cursor()
cursor.execute("SELECT config_type, config_data FROM configs WHERE user_id = ?", (user_id,))
user_configs = cursor.fetchall()
conn.close()
if not user_configs:
await callback.message.answer(get_text('no_configs_yet', lang))
else:
await callback.message.answer(get_text('your_configs', lang))
for config_type, config_data in user_configs:
if config_type.startswith("file:"):
file_id = config_data
caption = f"{get_text('config_type', lang)}: {config_type.split(':', 1)[1]}"
await bot.send_document(chat_id=user_id, document=file_id, caption=caption)
else:
await callback.message.answer(f"{get_text('config_type', lang)}: `{config_type}`\n\n`{config_data}`", parse_mode="Markdown")
await callback.message.answer(
get_text('next_action', lang),
reply_markup=get_main_keyboard_by_role(is_admin=False, lang=lang)
)
await callback.answer()
@router.callback_query(F.data == "user_help")
async def process_user_help(callback: types.CallbackQuery):
lang = get_user_lang(callback.from_user.id)
await callback.message.edit_text(
get_text('choose_tutorial', lang),
reply_markup=get_tutorials_user_keyboard(lang)
)
await callback.answer()
@router.callback_query(F.data.startswith("view_tutorial_"))
async def process_view_tutorial(callback: types.CallbackQuery, bot: Bot):
user_id = callback.from_user.id
lang = get_user_lang(user_id)
tutorial_id = int(callback.data.split("_")[-1])
conn = sqlite3.connect('bot.db')
cursor = conn.cursor()
cursor.execute("SELECT content_text, file_id FROM tutorials WHERE id = ?", (tutorial_id,))
tutorial = cursor.fetchone()
conn.close()
if tutorial:
content_text, file_id = tutorial
# Сначала удаляем предыдущее сообщение с кнопками
await callback.message.delete()
if file_id:
try:
await bot.send_photo(chat_id=user_id, photo=file_id, caption=content_text)
except:
await bot.send_video(chat_id=user_id, video=file_id, caption=content_text)
else:
await callback.message.answer(content_text)
await callback.message.answer(
get_text('next_action', lang),
reply_markup=get_main_keyboard_by_role(is_admin=False, lang=lang)
)
else:
await callback.answer(get_text('error_not_found', lang), show_alert=True)
await callback.answer()

181
keyboards.py Normal file
View File

@@ -0,0 +1,181 @@
# keyboards.py
import sqlite3
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
from localization import get_text
USERS_PER_PAGE = 5
# --- ОСНОВНЫЕ КЛАВИАТУРЫ ---
def get_main_keyboard_by_role(is_admin: bool, lang: str) -> InlineKeyboardMarkup:
"""Возвращает админскую или пользовательскую клавиатуру на нужном языке."""
buttons = []
if is_admin:
buttons = [
[InlineKeyboardButton(text=get_text('manage_users_btn', lang), callback_data="admin_users_page_0")],
[InlineKeyboardButton(text=get_text('manage_configs_btn', lang), callback_data="admin_configs_page_0")],
[InlineKeyboardButton(text=get_text('manage_tutorials_btn', lang), callback_data="admin_tutorials_menu")],
[InlineKeyboardButton(text=get_text('mass_send_btn', lang), callback_data="mass_send_start")]
]
else:
buttons = [
[InlineKeyboardButton(text=get_text('my_configs_btn', lang), callback_data="user_configs")],
[InlineKeyboardButton(text=get_text('help_btn', lang), callback_data="user_help")]
]
# Добавляем кнопку настроек для всех
buttons.append([InlineKeyboardButton(text=get_text('settings', lang), callback_data="settings")])
return InlineKeyboardMarkup(inline_keyboard=buttons)
def get_back_to_menu_keyboard(lang: str) -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text=get_text('back_to_menu', lang), callback_data="admin_menu")]
])
# --- НАСТРОЙКИ ЯЗЫКА ---
def get_language_choice_keyboard() -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="English 🇬🇧", callback_data="set_lang_en")],
[InlineKeyboardButton(text="Русский 🇷🇺", callback_data="set_lang_ru")]
])
# --- УПРАВЛЕНИЕ ПОЛЬЗОВАТЕЛЯМИ ---
def get_users_keyboard(page: int, lang: str) -> InlineKeyboardMarkup:
conn = sqlite3.connect('bot.db')
cursor = conn.cursor()
cursor.execute("SELECT COUNT(*) FROM users WHERE is_admin = 0")
total_users = cursor.fetchone()[0]
offset = page * USERS_PER_PAGE
cursor.execute("SELECT telegram_id, username FROM users WHERE is_admin = 0 LIMIT ? OFFSET ?", (USERS_PER_PAGE, offset))
users = cursor.fetchall()
conn.close()
keyboard = []
for user_id, username in users:
button_text = f"@{username}" if username else f"ID: {user_id}"
keyboard.append([InlineKeyboardButton(text=button_text, callback_data=f"manage_user_{user_id}")])
nav_buttons = []
if page > 0:
nav_buttons.append(InlineKeyboardButton(text=get_text('prev_btn', lang), callback_data=f"admin_users_page_{page-1}"))
if (page + 1) * USERS_PER_PAGE < total_users:
nav_buttons.append(InlineKeyboardButton(text=get_text('next_btn', lang), callback_data=f"admin_users_page_{page+1}"))
if nav_buttons:
keyboard.append(nav_buttons)
keyboard.append([InlineKeyboardButton(text=get_text('add_user_btn', lang), callback_data="add_user")])
keyboard.append([InlineKeyboardButton(text=get_text('back_to_menu', lang), callback_data="admin_menu")])
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def user_management_keyboard(user_id: int, lang: str) -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text=get_text('delete_user_btn', lang), callback_data=f"delete_user_{user_id}")],
[InlineKeyboardButton(text=get_text('back_to_list_btn', lang), callback_data="admin_users_page_0")]
])
# --- УПРАВЛЕНИЕ КОНФИГУРАЦИЯМИ ---
def get_users_for_configs_keyboard(page: int, lang: str) -> InlineKeyboardMarkup:
# Эта функция дублирует get_users_keyboard, но с другими callback_data
conn = sqlite3.connect('bot.db')
cursor = conn.cursor()
cursor.execute("SELECT COUNT(*) FROM users WHERE is_admin = 0")
total_users = cursor.fetchone()[0]
offset = page * USERS_PER_PAGE
cursor.execute("SELECT telegram_id, username FROM users WHERE is_admin = 0 LIMIT ? OFFSET ?", (USERS_PER_PAGE, offset))
users = cursor.fetchall()
conn.close()
keyboard = []
for user_id, username in users:
button_text = f"@{username}" if username else f"ID: {user_id}"
keyboard.append([InlineKeyboardButton(text=button_text, callback_data=f"user_configs_manage_{user_id}")])
nav_buttons = []
if page > 0:
nav_buttons.append(InlineKeyboardButton(text=get_text('prev_btn', lang), callback_data=f"admin_configs_page_{page-1}"))
if (page + 1) * USERS_PER_PAGE < total_users:
nav_buttons.append(InlineKeyboardButton(text=get_text('next_btn', lang), callback_data=f"admin_configs_page_{page+1}"))
if nav_buttons:
keyboard.append(nav_buttons)
keyboard.append([InlineKeyboardButton(text=get_text('back_to_menu', lang), callback_data="admin_menu")])
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def get_user_configs_management_keyboard(user_id: int, lang: str) -> InlineKeyboardMarkup:
conn = sqlite3.connect('bot.db')
cursor = conn.cursor()
cursor.execute("SELECT id, config_type, config_data FROM configs WHERE user_id = ?", (user_id,))
configs = cursor.fetchall()
conn.close()
keyboard = []
for config_id, config_type, config_data in configs:
if config_type.startswith("file:"):
display_text = f"{get_text('file_prefix', lang)}: {config_type.split(':', 1)[1]}"
else:
short_data = config_data[:20] + '...' if len(config_data) > 20 else config_data
display_text = f"{config_type}: {short_data}"
button_text = f"{get_text('delete_config_prefix', lang)} {display_text}"
keyboard.append([InlineKeyboardButton(text=button_text, callback_data=f"delete_config:{config_id}:{user_id}")])
keyboard.append([InlineKeyboardButton(text=get_text('add_config_btn', lang), callback_data=f"add_config_{user_id}")])
keyboard.append([InlineKeyboardButton(text=get_text('back_to_users_list_btn', lang), callback_data="admin_configs_page_0")])
keyboard.append([InlineKeyboardButton(text=get_text('main_menu', lang), callback_data="admin_menu")])
return InlineKeyboardMarkup(inline_keyboard=keyboard)
# --- УПРАВЛЕНИЕ ТУТОРИАЛАМИ ---
def get_tutorials_admin_keyboard(lang: str) -> InlineKeyboardMarkup:
conn = sqlite3.connect('bot.db')
cursor = conn.cursor()
cursor.execute("SELECT id, title FROM tutorials")
tutorials = cursor.fetchall()
conn.close()
keyboard = []
for tutorial_id, title in tutorials:
keyboard.append([InlineKeyboardButton(text=f"{get_text('delete_tutorial_prefix', lang)} {title}", callback_data=f"delete_tutorial_{tutorial_id}")])
keyboard.append([InlineKeyboardButton(text=get_text('add_tutorial_btn', lang), callback_data="add_tutorial")])
keyboard.append([InlineKeyboardButton(text=get_text('back_to_menu', lang), callback_data="admin_menu")])
return InlineKeyboardMarkup(inline_keyboard=keyboard)
def get_tutorials_user_keyboard(lang: str) -> InlineKeyboardMarkup:
conn = sqlite3.connect('bot.db')
cursor = conn.cursor()
cursor.execute("SELECT id, title FROM tutorials")
tutorials = cursor.fetchall()
conn.close()
keyboard = []
if not tutorials:
keyboard.append([InlineKeyboardButton(text=get_text('no_tutorials_yet', lang), callback_data="no_op")])
else:
for tutorial_id, title in tutorials:
keyboard.append([InlineKeyboardButton(text=f"📖 {title}", callback_data=f"view_tutorial_{tutorial_id}")])
# Добавляем кнопку "Назад", которая вернет пользователя в главное меню
# Для этого нам нужна информация о его роли
# Проще всего будет, если обработчик этой кнопки сам вернет главное меню
# Поэтому здесь просто ставим callback_data, который поймает user_handler
keyboard.append([InlineKeyboardButton(text=get_text('back_to_menu', 'ru' if lang=='ru' else 'en'), callback_data="user_main_menu")])
return InlineKeyboardMarkup(inline_keyboard=keyboard)
# --- РАССЫЛКА И ПРОЧЕЕ ---
def get_confirm_send_keyboard(lang: str) -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text=get_text('send_btn', lang), callback_data="send_confirmed")],
[InlineKeyboardButton(text=get_text('cancel_btn', lang), callback_data="send_cancelled")]
])
def get_skip_media_keyboard(lang: str) -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text=get_text('skip_btn', lang), callback_data="skip_media")]
])

150
localization.py Normal file
View File

@@ -0,0 +1,150 @@
# localization.py
locales = {
'en': {
# General
'welcome': "👋 Welcome!\n\nUse the menu to navigate.",
'welcome_admin': "👋 Welcome, Administrator!\n\nChoose an action:",
'back_to_menu': "⬅️ Back to Menu",
'main_menu': "🏠 Main Menu",
'error_not_found': "Not found.",
'under_development': "This section is under development.",
# Settings
'settings': "⚙️ Settings",
'choose_language': "Please choose your language:",
'language_changed': "Language has been changed to English 🇬🇧",
# User Menu
'my_configs_btn': "🔑 My Configurations",
'help_btn': "❓ Help",
'no_configs_yet': "You have no available configurations yet.",
'your_configs': "Your configurations:",
'next_action': "Choose the next action:",
'config_type': "Type",
# Help/Tutorials
'choose_tutorial': "Choose a tutorial you are interested in:",
'no_tutorials_yet': "No tutorials yet",
# Admin Menu
'manage_users_btn': "👤 User Management",
'manage_configs_btn': "🔑 Configuration Management",
'manage_tutorials_btn': "📚 Tutorial Management",
'mass_send_btn': "📢 Mass Messaging",
# User Management
'users_list': "👥 User List:",
'prev_btn': "⬅️ Prev.",
'next_btn': "➡️ Next.",
'add_user_btn': " Add User",
'manage_user_title': "Managing user:\nID: `{user_id}`\nUsername: @{username}",
'delete_user_btn': "🗑️ Delete User",
'back_to_list_btn': "⬅️ Back to List",
'user_deleted_ok': "User and their configurations have been deleted.",
'ask_for_user_id': "Enter the Telegram ID of the new user.\n\nTo cancel, press /start.",
'user_already_exists': "This user already exists in the database.",
'user_added_ok': "✅ User successfully added!",
'invalid_id_format': "Invalid format. Please enter a numerical Telegram ID.",
# Config Management
'choose_user_for_config': "Choose a user to manage their configurations:",
'user_configs_title': "User configurations:",
'delete_config_prefix': "🗑️",
'file_prefix': "File",
'add_config_btn': " Add Config",
'back_to_users_list_btn': "⬅️ Back to User List",
'config_deleted_ok': "Configuration deleted.",
'add_config_step1': "Step 1/2: Enter the configuration type (e.g., VLESS, WireGuard, SS).\nIf this is a file, the type will be used as its description.",
'add_config_step2': "Step 2/2: Now, send the configuration data (link, text, or **file**).",
'config_added_ok': "✅ Configuration successfully added!",
# Tutorial Management
'tutorials_menu_title': "Tutorial management menu:",
'delete_tutorial_prefix': "🗑️",
'add_tutorial_btn': " Add Tutorial",
'tutorial_deleted_ok': "Tutorial deleted.",
'add_tutorial_step1': "Step 1/3: Enter the tutorial title:",
'add_tutorial_step2': "Step 2/3: Enter the main text of the tutorial:",
'add_tutorial_step3': "Step 3/3: Now attach a photo or video. If no media is required, press 'Skip'.",
'skip_btn': "Skip Step ➡️",
'tutorial_added_ok_no_media': "✅ Tutorial without media added successfully.",
'tutorial_added_ok_with_media': "✅ Tutorial with media added successfully.",
# Mass Messaging
'mass_send_ask_message': "Enter the message for mass sending to all users. It will be copied and sent.",
'mass_send_confirm_message': "This message will be sent to all users. Confirm sending:",
'send_btn': "✅ Send",
'cancel_btn': "❌ Cancel",
'mass_send_cancelled': "Mass messaging cancelled.",
'mass_send_started': "⏳ Starting mass messaging...",
'mass_send_finished': "✅ Mass messaging finished!\n\nSuccessfully sent: {success_count}\nFailed to deliver: {fail_count}",
},
'ru': {
# General
'welcome': "👋 Добро пожаловать!\n\nИспользуйте меню для навигации.",
'welcome_admin': "👋 Добро пожаловать, Администратор!\n\nВыберите действие:",
'back_to_menu': "⬅️ Назад в меню",
'main_menu': "🏠 Главное меню",
'error_not_found': "Не найдено.",
'under_development': "Этот раздел находится в разработке.",
# Settings
'settings': "⚙️ Настройки",
'choose_language': "Пожалуйста, выберите язык:",
'language_changed': "Язык изменен на Русский 🇷🇺",
# User Menu
'my_configs_btn': "🔑 Мои конфигурации",
'help_btn': "❓ Помощь",
'no_configs_yet': "У вас пока нет доступных конфигураций.",
'your_configs': "Ваши конфигурации:",
'next_action': "Выберите следующее действие:",
'config_type': "Тип",
# Help/Tutorials
'choose_tutorial': "Выберите интересующий вас туториал:",
'no_tutorials_yet': "Туториалов пока нет",
# Admin Menu
'manage_users_btn': "👤 Управление пользователями",
'manage_configs_btn': "🔑 Управление конфигурациями",
'manage_tutorials_btn': "📚 Управление туториалами",
'mass_send_btn': "📢 Сделать рассылку",
# User Management
'users_list': "👥 Список пользователей:",
'prev_btn': "⬅️ Пред.",
'next_btn': "➡️ След.",
'add_user_btn': " Добавить пользователя",
'manage_user_title': "Управление пользователем:\nID: `{user_id}`\nUsername: @{username}",
'delete_user_btn': "🗑️ Удалить пользователя",
'back_to_list_btn': "⬅️ Назад к списку",
'user_deleted_ok': "Пользователь и его конфигурации удалены.",
'ask_for_user_id': "Введите Telegram ID нового пользователя.\n\nЧтобы отменить, нажмите /start.",
'user_already_exists': "Этот пользователь уже существует в базе.",
'user_added_ok': "✅ Пользователь успешно добавлен!",
'invalid_id_format': "❗️Неверный формат. Пожалуйста, введите числовой Telegram ID.",
# Config Management
'choose_user_for_config': "Выберите пользователя для управления его конфигурациями:",
'user_configs_title': "Конфигурации пользователя:",
'delete_config_prefix': "🗑️",
'file_prefix': "Файл",
'add_config_btn': " Добавить конфиг",
'back_to_users_list_btn': "⬅️ Назад к списку пользователей",
'config_deleted_ok': "Конфигурация удалена.",
'add_config_step1': "Шаг 1/2: Введите тип конфигурации (например, VLESS, WireGuard, SS).\nЕсли это файл, тип будет использован как его описание.",
'add_config_step2': "Шаг 2/2: Теперь отправьте данные конфигурации (ссылку, текст или **файл**).",
'config_added_ok': "✅ Конфигурация успешно добавлена!",
# Tutorial Management
'tutorials_menu_title': "Меню управления туториалами:",
'delete_tutorial_prefix': "🗑️",
'add_tutorial_btn': " Добавить туториал",
'tutorial_deleted_ok': "Туториал удален.",
'add_tutorial_step1': "Шаг 1/3: Введите заголовок туториала:",
'add_tutorial_step2': "Шаг 2/3: Введите основной текст туториала:",
'add_tutorial_step3': "Шаг 3/3: Теперь прикрепите фото или видео. Если медиа не требуется, нажмите 'Пропустить'.",
'skip_btn': "Пропустить шаг ➡️",
'tutorial_added_ok_no_media': "✅ Туториал без медиа успешно добавлен.",
'tutorial_added_ok_with_media': "✅ Туториал с медиа успешно добавлен.",
# Mass Messaging
'mass_send_ask_message': "Введите сообщение для рассылки всем пользователям. Оно будет скопировано и отправлено.",
'mass_send_confirm_message': "Это сообщение будет отправлено всем пользователям. Подтвердите рассылку:",
'send_btn': "✅ Отправить",
'cancel_btn': "❌ Отмена",
'mass_send_cancelled': "Рассылка отменена.",
'mass_send_started': "⏳ Начинаю рассылку...",
'mass_send_finished': "✅ Рассылка завершена!\n\nУспешно отправлено: {success_count}\nНе удалось доставить: {fail_count}",
}
}
def get_text(key: str, lang: str = 'en'):
"""Возвращает текст по ключу для заданного языка, с фолбэком на английский."""
return locales.get(lang, locales['en']).get(key, f"<{key}>")

128
main.py Normal file
View File

@@ -0,0 +1,128 @@
# main.py
import asyncio
import logging
import sqlite3
from aiogram import Bot, Dispatcher, types
from aiogram.filters.command import Command
# --- Import local modules ---
from keyboards import get_main_keyboard_by_role
from handlers import admin_handlers, user_handlers, settings_handlers # Import new settings handler
from localization import get_text
# --- Settings ---
BOT_TOKEN = "YOUR_BOT_TOKEN"
ADMIN_IDS = [YOUR_TELEGRAM_ID]
# --- Initialization ---
bot = Bot(token=BOT_TOKEN)
dp = Dispatcher()
# --- Database Initialization ---
def init_db():
"""Initializes the database and tables."""
conn = sqlite3.connect('bot.db')
cursor = conn.cursor()
# Enable foreign key support
cursor.execute("PRAGMA foreign_keys = ON")
# Create users table with a new language_code column
cursor.execute('''
CREATE TABLE IF NOT EXISTS users (
telegram_id INTEGER PRIMARY KEY,
username TEXT,
is_admin INTEGER DEFAULT 0,
language_code TEXT DEFAULT 'en'
)''')
# Create configs table with ON DELETE CASCADE
# This automatically deletes a user's configs when the user is deleted
cursor.execute('''
CREATE TABLE IF NOT EXISTS configs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
config_type TEXT,
config_data TEXT,
FOREIGN KEY (user_id) REFERENCES users (telegram_id) ON DELETE CASCADE
)''')
# Create tutorials table
cursor.execute('''
CREATE TABLE IF NOT EXISTS tutorials (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT,
content_text TEXT,
file_id TEXT
)''')
# Add/update admins from the ADMIN_IDS list
for admin_id in ADMIN_IDS:
cursor.execute(
"INSERT INTO users (telegram_id, is_admin, language_code) VALUES (?, 1, 'en') "
"ON CONFLICT(telegram_id) DO UPDATE SET is_admin = 1",
(admin_id,)
)
conn.commit()
conn.close()
# --- Register Routers ---
dp.include_router(admin_handlers.router)
dp.include_router(user_handlers.router)
dp.include_router(settings_handlers.router) # Register the new settings router
# --- /start Command Handler ---
@dp.message(Command("start"))
async def send_welcome(message: types.Message):
"""
Handles the /start command.
Greets the user, adds them to the DB if they are new,
and shows the appropriate menu in their selected language.
"""
user_id = message.from_user.id
username = message.from_user.username
conn = sqlite3.connect('bot.db')
cursor = conn.cursor()
# Check if user exists
cursor.execute("SELECT is_admin, language_code FROM users WHERE telegram_id = ?", (user_id,))
result = cursor.fetchone()
if result:
# User exists, get their role and language
is_admin, lang = result
else:
# New user, add to DB with default language 'en'
is_admin = 1 if user_id in ADMIN_IDS else 0
lang = 'en'
cursor.execute(
"INSERT INTO users (telegram_id, username, is_admin, language_code) VALUES (?, ?, ?, ?)",
(user_id, username, is_admin, lang)
)
conn.commit()
conn.close()
# Determine the welcome text based on role and language
welcome_text_key = 'welcome_admin' if is_admin else 'welcome'
welcome_text = get_text(welcome_text_key, lang)
# Show the main menu
await message.answer(
welcome_text,
reply_markup=get_main_keyboard_by_role(is_admin, lang)
)
# --- Entry Point ---
async def main():
"""Main function to start the bot."""
logging.basicConfig(level=logging.INFO)
init_db() # Initialize the database on startup
await dp.start_polling(bot)
if __name__ == "__main__":
asyncio.run(main())