Initial commit
This commit is contained in:
207
.gitignore
vendored
Normal file
207
.gitignore
vendored
Normal 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
15
README.md
Normal 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
373
handlers/admin_handlers.py
Normal 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)
|
||||
)
|
||||
48
handlers/settings_handlers.py
Normal file
48
handlers/settings_handlers.py
Normal 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
98
handlers/user_handlers.py
Normal 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
181
keyboards.py
Normal 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
150
localization.py
Normal 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
128
main.py
Normal 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())
|
||||
Reference in New Issue
Block a user