типо да

This commit is contained in:
admin
2025-09-08 00:40:18 +07:00
commit 0f05fc8455
83 changed files with 5775 additions and 0 deletions

35
.dockerignore Normal file
View File

@@ -0,0 +1,35 @@
# Исключить скрытые системные каталоги, но не всё подряд
.git/
.gitattributes
.gitignore
# Виртуальные окружения и Python-кэш
.venv/
venv/
__pycache__/
*.py[cod]
*.pyo
# IDE-файлы
.idea/
.vscode/
# Тесты и документация
tests/
test/
docs/
examples/
# Логи и артефакты сборки
*.log
*.logs
*.log.*
*.logs.*
Logs/
Log/
dist/
build/
# Примеры и шаблоны
.env
env

92
.env_example Normal file
View File

@@ -0,0 +1,92 @@
# Токены бота
BOT_TOKEN=your_bot_token_here
BOT_DEBUG_TOKEN=your_debug_bot_token_here
# Режим отладки
DEBUG=False
# Владелец бота
OWNER=@verdise
# Основные настройки
PARSE_MODE=HTML
ENCOD=utf-8
TIME_FORMAT=%Y-%m-%d %H:%M:%S
PREFIX=/!.&?
BOT_LANGUAGE=Aiogram3
# Настройки сообщений
DISABLE_NOTIFICATION=False
PROTECT_CONTENT=False
ALLOW_SENDING_WITHOUT_REPLY=True
LINK_PREVIEW_IS_DISABLED=False
LINK_PREVIEW_PREFER_SMALL_MEDIA=False
LINK_PREVIEW_PREFER_LARGE_MEDIA=True
LINK_PREVIEW_SHOW_ABOVE_TEXT=False
SHOW_CAPTION_ABOVE_MEDIA=False
# Разрешения
BOT_EDIT=False
START_INFO_CONSOLE=True
START_INFO_TO_FILE=True
# Логирование
LOG_CONSOLE=True
LOG_FILE=True
LOG_DIR=Logs
LOG_FILE_INFO=bot_info.log
# Вебхук
WEBHOOK=False
# API ключи
API_KEY=your_api_key
WEB_API_KEY=your_web_api_key
WEATHER_API_KEY=your_weather_api_key
# Telegram API ID и HASH
TG_API_UID=123456
TG_API_HASH=your_tg_api_hash
# Важные ID
ADMIN_ID=123456789
MODERATOR_ID=987654321
IMPORTANT_ID=1122334455
IMPORTANT_GROUP_ID=-1001122334455
IMPORTANT_CHANNEL_ID=-1009988776655
# Настройки бота
PROJECT_NAME=PRIMO
BOT_NAME=Первозданная Жемчужина
BOT_DESCRIPTION=Ваш помощник в удивительные миры! Prod. by:『@verdise』
BOT_SHORT_DESCRIPTION=Тех.поддержка: @verdise
# Настройки ролевого проекта
RP_NAME: str = "𝘗𝘳𝘪𝘮𝘰 𝘞𝘰𝘳𝘭𝘥"
# Права администратора
ANONYMOUS=False
MANAGE_CHAT=True
CHANGE_INFO=True
PROMOTE_MEMBERS=True
RESTRICT_MEMBERS=True
POST_MESSAGE=True
MANAGE_TOPICS=True
INVITE_USER=True
DELETE_MESSAGES=True
MANAGE_VIDEO_CHATS=True
EDIT_MESSAGES=True
PIN_MESSAGE=True
POST_STORIES=True
EDIT_STORIES=True
DELETE_STORIES=True
# Поддержка
SUPPORT_CHAT_ID=0

97
.gitattributes vendored Normal file
View File

@@ -0,0 +1,97 @@
# =============================================================================
# Git LFS: большие бинарные файлы, модели, архивы
# =============================================================================
*.7z filter=lfs diff=lfs merge=lfs -text
*.arrow filter=lfs diff=lfs merge=lfs -text
*.bin filter=lfs diff=lfs merge=lfs -text
*.bz2 filter=lfs diff=lfs merge=lfs -text
*.ckpt filter=lfs diff=lfs merge=lfs -text
*.ftz filter=lfs diff=lfs merge=lfs -text
*.gz filter=lfs diff=lfs merge=lfs -text
*.h5 filter=lfs diff=lfs merge=lfs -text
*.joblib filter=lfs diff=lfs merge=lfs -text
*.lfs.* filter=lfs diff=lfs merge=lfs -text
*.mlmodel filter=lfs diff=lfs merge=lfs -text
*.model filter=lfs diff=lfs merge=lfs -text
*.msgpack filter=lfs diff=lfs merge=lfs -text
*.npy filter=lfs diff=lfs merge=lfs -text
*.npz filter=lfs diff=lfs merge=lfs -text
*.onnx filter=lfs diff=lfs merge=lfs -text
*.ot filter=lfs diff=lfs merge=lfs -text
*.parquet filter=lfs diff=lfs merge=lfs -text
*.pb filter=lfs diff=lfs merge=lfs -text
*.pickle filter=lfs diff=lfs merge=lfs -text
*.pkl filter=lfs diff=lfs merge=lfs -text
*.pt filter=lfs diff=lfs merge=lfs -text
*.pth filter=lfs diff=lfs merge=lfs -text
*.rar filter=lfs diff=lfs merge=lfs -text
*.safetensors filter=lfs diff=lfs merge=lfs -text
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
*.tar.* filter=lfs diff=lfs merge=lfs -text
*.tar filter=lfs diff=lfs merge=lfs -text
*.tflite filter=lfs diff=lfs merge=lfs -text
*.tgz filter=lfs diff=lfs merge=lfs -text
*.wasm filter=lfs diff=lfs merge=lfs -text
*.xz filter=lfs diff=lfs merge=lfs -text
*.zip filter=lfs diff=lfs merge=lfs -text
*.zst filter=lfs diff=lfs merge=lfs -text
*tfevents* filter=lfs diff=lfs merge=lfs -text
# =============================================================================
# Автоопределение текста, окончания строк
# =============================================================================
* text=auto eol=lf
# =============================================================================
# Текстовые файлы (Python, конфиги, документы)
# =============================================================================
*.py text
*.pyi text
*.ipynb text
*.html text
*.css text
*.js text
*.json text
*.md text
*.yml text
*.yaml text
*.xml text
*.txt text
*.cfg text
*.toml text
*.ini text
*.env text
# =============================================================================
# Изображения
# =============================================================================
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.bmp binary
*.webp binary
*.ico binary
*.svg text
# =============================================================================
# Шрифты
# =============================================================================
*.eot binary
*.ttf binary
*.woff binary
*.woff2 binary
*.otf binary
# =============================================================================
# GitHub Linguist (указание языка для отображения)
# =============================================================================
*.py linguist-language=Python
*.ipynb linguist-language=Jupyter Notebook
*.html linguist-language=HTML
*.css linguist-language=CSS
*.js linguist-language=JavaScript
*.json linguist-language=JSON
*.md linguist-language=Markdown
*.yml linguist-language=YAML
*.yaml linguist-language=YAML

73
.gitignore vendored Normal file
View File

@@ -0,0 +1,73 @@
# .gitignore: Игнорируемые файлы для Python проектов
# Подробнее: https://github.com/github/gitignore/blob/main/Python.gitignore
### Python ###
# Виртуальные окружения и настройки
.venv
.env
env
venv/
env/
env.bak/
venv.bak/
# Кэш интерпретатора
__pycache__/
*.py[cod]
*$py.class
# Пакеты и сборки
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.eg
*.egg
*.eggs
# Poetry
poetry.lock
.pypoetry/
### Логи и БД ###
*.log
*.logs
*.log.*
*.logs.*
log/
logs/
*.sqlite
*.db
### IDE ###
.idea/
.vscode/
*.swp
*.sublime-*
### OS ###
.DS_Store
Thumbs.db
### Тестирование ###
.coverage
htmlcov/
.tox/
.nox/
.pytest_cache/
.mypy_cache/
test/
tests/
Test/
Tests/
count.py

10
.idea/PrimoAranarBot.iml generated Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.13 (PrimoAranarBot)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/YashaSystemBot.iml" filepath="$PROJECT_DIR$/.idea/YashaSystemBot.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

258
.idea/workspace.xml generated Normal file
View File

@@ -0,0 +1,258 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="2643be13-7905-4315-bb25-9c65e899084f" name="Changes" comment="">
<change afterPath="$PROJECT_DIR$/.dockerignore" afterDir="false" />
<change afterPath="$PROJECT_DIR$/.env_example" afterDir="false" />
<change afterPath="$PROJECT_DIR$/.gitattributes" afterDir="false" />
<change afterPath="$PROJECT_DIR$/.gitignore" afterDir="false" />
<change afterPath="$PROJECT_DIR$/.idea/PrimoAranarBot.iml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/.idea/inspectionProfiles/profiles_settings.xml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/.idea/modules.xml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/.idea/vcs.xml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/Dockerfile" afterDir="false" />
<change afterPath="$PROJECT_DIR$/LICENSE" afterDir="false" />
<change afterPath="$PROJECT_DIR$/README.md" afterDir="false" />
<change afterPath="$PROJECT_DIR$/assets/default.jpg" afterDir="false" />
<change afterPath="$PROJECT_DIR$/assets/start.jpg" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bot/__init__.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bot/core/__init__.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bot/core/bots.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bot/core/webhook.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bot/filters/__init__.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bot/filters/callback.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bot/filters/chat_rights.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bot/filters/chat_type.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bot/filters/message_content.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bot/filters/subscrided.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bot/handlers/__init__.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bot/handlers/commands/__init__.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bot/handlers/commands/admins/__init__.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bot/handlers/commands/admins/settings_cmd.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bot/handlers/commands/users/__init__.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bot/handlers/commands/users/active.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bot/handlers/commands/users/anketa_cmd.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bot/handlers/commands/users/new_cmd.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bot/handlers/commands/users/start_cmd.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bot/handlers/messages/__init__.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bot/handlers/messages/default.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bot/handlers/messages/reply_msg.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bot/handlers/secret/__init__.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bot/handlers/secret/secret1.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bot/handlers/secret/secret2.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bot/keyboards/__init__.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bot/keyboards/inline/__init__.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bot/keyboards/inline/decision.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bot/keyboards/reply/__init__.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bot/middlewares/__init__.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bot/middlewares/error_mdw.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bot/middlewares/logging_mdw.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bot/middlewares/msg_mdw.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bot/middlewares/spam_mdw.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bot/middlewares/subscription_mdw.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bot/middlewares/time_mdw.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bot/states/__init__.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bot/states/anketa_states.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bot/states/new_states.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bot/templates/__init__.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bot/templates/message_callback.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bot/utils/__init__.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bot/utils/argument.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bot/utils/interesting_facts.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bot/utils/pagination.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bot/utils/random_lists.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bot/utils/type_message.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bot/utils/usernames.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/configs/__init__.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/configs/cmd_list.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/configs/config.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/configs/roles.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/database/__init__.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/database/database.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/locales/en/LC_MESSAGES/bot.mo" afterDir="false" />
<change afterPath="$PROJECT_DIR$/locales/en/LC_MESSAGES/bot.po" afterDir="false" />
<change afterPath="$PROJECT_DIR$/locales/messages.pot" afterDir="false" />
<change afterPath="$PROJECT_DIR$/locales/ru/LC_MESSAGES/bot.mo" afterDir="false" />
<change afterPath="$PROJECT_DIR$/locales/ru/LC_MESSAGES/bot.po" afterDir="false" />
<change afterPath="$PROJECT_DIR$/locales/uk/LC_MESSAGES/bot.mo" afterDir="false" />
<change afterPath="$PROJECT_DIR$/locales/uk/LC_MESSAGES/bot.po" afterDir="false" />
<change afterPath="$PROJECT_DIR$/main.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/middleware/__init__.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/middleware/loggers/__init__.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/middleware/loggers/logs.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/middleware/validators/__init__.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/middleware/validators/email_vld.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/middleware/validators/url_vld.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/pyproject.toml" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="FileTemplateManagerImpl">
<option name="RECENT_TEMPLATES">
<list>
<option value="Python Script" />
</list>
</option>
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="KubernetesApiPersistence">{}</component>
<component name="KubernetesApiProvider">{
&quot;isMigrated&quot;: true
}</component>
<component name="ProjectColorInfo">{
&quot;associatedIndex&quot;: 6
}</component>
<component name="ProjectId" id="30zwIGAUUSITtvba70bSTcC47qk" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;ModuleVcsDetector.initialDetectionPerformed&quot;: &quot;true&quot;,
&quot;Python.main.executor&quot;: &quot;Run&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager&quot;: &quot;true&quot;,
&quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,
&quot;git-widget-placeholder&quot;: &quot;master&quot;,
&quot;last_opened_file_path&quot;: &quot;C:/Users/admin/Documents/Projects/Python/PrimoAranarBot&quot;,
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
&quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
&quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
&quot;settings.editor.selected.configurable&quot;: &quot;com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable&quot;,
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
}
}</component>
<component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS">
<recent name="C:\Users\admin\Documents\Projects\Python\PrimoAranarBot" />
<recent name="C:\Users\admin\Documents\Projects\Python\YashaSystemBot" />
<recent name="C:\Users\admin\Documents\Projects\Python\YashaSystemBot\middleware" />
<recent name="C:\Users\admin\Documents\Projects\Python\YashaSystemBot\locales\uk\LC_MESSAGES" />
<recent name="C:\Users\admin\Documents\Projects\Python\YashaSystemBot\locales\ru\LC_MESSAGES" />
</key>
<key name="MoveFile.RECENT_KEYS">
<recent name="C:\Users\admin\Documents\Projects\Python\PrimoAranarBot\bot\handlers\secret2" />
<recent name="C:\Users\admin\Documents\Projects\Python\PrimoAranarBot\bot\handlers" />
<recent name="C:\Users\admin\Documents\Projects\Python\YashaSystemBot" />
<recent name="C:\Users\admin\Documents\Projects\Python\YashaSystemBot\bot\core" />
<recent name="C:\Users\admin\Documents\Projects\Python\YashaSystemBot\middleware\loggers" />
</key>
</component>
<component name="RunManager">
<configuration name="main" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
<module name="PrimoAranarBot" />
<option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="SDK_NAME" value="Python 3.13 (PrimoAranarBot)" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="IS_MODULE_SDK" value="false" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/main.py" />
<option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
<recent_temporary>
<list>
<item itemvalue="Python.main" />
</list>
</recent_temporary>
</component>
<component name="SharedIndexes">
<attachedChunks>
<set>
<option value="bundled-js-predefined-d6986cc7102b-09060db00ec0-JavaScript-PY-251.26927.90" />
<option value="bundled-python-sdk-41e8cd69c857-64d779b69b7a-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-251.26927.90" />
</set>
</attachedChunks>
</component>
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="2643be13-7905-4315-bb25-9c65e899084f" name="Changes" comment="" />
<created>1754643627985</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1754643627985</updated>
<workItem from="1754643629040" duration="10355000" />
<workItem from="1754662667867" duration="7333000" />
<workItem from="1754744202078" duration="9537000" />
<workItem from="1754831073792" duration="2983000" />
<workItem from="1754834942318" duration="73000" />
<workItem from="1755110743443" duration="663000" />
<workItem from="1755117853241" duration="600000" />
<workItem from="1755158975813" duration="2965000" />
<workItem from="1755338132928" duration="757000" />
<workItem from="1756535467015" duration="566000" />
<workItem from="1756536041631" duration="326000" />
<workItem from="1756536379874" duration="386000" />
<workItem from="1756536789625" duration="4088000" />
<workItem from="1756649477917" duration="228000" />
<workItem from="1756649731073" duration="9987000" />
<workItem from="1757043734454" duration="3772000" />
<workItem from="1757061298817" duration="13000" />
<workItem from="1757131608578" duration="1256000" />
<workItem from="1757133500304" duration="12844000" />
<workItem from="1757256800838" duration="196000" />
<workItem from="1757257115686" duration="2310000" />
<workItem from="1757259792275" duration="5293000" />
</task>
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
<component name="XDebuggerManager">
<breakpoint-manager>
<breakpoints>
<line-breakpoint enabled="true" suspend="THREAD" type="python-line">
<url>file://$PROJECT_DIR$/bot/handlers/__init__.py</url>
<option name="timeStamp" value="1" />
</line-breakpoint>
<line-breakpoint enabled="true" suspend="THREAD" type="python-line">
<url>file://$PROJECT_DIR$/bot/handlers/secret/secret1.py</url>
<option name="timeStamp" value="3" />
</line-breakpoint>
<line-breakpoint enabled="true" suspend="THREAD" type="python-line">
<url>file://$PROJECT_DIR$/bot/handlers/messages/__init__.py</url>
<option name="timeStamp" value="4" />
</line-breakpoint>
<line-breakpoint enabled="true" suspend="THREAD" type="python-line">
<url>file://$PROJECT_DIR$/bot/handlers/messages/default.py</url>
<line>59</line>
<option name="timeStamp" value="9" />
</line-breakpoint>
<line-breakpoint enabled="true" suspend="THREAD" type="python-line">
<url>file://$PROJECT_DIR$/bot/core/__init__.py</url>
<option name="timeStamp" value="10" />
</line-breakpoint>
</breakpoints>
</breakpoint-manager>
</component>
<component name="com.intellij.coverage.CoverageDataManagerImpl">
<SUITE FILE_PATH="coverage/YashaSystemBot$main.coverage" NAME="main Coverage Results" MODIFIED="1754831895813" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
<SUITE FILE_PATH="coverage/PrimoAranarBot$main.coverage" NAME="main Coverage Results" MODIFIED="1755353593880" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
</component>
</project>

26
Dockerfile Normal file
View File

@@ -0,0 +1,26 @@
# Используем официальный образ Python с подходящей версией
FROM python:3.12-slim
# Устанавливаем Poetry
RUN pip install poetry
# Устанавливаем рабочую директорию внутри контейнера
WORKDIR /app
# Копируем файлы Poetry
COPY pyproject.toml poetry.lock* ./
# Настраиваем Poetry (не создавать виртуальное окружение внутри контейнера)
RUN poetry config virtualenvs.create false
# Устанавливаем зависимости через Poetry
RUN poetry install --no-interaction --no-ansi --no-root
# Копируем все файлы проекта внутрь контейнера
COPY . .
# Устанавливаем переменную окружения для буферизации
ENV PYTHONUNBUFFERED=1
# Команда запуска — запуск скрипта main.py
CMD ["python", "main.py"]

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) [2025] [Verum]
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

BIN
README.md Normal file

Binary file not shown.

BIN
assets/default.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 554 KiB

BIN
assets/start.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

3
bot/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
from .core import *
from .handlers import *
from .middlewares import *

2
bot/core/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
from .bots import *
from .webhook import *

203
bot/core/bots.py Normal file
View File

@@ -0,0 +1,203 @@
from datetime import datetime
from asyncio import sleep
from aiogram import Bot, Dispatcher
from aiogram.client.default import DefaultBotProperties
from aiogram.exceptions import TelegramRetryAfter
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.types import (
User,
ChatAdministratorRights,
BotDescription,
BotShortDescription,
)
from aiogram.utils.i18n import I18n, SimpleI18nMiddleware
from configs.config import BotSettings, BotEdit, Webhook, Permission
from middleware.loggers import log
__all__ = ("dp", "bot", "BotInfo", "i18n")
# FSM-хранилище и диспетчер
storage: MemoryStorage = MemoryStorage()
dp: Dispatcher = Dispatcher(storage=storage)
dp["is_active"]: bool = True
# Локализация
i18n: I18n = I18n(path="locales", default_locale="ru", domain="bot")
i18n_middleware: SimpleI18nMiddleware = SimpleI18nMiddleware(i18n=i18n)
i18n_middleware.setup(dp)
# Экземпляр бота
bot: Bot = Bot(
token=BotSettings.BOT_TOKEN,
default=DefaultBotProperties(
parse_mode=BotSettings.PARSE_MODE,
disable_notification=BotSettings.DISABLE_NOTIFICATION,
protect_content=BotSettings.PROTECT_CONTENT,
allow_sending_without_reply=BotSettings.ALLOW_SENDING_WITHOUT_REPLY,
link_preview_is_disabled=BotSettings.LINK_PREVIEW_IS_DISABLED,
link_preview_prefer_small_media=BotSettings.LINK_PREVIEW_PREFER_SMALL_MEDIA,
link_preview_prefer_large_media=BotSettings.LINK_PREVIEW_PREFER_LARGE_MEDIA,
link_preview_show_above_text=BotSettings.LINK_PREVIEW_SHOW_ABOVE_TEXT,
show_caption_above_media=BotSettings.SHOW_CAPTION_ABOVE_MEDIA,
),
)
class BotInfo:
"""Класс для хранения и настройки информации о боте."""
id: int | None = None
url: str | None = None
first_name: str | None = None
last_name: str | None = None
username: str | None = None
description: str | None = None
short_description: str | None = None
language_code: str = BotSettings.BOT_LANGUAGE
prefix: str = BotSettings.PREFIX
bot_owner: str = BotSettings.OWNER
added_to_attachment_menu: bool = False
supports_inline_queries: bool = False
can_connect_to_business: bool = False
has_main_web_app: bool = False
can_join_groups: bool = False
can_read_all_group_messages: bool = False
@classmethod
@log(level="INFO", log_type="BOT", text="Настройка вебхука бота")
async def webhook(
cls, bots: Bot = bot, webhook_url: str = Webhook.WEBHOOK_URL, use_webhook: bool = Webhook.WEBHOOK
) -> None:
"""
Удаление или установка вебхука.
"""
await bots.delete_webhook(drop_pending_updates=True)
if use_webhook:
if webhook_url is None:
raise ValueError("Для установки вебхука необходимо указать webhook_url")
try:
await bots.set_webhook(Webhook.WEBHOOK_URL)
except TelegramRetryAfter as e:
print(f"Flood control: повтор через {e.retry_after} секунд")
await sleep(e.retry_after)
await bots.set_webhook(Webhook.WEBHOOK_URL)
@classmethod
@log(level="INFO", log_type="BOT", text="Получение информации о боте")
async def info(cls, bots: Bot = bot) -> dict[str, object]:
"""
Получает и сохраняет информацию о боте.
"""
bot_info: User = await bots.get_me()
cls.id = bot_info.id
cls.url = f"tg://user?id={cls.id}"
cls.first_name = bot_info.first_name
cls.last_name = bot_info.last_name
cls.username = bot_info.username
cls.language_code = bot_info.language_code
cls.is_premium = getattr(bot_info, "is_premium", False)
cls.added_to_attachment_menu = bot_info.added_to_attachment_menu
cls.supports_inline_queries = bot_info.supports_inline_queries
cls.can_connect_to_business = bot_info.can_connect_to_business
cls.has_main_web_app = bot_info.has_main_web_app
cls.can_join_groups = getattr(bot_info, "can_join_groups", False)
cls.can_read_all_group_messages = getattr(bot_info, "can_read_all_group_messages", False)
return {
"id": cls.id,
"url": cls.url,
"first_name": cls.first_name,
"last_name": cls.last_name,
"username": cls.username,
"language_code": cls.language_code,
"prefix": cls.prefix,
"bot_owner": cls.bot_owner,
}
@staticmethod
@log(level="INFO", log_type="BOT", text="Установка прав администратора")
async def set_administrator_rights(
bots: Bot = bot, rights: ChatAdministratorRights = BotEdit.RIGHTS
) -> None:
current_rights: ChatAdministratorRights = await bots.get_my_default_administrator_rights()
if current_rights != rights:
await bots.set_my_default_administrator_rights(rights)
@staticmethod
@log(level="INFO", log_type="BOT", text="Обновление имени бота")
async def set_name(bots: Bot = bot, new_name: str = BotEdit.NAME) -> None:
current_name: str = (await bots.get_me()).first_name
if not (1 <= len(new_name) <= 32):
raise ValueError("Имя бота должно быть от 1 до 32 символов.")
if current_name != new_name:
await bots.set_my_name(new_name)
@staticmethod
@log(level="INFO", log_type="BOT", text="Обновление описания бота")
async def set_description(bots: Bot = bot, new_description: str = BotEdit.DESCRIPTION) -> None:
current_description: BotDescription = await bots.get_my_description()
if not (0 < len(new_description) <= 255):
raise ValueError("Описание должно быть от 1 до 255 символов.")
if current_description.description != new_description:
await bots.set_my_description(description=new_description)
@staticmethod
@log(level="INFO", log_type="BOT", text="Обновление короткого описания бота")
async def set_short_description(bots: Bot = bot, new_short: str = BotEdit.SHORT_DESCRIPTION) -> None:
current_short: BotShortDescription = await bots.get_my_short_description()
if not (0 < len(new_short) <= 512):
raise ValueError("Короткое описание должно быть от 1 до 512 символов.")
if current_short.short_description != new_short:
await bots.set_my_short_description(short_description=new_short)
@staticmethod
def start_info_out(out: bool = True) -> str:
bot_time: str = f"Бот @{BotInfo.username} запущен в {datetime.now().strftime("%S:%M:%H %d-%m-%Y")}\n"
bot_name: str = f"Основное имя: {BotInfo.first_name}\n"
bot_postname: str = f" Доп. имя: {BotInfo.last_name}\n"
bot_username: str = f" Юзернейм: @{BotInfo.username}\n"
bot_id: str = f" ID: {BotInfo.id}\n"
bot_can_join_groups: str = f" Может ли вступать в группы: {BotInfo.can_join_groups}\n"
bot_can_read_all_group_messages: str = f" Чтение всех сообщений: {BotInfo.can_read_all_group_messages}\n"
bot_added_to_attachment_menu: str = f" Добавлен в меню вложений: {BotInfo.added_to_attachment_menu}\n"
bot_supports_inline_queries: str = f" Поддерживает инлайн-запросы: {BotInfo.supports_inline_queries}\n"
bot_can_connect_to_business: str = f" Подключение к бизнес-аккаунтам: {BotInfo.can_connect_to_business}\n"
bot_has_main_web_app: str = f" Основное веб-приложение: {BotInfo.has_main_web_app}\n"
# Формируем полный текст с выводом информации о боте
bot_all_info: str = (f"{bot_name} {bot_postname} {bot_username} {bot_id} "
f"{bot_can_join_groups} {bot_can_read_all_group_messages} "
f"{bot_added_to_attachment_menu} {bot_supports_inline_queries} {bot_can_connect_to_business} "
f"{bot_has_main_web_app}")
if out:
print(f"\033[34m{bot_all_info}\033[0m")
# Записываем информацию в файл
try:
with open("Logs/info.log", 'w', encoding='utf-8') as log_file:
log_file.write(f"{bot_time}{bot_all_info}")
# Создание файла bot_start.log
with open("Logs/bot_start.log", 'a', encoding='utf-8') as log_start_file:
log_start_file.write(f"{bot_time}\n")
return bot_all_info
# Проверка на ошибку и ее логирование
except Exception as e:
raise f"Ошибка при получении ID пользователя: {e}"
@classmethod
@log(level="INFO", log_type="START", text="Процесс запуска бота!")
async def setup(cls, bots: Bot = bot, perm: bool = Permission.BOT_EDIT):
await cls.webhook(bots=bots)
await cls.info(bots=bots)
if perm:
await cls.set_administrator_rights(bots=bots)
await cls.set_description(bots=bots)
await cls.set_short_description(bots=bots)
await cls.set_name(bots=bots)

44
bot/core/webhook.py Normal file
View File

@@ -0,0 +1,44 @@
from typing import Any
from aiohttp import web
from aiogram.types import Update
from .bots import dp, bot
from middleware import loggers
class WebhookApp:
"""Приложение aiohttp для обработки webhook-запросов."""
def __init__(self, host: str = "0.0.0.0", port: int = 8080) -> None:
self.host = host
self.port = port
self.app: web.Application = web.Application()
self.app.router.add_post("/webhook", self.handle_update)
self.runner: web.AppRunner | None = None
self.site: web.TCPSite | None = None
@staticmethod
async def handle_update(request: web.Request) -> web.Response:
"""Обработчик входящих запросов от Telegram."""
try:
update_json: dict[str, Any] = await request.json()
update: Update = Update.model_validate(update_json)
await dp.feed_update(bot=bot, update=update)
except Exception as e:
print(f"Ошибка обработки webhook-запроса: {e}")
return web.Response(status=500)
return web.Response(status=200)
async def start(self) -> None:
"""Асинхронный запуск aiohttp-приложения."""
self.runner = web.AppRunner(self.app)
await self.runner.setup()
self.site = web.TCPSite(self.runner, self.host, self.port)
await self.site.start()
loggers.info(f"🌍 Webhook сервер запущен на http://{self.host}:{self.port}")
async def stop(self) -> None:
"""Остановка aiohttp-приложения."""
if self.runner:
await self.runner.cleanup()

5
bot/filters/__init__.py Normal file
View File

@@ -0,0 +1,5 @@
from .callback import *
from .chat_rights import *
from .chat_type import *
from .message_content import *
from .subscrided import *

21
bot/filters/callback.py Normal file
View File

@@ -0,0 +1,21 @@
from aiogram.filters import BaseFilter
from aiogram.types import CallbackQuery
# Настройка экспорта
__all__ = ("CallbackDataStartsWith",)
class CallbackDataStartsWith(BaseFilter):
"""
Фильтр для callback_data, начинающихся с префикса.
Example:
@router.callback_query(CallbackDataStartsWith("menu:"))
async def handler(cb: CallbackQuery):
await cb.answer("Это callback из меню ✅")
"""
def __init__(self, prefix: str) -> None:
self.prefix = prefix
async def __call__(self, callback: CallbackQuery) -> bool:
return bool(callback.data and callback.data.startswith(self.prefix))

View File

@@ -0,0 +1,73 @@
from aiogram import Bot
from aiogram.filters import BaseFilter
from aiogram.types import Message, ResultChatMemberUnion
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError
# Настройка экспорта
__all__ = ("IsChatCreator", "IsAdmin", "IsModerator",)
class IsChatCreator(BaseFilter):
"""
Пользователь является создателем чата.
Example:
@router.message(IsChatCreator())
async def handler(msg: Message):
await msg.answer("Ты создатель этого чата 👑")
"""
async def __call__(self, message: Message, bot: Bot) -> bool:
try:
member: ResultChatMemberUnion = await bot.get_chat_member(message.chat.id, message.from_user.id)
return member.status == "creator"
except (TelegramBadRequest, TelegramForbiddenError):
return False
class IsAdmin(BaseFilter):
"""
Пользователь является администратором (или создателем).
Example:
@router.message(IsAdmin())
async def handler(msg: Message):
await msg.answer("Ты админ ✅")
"""
async def __call__(self, message: Message, bot: Bot) -> bool:
try:
member: ResultChatMemberUnion = await bot.get_chat_member(message.chat.id, message.from_user.id)
return member.status in {"administrator", "creator"}
except (TelegramBadRequest, TelegramForbiddenError):
return False
class IsModerator(BaseFilter):
"""
Администратор с модераторскими правами:
- удаление сообщений
- ограничение пользователей
- закрепление сообщений
Example:
@router.message(IsModerator())
async def handler(msg: Message):
await msg.answer("Ты модератор ✅")
"""
async def __call__(self, message: Message, bot: Bot) -> bool:
try:
member: ResultChatMemberUnion = await bot.get_chat_member(message.chat.id, message.from_user.id)
if member.status == "creator":
return True
if member.status != "administrator":
return False
required_rights: list[bool] = [
getattr(member, "can_delete_messages", False),
getattr(member, "can_restrict_members", False),
getattr(member, "can_pin_messages", False),
]
return all(required_rights)
except (TelegramBadRequest, TelegramForbiddenError):
return False

31
bot/filters/chat_type.py Normal file
View File

@@ -0,0 +1,31 @@
from aiogram.filters import BaseFilter
from aiogram.types import Message
# Настройка экспорта
__all__ = ("IsPrivate", "IsGroup",)
class IsPrivate(BaseFilter):
"""
Сообщение в личке с ботом.
Example:
@router.message(IsPrivate())
async def handler(msg: Message):
await msg.answer("Это ЛС ✅")
"""
async def __call__(self, message: Message) -> bool:
return message.chat.type == "private"
class IsGroup(BaseFilter):
"""
Сообщение в группе или супергруппе.
Example:
@router.message(IsGroup())
async def handler(msg: Message):
await msg.answer("Это сообщение в группе ✅")
"""
async def __call__(self, message: Message) -> bool:
return message.chat.type in {"group", "supergroup"}

View File

@@ -0,0 +1,67 @@
from aiogram.filters import BaseFilter
from aiogram.types import Message
# Настройка экспорта
__all__ = ("IsReply", "IsForwarded", "HasMedia", "ContainsURL",)
class IsReply(BaseFilter):
"""
Сообщение является ответом.
Example:
@router.message(IsReply())
async def handler(msg: Message):
await msg.answer("Это реплай ✅")
"""
async def __call__(self, message: Message) -> bool:
return message.reply_to_message is not None
class IsForwarded(BaseFilter):
"""
Сообщение переслано из другого чата/от пользователя.
Example:
@router.message(IsForwarded())
async def handler(msg: Message):
await msg.answer("Это пересланное сообщение 🔄")
"""
async def __call__(self, message: Message) -> bool:
return (message.forward_from is not None) or (message.forward_from_chat is not None)
class HasMedia(BaseFilter):
"""
Сообщение содержит медиа (фото, видео, документ и т.д.).
Example:
@router.message(HasMedia())
async def handler(msg: Message):
await msg.answer("Это медиа ✅")
"""
async def __call__(self, message: Message) -> bool:
return any([
message.photo,
message.video,
message.document,
message.audio,
message.voice,
message.video_note,
message.sticker,
])
class ContainsURL(BaseFilter):
"""
Сообщение содержит ссылку (http/https).
Example:
@router.message(ContainsURL())
async def handler(msg: Message):
await msg.answer("Это сообщение с ссылкой 🔗")
"""
async def __call__(self, message: Message) -> bool:
if not message.text:
return False
return "http://" in message.text or "https://" in message.text

39
bot/filters/subscrided.py Normal file
View File

@@ -0,0 +1,39 @@
from aiogram.types import Message, ResultChatMemberUnion
from aiogram.filters import BaseFilter
from aiogram import Bot
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError
from typing import Union
# Настройки экспорта
__all__ = ("FilterSubscribed",)
class FilterSubscribed(BaseFilter):
"""
Фильтр для проверки подписки пользователя на один или несколько каналов.
Поддерживает как публичные каналы (username), так и приватные (ID).
Пример:
# Проверка сразу двух каналов: публичный по username и приватный по ID
@router.message(FilterSubscribed(["@public_channel", -1001234567890]))
async def only_subscribed(message: Message):
await message.answer("Ты подписан и на публичный, и на приватный канал ✅")
"""
def __init__(self, channels: list[Union[str, int]]) -> None:
self.channels = channels
async def __call__(self, message: Message, bot: Bot) -> bool:
for channel in self.channels:
try:
member: ResultChatMemberUnion = await bot.get_chat_member(
chat_id=channel,
user_id=message.from_user.id
)
if member.status in ("left", "kicked"):
return False
except (TelegramBadRequest, TelegramForbiddenError):
# Канал недоступен, либо у бота нет прав
return False
return True

15
bot/handlers/__init__.py Normal file
View File

@@ -0,0 +1,15 @@
from aiogram import Router
#from .commands import router as cmd_routers
from .messages import router as messages_routers
from .secret import router as secret_routers
# Настройка экспорта и роутера
__all__ = ("router",)
router: Router = Router(name=__name__)
# Подключение роутеров
router.include_routers(
#cmd_routers,
secret_routers,
messages_routers,
)

View File

@@ -0,0 +1,13 @@
from aiogram import Router
from .admins import router as admin_cmd_router
from .users import router as users_cmd_router
# Настройка экспорта и роутера
__all__ = ("router",)
router: Router = Router(name=__name__)
# Подключение роутеров
router.include_routers(
admin_cmd_router,
users_cmd_router,
)

View File

@@ -0,0 +1,11 @@
from aiogram import Router
from .settings_cmd import router as settings_cmd_router
# Настройка экспорта и роутера
__all__ = ("router",)
router: Router = Router(name=__name__)
# Подключение роутеров
router.include_routers(
settings_cmd_router,
)

View File

@@ -0,0 +1,51 @@
from aiogram import Router, F
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.utils.i18n import gettext as _
from bot.templates import msg_photo
from bot.utils.interesting_facts import interesting_fact
from bot.core.bots import BotInfo
from configs import COMMANDS, RpValue
# Настройки экспорта и роутера
__all__ = ("router",)
CMD: str = "settings".lower()
router: Router = Router(name=f"{CMD}_cmd_router")
@router.callback_query(F.data.lower() == CMD)
@router.message(Command(*COMMANDS[CMD], prefix=BotInfo.prefix, ignore_case=True))
async def start_cmd(message: Message | CallbackQuery, state: FSMContext) -> None:
"""Обработчик команды /start"""
await state.clear()
# Создание инлайн-клавиатуры
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
ikb.row(InlineKeyboardButton(text="Инфо-канал🗂", url=RpValue.INFO_URL))
ikb.row(InlineKeyboardButton(text="Вступление🚀", callback_data='new'),
InlineKeyboardButton(text="Анкета📖", callback_data='anketa'))
ikb.row(InlineKeyboardButton(text="Связь с администрацией🌐", callback_data='admin'))
# Формируем приветственное сообщение
text: str = _(
"""Добро пожаловать, <a href="{url}">{name}</a>!
Я ваш искусственный помощник по ролевой - <b>{rp_name}</b>!
Моя цель — помочь вам сориентироваться и сделать ваше вступление куда проще!
Надеюсь, я смогу вам помочь! Пожалуйста, выберите нужную функцию на клавиатуре!
Интересный факт:
<blockquote>{fact}</blockquote>
"""
).format(
url=message.from_user.url if message.from_user else "",
name=message.from_user.first_name if message.from_user else "пользователь",
rp_name=RpValue.RP_NAME,
fact=interesting_fact(),
)
# Отправляем сообщение
await msg_photo(message=message, text=text, file=f'assets/{CMD}.jpg', markup=ikb)

View File

@@ -0,0 +1,13 @@
from aiogram import Router
from .start_cmd import router as start_cmd_router
from .active import router as active_cmd_router
# Настройка экспорта и роутера
__all__ = ("router",)
router: Router = Router(name=__name__)
# Подключение роутеров
router.include_routers(
start_cmd_router,
active_cmd_router,
)

View File

@@ -0,0 +1,45 @@
from aiogram import Router, F
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.types import Message, CallbackQuery
from bot.templates import msg_photo
from bot.core.bots import BotInfo
from configs import COMMANDS
from database import db
# Настройки экспорта и роутера
__all__ = ("router",)
CMD: str = "active".lower()
router: Router = Router(name=f"{CMD}_cmd_router")
@router.callback_query(F.data.lower() == CMD)
@router.message(Command(*COMMANDS[CMD], prefix=BotInfo.prefix, ignore_case=True))
async def active_cmd(message: Message | CallbackQuery, state: FSMContext) -> None:
"""Обработчик команды /active"""
await state.clear()
# Получить статистику сообщений пользователя
day, week, month, total = await db.get_message_stats(message.from_user.id)
print(f"За день: {day} сообщений")
print(f"За неделю: {week} сообщений")
print(f"За месяц: {month} сообщений")
print(f"Всего: {total} сообщений")
# Формируем приветственное сообщение
text: str =f"""
За день: {day} сообщений
За неделю: {week} сообщений
За месяц: {month} сообщений
Всего: {total} сообщений
"""
# Отправляем сообщение
await msg_photo(message=message, text=text,)

View File

@@ -0,0 +1,218 @@
import re
from typing import Optional, Dict, Tuple
from aiogram import Router, F
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.utils.i18n import gettext as _
from bot.core.bots import BotInfo
from bot.keyboards.inline.decision import decision_keyboard
from bot.states.new_states import NewStates
from bot.templates import msg
from middleware.loggers import log
from configs import COMMANDS, ImportantID, RpValue
# Глобальная мапа для хранения связей пользователь-топик
user_topic_map: Dict[Tuple[int, str], int] = {}
__all__ = ("router",)
CMD: str = "new"
router: Router = Router(name=f"{CMD}_cmd_router")
TOPIC_TYPE: str = "anketa"
TEXTS: Dict[str, Dict[str, str]] = {
"anketa": {
"accept": f"<b>🎉 Ваша анкета принята!</b>\n\nДобро пожаловать в проект!\n\nФлуд: {RpValue.FLUD_URL}\nРолевая: {RpValue.RP_URL}",
"reject": "<b>❌ Ваша анкета отклонена.</b>\n\nВы можете попробовать позже."
}
}
async def validate_russian_text(text: str) -> bool:
"""Проверяет текст на соответствие русским буквам, пробелам и дефисам."""
return bool(re.fullmatch(r"[А-Яа-яЁё\s\-]+", text))
# ===================== Команда /new =====================
@router.callback_query(F.data == CMD)
@router.message(Command(*COMMANDS[CMD], prefix=BotInfo.prefix, ignore_case=True))
@log(level='INFO', log_type=CMD.upper(), text=f"использовал команду /{CMD}")
async def new_cmd(message: Message | CallbackQuery, state: FSMContext) -> None:
"""
Начало анкеты /new.
Отправляет пользователю сообщение с просьбой указать желаемую роль.
"""
await state.clear()
await state.set_state(NewStates.role)
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
ikb.row(InlineKeyboardButton(text="Отмена↩️", callback_data='start'))
text: str = _(
"Пожалуйста, отправьте желаемую роль:\n"
"(только русские буквы, пробелы или дефисы)"
)
await msg(message=message, text=text, markup=ikb)
# ===================== Обработка роли =====================
@router.message(NewStates.role)
async def process_role(message: Message, state: FSMContext) -> None:
"""Обрабатывает ввод роли и запрашивает сортол."""
if not await validate_russian_text(message.text):
await message.reply("Ошибка: роль должна содержать только русские буквы, пробелы или дефисы.")
return
await state.update_data(role=message.text.strip().title())
await state.set_state(NewStates.sorol)
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
ikb.row(InlineKeyboardButton(text="Отмена↩️", callback_data='start'))
await message.reply(
text="Теперь укажите желаемый сортол:\n(только русские буквы, пробелы или дефисы)",
reply_markup=ikb.as_markup()
)
# ===================== Обработка сортола =====================
@router.message(NewStates.sorol)
async def process_sortol(message: Message, state: FSMContext) -> None:
"""Обрабатывает ввод сортола и запрашивает кодовую фразу."""
if not await validate_russian_text(message.text):
await message.reply("Ошибка: сорол должен содержать только русские буквы, пробелы или дефисы.")
return
await state.update_data(sortol=message.text.strip().title())
await state.set_state(NewStates.code_phrase)
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
ikb.row(InlineKeyboardButton(text="Отмена↩️", callback_data='start'))
await message.reply(
text="Теперь введите кодовую фразу из правил:",
reply_markup=ikb.as_markup()
)
# ===================== Обработка кодовой фразы =====================
@router.message(NewStates.code_phrase)
async def process_code_phrase(message: Message, state: FSMContext) -> None:
"""Обрабатывает ввод кодовой фразы и показывает предпросмотр анкеты."""
code_phrase = message.text.strip()
if not code_phrase:
await message.reply("Кодовая фраза не может быть пустой.")
return
await state.update_data(code_phrase=code_phrase)
data: Dict[str, str] = await state.get_data()
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
ikb.row(
InlineKeyboardButton(text="Отправить!", callback_data="submit_new"),
InlineKeyboardButton(text="Отмена↩️", callback_data="start")
)
text: str = (
f"<b>Проверьте данные анкеты:</b>\n\n"
f"• Роль: {data['role']}\n"
f"• Сортол: {data['sortol']}\n"
f"• Кодовая фраза: {data['code_phrase']}"
)
await message.reply(text, reply_markup=ikb.as_markup())
# ===================== Отправка анкеты в поддержку =====================
@router.callback_query(F.data == "submit_new")
async def submit_new_cmd(callback: CallbackQuery, state: FSMContext) -> None:
"""Отправляет анкету в топик форума поддержки и создает запись в мапе."""
data: Dict[str, str] = await state.get_data()
user = callback.from_user
# Создаем топик в форуме
topic = await callback.bot.create_forum_topic(
chat_id=ImportantID.SUPPORT_CHAT_ID,
name=f"Анкета от {user.full_name}"
)
thread_id: int = topic.message_thread_id
# Сохраняем связь пользователь-топик
user_topic_map[(user.id, TOPIC_TYPE)] = thread_id
# Формируем текст анкеты
text: str = (
f'<b><a href="tg://user?id={user.id}">Анкета</a></b>\n\n'
f"• Роль: {data['role']}\n"
f"• Сортол: {data['sortol']}\n"
f"• Кодовая фраза: {data['code_phrase']}"
)
# Отправляем в топик с кнопками принятия/отклонения
await callback.bot.send_message(
chat_id=ImportantID.SUPPORT_CHAT_ID,
message_thread_id=thread_id,
text=text,
parse_mode="HTML",
reply_markup=decision_keyboard(thread_id=thread_id, kind=TOPIC_TYPE)
)
await callback.message.edit_text("✅ Ваша анкета успешно отправлена на рассмотрение!")
await state.clear()
# ===================== Обработка решения админов =====================
@router.callback_query(F.data.regexp(r"^([a-z_]+):(accept|reject):(\d+)$"))
async def process_decision_callback(callback: CallbackQuery) -> None:
"""Обрабатывает решение администраторов и отправляет результат пользователю."""
kind, action, thread_id_str = callback.data.split(":")
thread_id = int(thread_id_str)
# Ищем пользователя по thread_id в мапе
user_id = None
for (uid, k), tid in user_topic_map.items():
if k == kind and tid == thread_id:
user_id = uid
break
if not user_id:
await callback.answer("Пользователь не найден.", show_alert=True)
return
text_to_send: Optional[str] = TEXTS.get(kind, {}).get(action)
if not text_to_send:
await callback.answer("Некорректные данные.", show_alert=True)
return
await callback.bot.send_message(chat_id=user_id, text=text_to_send, parse_mode="HTML")
await callback.message.edit_reply_markup(reply_markup=None)
await callback.answer("Ответ отправлен пользователю.")
# ===================== Пересылка ответов админов пользователю =====================
@router.message(F.is_topic_message, F.reply_to_message, ~F.from_user.is_bot)
async def forward_reply_to_user(message: Message) -> None:
"""Пересылает ответы администраторов из топика пользователю."""
thread_id = message.message_thread_id
if not thread_id:
return
# Ищем пользователя по thread_id
user_id = None
for (uid, _), tid in user_topic_map.items():
if tid == thread_id:
user_id = uid
break
if not user_id:
return
reply_text: str = f"<b>Ответ администратора:</b>\n{message.html_text}"
try:
await message.bot.send_message(chat_id=user_id, text=reply_text, parse_mode="HTML")
except Exception as e:
await message.reply(f"⚠️ Не удалось отправить сообщение пользователю: {e}")

View File

@@ -0,0 +1,51 @@
from aiogram import Router, F
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.utils.i18n import gettext as _
from bot.templates import msg_photo
from bot.utils.interesting_facts import interesting_fact
from bot.core.bots import BotInfo
from configs import COMMANDS, RpValue
# Настройки экспорта и роутера
__all__ = ("router",)
CMD: str = "start".lower()
router: Router = Router(name=f"{CMD}_cmd_router")
@router.callback_query(F.data.lower() == CMD)
@router.message(Command(*COMMANDS[CMD], prefix=BotInfo.prefix, ignore_case=True))
async def start_cmd(message: Message | CallbackQuery, state: FSMContext) -> None:
"""Обработчик команды /start"""
await state.clear()
# Создание инлайн-клавиатуры
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
ikb.row(InlineKeyboardButton(text="Инфо-канал🗂", url=RpValue.INFO_URL))
ikb.row(InlineKeyboardButton(text="Вступление🚀", callback_data='new'),
InlineKeyboardButton(text="Анкета📖", callback_data='anketa'))
ikb.row(InlineKeyboardButton(text="Связь с администрацией🌐", callback_data='admin'))
# Формируем приветственное сообщение
text: str = _(
"""Добро пожаловать, <a href="{url}">{name}</a>!
Я ваш искусственный помощник по ролевой - <b>{rp_name}</b>!
Моя цель — помочь вам сориентироваться и сделать ваше вступление куда проще!
Надеюсь, я смогу вам помочь! Пожалуйста, выберите нужную функцию на клавиатуре!
Интересный факт:
<blockquote>{fact}</blockquote>
"""
).format(
url=message.from_user.url if message.from_user else "",
name=message.from_user.first_name if message.from_user else "пользователь",
rp_name=RpValue.RP_NAME,
fact=interesting_fact(),
)
# Отправляем сообщение
await msg_photo(message=message, text=text, file=f'assets/{CMD}.jpg', markup=ikb)

View File

@@ -0,0 +1,15 @@
from aiogram import Router
from .default import router as default_message_router
from .reply_msg import router as reply_message_router
# Настройка экспорта и роутера
__all__ = ('router',)
router: Router = Router(name=__name__)
# Подготовка роутера команд
#router.include_routers(
#reply_message_router,
#)
# Подключение стандартного роутера
router.include_router(default_message_router)

View File

@@ -0,0 +1,139 @@
from typing import Dict, List
from aiogram import Router
from aiogram.fsm.context import FSMContext
from aiogram.types import Message
from bot.utils import get_best_response
# Настройка экспорта и роутера
__all__ = ("router",)
router: Router = Router(name="message_router")
# === Словарь ключевых слов (синонимы) и возможных ответов ===
RESPONSES: Dict[str, Dict[str, List[str]]] = {
"док": {
"keywords": ["доктор", "док", "дотторе", "зандик"],
"answers": [
"Дотторе довольно милый друг! Мне нравится проводить с ним время!",
"Иногда он бывает слишком суровым... Но я верю, что смогу его перевоспитать!",
"Мне иногда кажется, что он знает больше историй, чем хранится в библиотеке!",
"Дотторе говорит загадками... а я всё равно не всегда понимаю!",
"Он умный, но я уверен — внутри он добрый!",
"Дотторе иногда ворчит, но всё равно заботится обо мне по-своему!",
"Он часто думает о науке... а я думаю о печеньках!",
"Мне кажется, он притворяется злым, а на самом деле просто боится дружбы.",
"Когда он работает, в комнате становится тихо... даже огонь боится мешать ему.",
"Я иногда думаю... а улыбается ли он, когда меня не видит?",
],
},
"ара": {
"keywords": ["ара", "аранара", "аранары", "ары", "кто ты", "ты кто"],
"answers": [
"Мы, аранары, очень любим веселиться и смеяться!",
"Хи-хи! 🌱 Ты можешь звать меня Ари!",
"Наш народ живёт уже тысячи лет... но мы не умеем считать!",
"Я маленький грибочек, но у меня большое сердце!",
"Аранара — это хранитель улыбок и весёлых историй!",
"Я люблю играть с детьми и рассказывать им истории!",
"Говорят, что аранары видят то, что скрыто от других.",
"Я — часть этой библиотеки, её дыхание и её смех!",
"Аранара — это маленький проводник в мир грёз и чудес.",
"Мы появляемся там, где нужен друг, даже если никто не звал!",
],
},
"малыш": {
"keywords": ["малыш", "девочка", "малышка", "она", "болезнь"],
"answers": [
"Она милая девочка! Жаль, что больна!",
"Она обожает сказки! Может, именно поэтому засыпает так сладко.",
"А как её зовут?.. Я всегда забываю спросить!",
"Иногда во сне она улыбается... значит, ей снятся хорошие истории.",
"Дотторе грустит, когда смотрит на неё... но я верю, он её спасёт!",
"Она словно светильник в тёмной комнате... даже если свет её тускнеет.",
"Я думаю, её мечты сильнее болезни.",
],
},
"эфир": {
"keywords": ["эфир", "проект", "изобретение", "сплав", "эксперимент", "ядро"],
"answers": [
"Эфир звучит как ветер, который нельзя поймать... но можно почувствовать!",
"Дотторе часто говорит о проектах, но я понимаю в них только половину!",
"Каждый новый сплав для него как новая история для меня.",
"Эксперимент — это как игра, только иногда она пахнет гарью...",
"Я слышал, что ядро может изменить всё... даже судьбы людей.",
"В лаборатории так много звуков — шипение кислот, стук молотов, шёпот формул.",
"Иногда мне кажется, что изобретения Дотторе живут своей жизнью...",
"Эфир? Кефир? ЗЕФИР!",
],
},
"мысль": {
"keywords": ["мысл", "мысль", "мысли", "думаешь"],
"answers": [
"О чём я думаю?.. Иногда о печеньках!",
"Голова как будто полная тумана...",
"Кажется, я что-то забыл... но не могу вспомнить...",
"Мысли приходят и уходят, как маленькие птички.",
"А ты когда-нибудь задумывался, откуда приходят мысли?",
"Иногда мои мысли путаются и превращаются в сказки.",
"Я думаю, что думать — это тяжело... лучше веселиться!",
"Может, мысли — это просто шёпот библиотеки в моей голове?",
"Когда я думаю слишком долго — у меня начинает чесаться макушка!",
"Мысли — как облака... смотришь, и они уже другие.",
],
},
"тайн": {
"keywords": ["тайн", "тайны", "тайну", "тайна"],
"answers": [
"Тайны? О-о, мы играем в детективов?!",
"Я знаю много секретов... но не все можно рассказывать!",
"Иногда самые большие тайны прячутся на виду.",
"Тайна — это как закрытая книга. Ты хочешь открыть её?",
"Хи-хи... а если твоя тайна уже записана в библиотеке?",
"Некоторые тайны лучше хранить, чем раскрывать.",
"Каждый друг — это тоже тайна, которую мы открываем постепенно.",
"А твои секреты я храню надёжнее любого сундука!",
"Тайна — это искра любопытства! Без неё жизнь скучная.",
"Ш-ш-ш... хочешь услышать одну маленькую, но очень смешную тайну?",
],
},
}
# === Случайные фразы, если совпадения нет ===
RANDOM_PHRASES: List[str] = [
"Я Ари! Компаньон Дотторе и ваш лучший друг! Можете обращаться ко мне!",
"Я живу здесь уже десятки лет... и мне всё ещё весело!",
"Кхм... почему ты так странно разговариваешь? Ничего не понимаю!",
"Мы играем в шарады? Давай попробуй ещё раз, может я пойму хоть одно слово!",
"Ты кажешься таким загадочным... прямо как проекты Дотторе, которые меня вечно пугают!",
"Ой! Ты меня напугал! Но всё равно приятно видеть нового друга!",
"Если вдруг станет грустно — просто обними аранару. Мы очень мягкие!",
"Иногда даже мне хочется спрятаться между колб и подремать...",
"А может, именно твоё слово станет началом новой истории?",
"Дотторе говорит, что я слишком болтлив... а разве это плохо?",
"Ты такой серьёзный... может, стоит немного пошутить?",
"Иногда кажется, что слова сами выбирают нас, а не мы их!",
]
# === Хэндлеры ===
@router.message()
async def handle_message(message: Message, state: FSMContext) -> None:
"""
Обрабатывает входящие сообщения от пользователя.
Определяет ответ по ключевым словам или случайную фразу.
:param message: объект сообщения
:param state: FSMContext для работы с состояниями
"""
await state.clear()
response: str = get_best_response(
message.text or "",
responses=RESPONSES,
random_phrases=RANDOM_PHRASES,
)
await message.answer(text=response)

View File

@@ -0,0 +1,39 @@
from random import choice
from typing import List
from aiogram import Router
from aiogram.fsm.context import FSMContext
from aiogram.types import Message
router: Router = Router(name="reply_router")
RANDOM_PHRASES: List[str] = [
"Бла-бла-бла!", "Хва-а-а-тит!", "Серьёзно? 😏", "Опять ты это говоришь...",
"Хи-хи, смешно же!", "Ты снова шутник?", "Я уже слышал это раньше!", "Эй, не надо так!",
"Ладно, ладно, хватит!", "Хмм... интересно...", "Ты меня удивляешь!", "А давай лучше что-то новое?",
"Не могу поверить!", "Ахаха, это забавно!", "Серьёзно? Ну ладно...", "Эй, это уже слишком!",
"О, это было неожиданно!",
]
@router.message()
async def reply_message(message: Message, state: FSMContext) -> None:
# Достаём данные из состояния
data = await state.get_data()
last_bot_text = data.get("last_bot_text", "")
# КРИТИЧЕСКИ ВАЖНО: Проверяем, что состояние не пустое после перезапуска.
# Если состояние пустое (например, после перезапуска), то мы НЕ должны считать,
if last_bot_text and message.text and message.text.strip() == last_bot_text.strip():
response = "Не повторяй за мной!"
else:
response = choice(RANDOM_PHRASES)
ids = message.message_id-1
print(str())
# Отправляем ответ и ПОЛУЧАЕМ ОБЪЕКТ ОТПРАВЛЕННОГО СООБЩЕНИЯ
sent_message = await message.reply(response)
# Сохраняем текст последнего сообщения бота в состоянии
# Теперь состояние будет обновлено после каждого сообщения бота
await state.update_data(last_bot_text=sent_message.text)

View File

@@ -0,0 +1,13 @@
from aiogram import Router
from .secret1 import router as secret1_router
#from .secret2 import router as secret2_router
# Настройка экспорта и роутера
__all__ = ('router',)
router: Router = Router(name=__name__)
# Подключение секретного роутера
router.include_routers(
secret1_router,
#secret2_router,
)

View File

@@ -0,0 +1,45 @@
from aiogram import Router, F
from aiogram.fsm.context import FSMContext
from aiogram.types import Message
from aiogram.utils.markdown import hide_link
from middleware.loggers import log
# Настройки экспорта и роутера
__all__ = ("router",)
router: Router = Router(name="secret_router")
CMD: str = "secret_1"
@router.message(F.text.lower() == "истинная цель короля всегда было мироздание")
@log(level='INFO', log_type=CMD.upper(), text=f"использовал команду /{CMD}")
async def secret1_cmd(message: Message, state: FSMContext) -> None:
"""Обработчик секретов"""
await state.clear()
# Формируем приветственное сообщение
text: str = f"""{hide_link("https://rp.primo.dpdns.org/wp-content/uploads/2025/08/1234567.png")}
<b><u>Запись №-...18</u></b>
<blockquote><i>Значит, этот </i><i><b>правда</b> действительно </i><i><b>существует…</b> Хах.. Хахахах! Я смог найти решение</i><i><b>! Я!! СМОГ!!!</b>
Все линии, пропорции, каждый слой металла и кристалла — всё сходится.
Сколько лет я </i><i><b>скитался</b> по лабораториям, библиотекам, ища этот след… а ведь всё это время </i><i><b>ключ</b> к моей цели лежал прямо перед глазами — на чертеже.</i></blockquote>
<blockquote><i>Получится ли у меня...?\n
Я создаю не просто броню. Я пытаюсь воплотить в материале замысел, который перевернёт всё, что мы знали.
Каждый слой, каждая руна — это шаг к воплощению моей идеи. Даже спустя десятки лет я помню, как возвращал из небытия те конструкции, что раньше казались невозможными…</i></blockquote>
<blockquote><i>Возможно ли, что сама структура материи и магии </i><i><b>изменит..</b>
Или же это моя броня станет первым устройством, способное изменить их мнение?..</i></blockquote>
<blockquote><i>И всё же один вопрос не даёт мне покоя: сможет ли этот замысел завершить то, что я задумал…
Станет ли моя броня инструментом, с помощью которого замысел воплотится в реальность?..</i></blockquote>
<blockquote><i>Пожалуй, придётся ещё раз вернуться к чертежам и проверить расчёты.
Что-то подсказывает мне: каждая линия, каждый символ на этом листе — это не просто металл и руны, это путь к моей великой… </i><i><b>идее</b>. ~</i></blockquote>
<tg-spoiler>Да… это оно. Всё ведёт к замыслу, к который я стремился десятилетиями…</tg-spoiler>"""
# Отправляем сообщение
await message.reply(text=text)

View File

@@ -0,0 +1,134 @@
from aiogram import Router, F
from aiogram.filters import Command, StateFilter
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import Message
# Создаем роутер
knowledge_router = Router()
# Определяем состояния
class KnowledgeStates(StatesGroup):
question1 = State()
question2 = State()
question3 = State()
question4 = State()
question5 = State()
question6 = State()
# Вопросы и ответы (замените на свои)
QUESTIONS = {
1: "Вопрос1",
2: "Вопрос2",
3: "Вопрос3",
4: "Вопрос4",
5: "Вопрос5",
6: "Вопрос6"
}
ANSWERS = {
1: {"Ответ 11": "СообщениеА1", "Ответ 12": "СообщениеБ1"},
2: {"Ответ 21": "СообщениеА2", "Ответ 22": "СообщениеБ2"},
3: {"Ответ 31": "СообщениеА3", "Ответ 32": "СообщениеБ3"},
4: {"Ответ 41": "СообщениеА4", "Ответ 42": "СообщениеБ4"},
5: {"Ответ 51": "СообщениеА5", "Ответ 52": "СообщениеБ5"},
6: {"Ответ 61": "СообщениеА6", "Ответ 62": "СообщениеБ6"}
}
FINAL_MESSAGES = {
"all_1": "ИТОГ1 - Все ответы первого типа!",
"all_2": "ИТОГ2 - Все ответы второго типа!",
"mixed": "ИТОГ1 - Смешанные ответы!"
}
# Запуск сессии знаний
@knowledge_router.message(StateFilter(None), Command("знания"))
@knowledge_router.message(StateFilter(None), F.text.casefold() == "пора заняться знаниями")
async def start_knowledge_session(message: Message, state: FSMContext):
await message.answer("Отлично! Начинаем сессию знаний! 🧠")
await message.answer(QUESTIONS[1])
await state.set_state(KnowledgeStates.question1)
await state.update_data(answers={})
# Обработчики для каждого вопроса
@knowledge_router.message(KnowledgeStates.question1, F.text.in_(ANSWERS[1].keys()))
async def process_question1(message: Message, state: FSMContext):
user_answer = message.text
response_message = ANSWERS[1][user_answer]
# Сохраняем ответ
answer_code = 1 if user_answer == "Ответ 11" else 2
await state.update_data(answers={"q1": answer_code})
# Отправляем сообщение и следующий вопрос
await message.answer(response_message + "\n\n" + QUESTIONS[2])
await state.set_state(KnowledgeStates.question2)
@knowledge_router.message(KnowledgeStates.question2, F.text.in_(ANSWERS[2].keys()))
async def process_question2(message: Message, state: FSMContext):
user_answer = message.text
response_message = ANSWERS[2][user_answer]
# Сохраняем ответ
answer_code = 1 if user_answer == "Ответ 21" else 2
data = await state.get_data()
answers = data.get("answers", {})
answers["q2"] = answer_code
await state.update_data(answers=answers)
# Отправляем сообщение и следующий вопрос
await message.answer(response_message + "\n\n" + QUESTIONS[3])
await state.set_state(KnowledgeStates.question3)
# Добавьте аналогичные обработчики для question3-question5
@knowledge_router.message(KnowledgeStates.question6, F.text.in_(ANSWERS[6].keys()))
async def process_question6(message: Message, state: FSMContext):
user_answer = message.text
response_message = ANSWERS[6][user_answer]
# Сохраняем ответ
answer_code = 1 if user_answer == "Ответ 61" else 2
data = await state.get_data()
answers = data.get("answers", {})
answers["q6"] = answer_code
await state.update_data(answers=answers)
# Отправляем финальное сообщение
await message.answer(response_message)
await finish_knowledge_session(message, state)
# Обработчики для некорректных ответов
@knowledge_router.message(KnowledgeStates.question1)
async def process_incorrect_answer1(message: Message):
await message.answer("Пожалуйста, выберите один из предложенных вариантов ответа.")
await message.answer(QUESTIONS[1])
@knowledge_router.message(KnowledgeStates.question2)
async def process_incorrect_answer2(message: Message):
await message.answer("Пожалуйста, выберите один из предложенных вариантов ответа.")
await message.answer(QUESTIONS[2])
# Добавьте аналогичные обработчики для остальных вопросов
# Завершение сессии
async def finish_knowledge_session(message: Message, state: FSMContext):
data = await state.get_data()
answers = data.get("answers", {})
# Проверяем результаты
if all(answer == 2 for answer in answers.values()):
await message.answer(FINAL_MESSAGES["all_2"])
else:
await message.answer(FINAL_MESSAGES["mixed"])
await state.clear()

View File

@@ -0,0 +1,2 @@
from .reply import *
from .inline import *

View File

@@ -0,0 +1 @@
from .decision import *

View File

@@ -0,0 +1,17 @@
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
def decision_keyboard(thread_id: int, kind: str) -> InlineKeyboardMarkup:
"""
Получение клавиатуры Принятия\Отклонить.
:param thread_id: Айди действия.
:param kind: Вид для клавиатуры.
:return: Инлайн-клавиатуру (Принять, Отклонить).
"""
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
ikb.row(
InlineKeyboardButton(text="✅ Принять", callback_data=f"{kind}:accept:{thread_id}"),
InlineKeyboardButton(text="❌ Отклонить", callback_data=f"{kind}:reject:{thread_id}")
)
return ikb.as_markup()

View File

View File

@@ -0,0 +1,48 @@
from aiogram import Dispatcher, Bot
from configs import ImportantID
from .logging_mdw import LoggingMiddleware
from .msg_mdw import MessageCounterMiddleware
from .spam_mdw import RateLimitMiddleware
from .subscription_mdw import SubscriptionMiddleware
from .error_mdw import ErrorHandlingMiddleware
from .time_mdw import TimingMiddleware
# Настройки экспорта
__all__ = (
"LoggingMiddleware",
"SubscriptionMiddleware",
"RateLimitMiddleware",
"ErrorHandlingMiddleware",
"TimingMiddleware",
"MessageCounterMiddleware",
"setup_middlewares",)
def setup_middlewares(dp: Dispatcher, bot: Bot, channel_ids: list[int | str] = None) -> None:
"""
Регистрирует все middleware в диспетчере.
"""
channel_ids: list = channel_ids or []
# Middleware для ВСЕХ событий (update level)
middlewares_updates: list = [
TimingMiddleware(), # Замер времени
LoggingMiddleware(), # Логирование
ErrorHandlingMiddleware(admin_ids=ImportantID.ADMIN_ID), # Обработка ошибок
]
# Middleware только для СООБЩЕНИЙ (message level)
middlewares_msg: list = [
#RateLimitMiddleware(rate_limit=3, time_period=5.0), # Антифлуд
#SubscriptionMiddleware(bot=bot, channel_ids=channel_ids), # Проверка подписки
MessageCounterMiddleware(), # Подсчет сообщений
]
# Регистрируем middleware для всех событий
for middleware in middlewares_updates:
dp.update.middleware(middleware)
# Регистрируем middleware только для сообщений
for middleware in middlewares_msg:
dp.message.middleware(middleware)

View File

@@ -0,0 +1,201 @@
from typing import Callable, Awaitable, Any, Dict
from aiogram import BaseMiddleware
from aiogram.types import TelegramObject, Message, CallbackQuery, Update
from middleware.loggers import loggers # ваш логгер
class ErrorHandlingMiddleware(BaseMiddleware):
"""
Middleware для глобальной обработки ошибок в хендлерах.
Зачем нужен:
- Централизованная обработка исключений
- Уведомление администраторов об ошибках
- Graceful degradation при сбоях
"""
def __init__(self, admin_ids: list[int]):
"""
Инициализация middleware обработки ошибок.
Args:
admin_ids: Список ID администраторов для уведомлений
"""
self.admin_ids = admin_ids
super().__init__()
async def __call__(
self,
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: Dict[str, Any]
) -> Any:
"""
Перехватывает и обрабатывает ошибки в хендлерах.
"""
try:
return await handler(event, data)
except Exception as e:
# Получаем информацию о пользователе безопасным способом
user_str = self._extract_user_info(event)
# Логируем ошибку
error_message = f"Ошибка в хендлере: {type(e).__name__}: {str(e)}"
loggers.error(
text=error_message,
log_type="HANDLER_ERROR",
user=user_str
)
# Уведомляем администраторов
await self._notify_admins(error_message, event, user_str)
# Отправляем пользователю сообщение об ошибке
await self._send_error_message(event, user_str)
return None
@staticmethod
def _extract_user_info(event: TelegramObject) -> str:
"""
Безопасно извлекает информацию о пользователе из события.
Args:
event: Объект события
Returns:
Строка с идентификатором пользователя
"""
user_str = "@System"
# Для Message и CallbackQuery
if isinstance(event, (Message, CallbackQuery)) and hasattr(event, 'from_user') and event.from_user:
user = event.from_user
user_str = f"@{user.username}" if user.username else f"id{user.id}"
# Для Update (который содержит message или callback_query)
elif isinstance(event, Update):
# Пытаемся найти пользователя в различных полях Update
user_object = None
if event.message and event.message.from_user:
user_object = event.message.from_user
elif event.edited_message and event.edited_message.from_user:
user_object = event.edited_message.from_user
elif event.callback_query and event.callback_query.from_user:
user_object = event.callback_query.from_user
elif event.channel_post and event.channel_post.from_user:
user_object = event.channel_post.from_user
elif event.edited_channel_post and event.edited_channel_post.from_user:
user_object = event.edited_channel_post.from_user
if user_object:
user_str = f"@{user_object.username}" if user_object.username else f"id{user_object.id}"
return user_str
@staticmethod
def _extract_event_text(event: TelegramObject) -> str:
"""
Безопасно извлекает текст из события.
Args:
event: Объект события
Returns:
Текст события или пустая строка
"""
event_text = ""
# Для Message
if isinstance(event, Message) and hasattr(event, 'text') and event.text:
event_text = event.text
# Для CallbackQuery
elif isinstance(event, CallbackQuery) and hasattr(event, 'data') and event.data:
event_text = f"callback: {event.data}"
# Для Update
elif isinstance(event, Update):
if event.message and event.message.text:
event_text = event.message.text
elif event.callback_query and event.callback_query.data:
event_text = f"callback: {event.callback_query.data}"
elif event.edited_message and event.edited_message.text:
event_text = event.edited_message.text
return event_text[:100] + "..." if len(event_text) > 100 else event_text
async def _notify_admins(
self,
error_message: str,
event: TelegramObject,
user_str: str
) -> None:
"""Уведомляет администраторов об ошибке."""
from aiogram import Bot
bot: Bot = event.bot if hasattr(event, 'bot') else None
if bot:
for admin_id in self.admin_ids:
try:
event_info = f"Событие: {type(event).__name__}"
event_text = self._extract_event_text(event)
if event_text:
event_info += f", текст: {event_text}"
full_message = (
f"🚨 Ошибка в боте:\n\n"
f"Пользователь: {user_str}\n"
f"Ошибка: {error_message}\n"
f"{event_info}"
)
await bot.send_message(admin_id, full_message)
loggers.info(
text=f"Администратор {admin_id} уведомлен об ошибке",
log_type="ADMIN_NOTIFIED",
user=user_str
)
except Exception as e:
loggers.error(
text=f"Не удалось уведомить админа {admin_id}: {e}",
log_type="ADMIN_NOTIFY_ERROR",
user=user_str
)
@staticmethod
async def _send_error_message(
event: TelegramObject,
user_str: str
) -> None:
"""Отправляет пользователю сообщение об ошибке."""
error_text = (
"⚠️ Произошла непредвиденная ошибка. "
"Разработчики уже уведомлены и работают над исправлением.\n\n"
"Попробуйте повторить действие позже или нажмите /start"
)
try:
if isinstance(event, Message):
await event.answer(error_text)
elif isinstance(event, CallbackQuery):
await event.message.answer(error_text)
await event.answer()
elif isinstance(event, Update) and event.message:
await event.message.answer(error_text)
loggers.info(
text="Пользователю отправлено сообщение об ошибке",
log_type="ERROR_MESSAGE_SENT",
user=user_str
)
except Exception as e:
loggers.error(
text=f"Не удалось отправить сообщение об ошибке: {e}",
log_type="ERROR_MESSAGE_FAILED",
user=user_str
)

View File

@@ -0,0 +1,271 @@
from typing import Callable, Awaitable, Any, Dict, Optional, Tuple, Set
from aiogram import BaseMiddleware
from aiogram.types import TelegramObject, Update, Message, CallbackQuery, MaybeInaccessibleMessageUnion, User
from bot.utils import type_msg
from middleware.loggers import loggers # ваш глобальный логгер
from configs import BotSettings, COMMANDS # импортируем настройки и команды
class LoggingMiddleware(BaseMiddleware):
"""
Middleware для логирования апдейтов с определением типа события,
пользователя и добавлением префикса проекта к типу лога.
Автоматически добавляет префикс проекта (например, 'PRIMO-') к типам логов:
- PRIMO-UPDATE: общий апдейт без определенного типа
- PRIMO-MSG: текстовое сообщение от пользователя
- PRIMO-CMD: команда (сообщение, начинающееся с любого префикса)
- PRIMO-CBD: callback query от инлайн-кнопок
"""
# Префикс проекта для логов
PROJECT_PREFIX: str = "PRIMO"
# Кэш для всех команд из COMMANDS
_all_commands: Optional[Set[str]] = None
def __init__(self):
super().__init__()
# Предварительно загружаем все команды
self._load_all_commands()
def _load_all_commands(self) -> None:
"""Загружает все команды из COMMANDS в множество для быстрого поиска."""
if self._all_commands is None:
self._all_commands = set()
for command_list in COMMANDS.values():
self._all_commands.update(command_list)
def _is_command(self, text: str) -> bool:
"""
Проверяет, является ли текст командой с любым префиксом.
Args:
text: Текст для проверки
Returns:
True если это команда, False если нет
"""
if not text:
return False
# Проверяем все префиксы из BotSettings
for prefix in BotSettings.PREFIX:
if text.startswith(prefix):
# Извлекаем команду без префикса
command_without_prefix = text[len(prefix):].strip()
# Проверяем, есть ли такая команда в нашем списке
if command_without_prefix in self._all_commands:
return True
# Также проверяем команды с префиксом / (стандартные)
if text.startswith('/'):
command_without_slash = text[1:].strip()
if command_without_slash in self._all_commands:
return True
return False
@staticmethod
def _extract_command_name(text: str) -> str:
"""
Извлекает название команды из текста.
Args:
text: Текст команды с префиксом
Returns:
Название команды без префикса
"""
for prefix in BotSettings.PREFIX:
if text.startswith(prefix):
return text[len(prefix):].strip()
if text.startswith('/'):
return text[1:].strip()
return text
async def __call__(
self,
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: Dict[str, Any]
) -> Any:
"""
Обрабатывает входящее событие, определяет его тип, логирует с префиксом проекта
и передает следующему обработчику.
Args:
handler: Следующий обработчик в цепочке middleware
event: Входящее событие для обработки (Update, Message, CallbackQuery)
data: Словарь с контекстными данными FSM
Returns:
Результат выполнения следующего обработчика
Raises:
Exception: Любое исключение, возникшее при обработке хендлером
"""
# Определяем тип события и информацию для логирования
log_type: str
log_text: str
message_obj: Optional[Message]
log_type, log_text, message_obj = self._determine_event_type(event)
# Добавляем префикс проекта к типу лога
prefixed_log_type: str = f"{log_type}"
# Определяем информацию о пользователе
user_str: str = self._extract_user_info(event, message_obj)
# Логируем получение события с префиксом проекта
loggers.info(
text=log_text,
log_type=prefixed_log_type,
user=user_str
)
try:
# Передаем событие следующему обработчику
result: Any = await handler(event, data)
# Логируем успешное выполнение для команд
if log_type == "CMD":
loggers.info(
text=f"[SUCCESS] команда обработана",
log_type=prefixed_log_type,
user=user_str
)
return result
except Exception as e:
# Логируем ошибку при обработке с префиксом проекта
loggers.error(
text=f"Ошибка обработки: {str(e)}",
log_type=prefixed_log_type,
user=user_str
)
raise
def _determine_event_type(
self,
event: TelegramObject
) -> Tuple[str, str, Optional[Message]]:
"""
Определяет тип события и извлекает информацию для логирования.
Args:
event: Объект события для анализа
Returns:
Кортеж из (тип_лога, текст_лога, объект_сообщения)
"""
log_type: str = "UPDATE"
log_text: str = f"Получен апдейт: {type(event).__name__}"
message_obj: Optional[Message] = None
# Обработка Update объектов (основной тип в middleware)
if isinstance(event, Update):
# Пытаемся найти сообщение в различных полях Update
message_obj = (
event.message or
event.edited_message or
event.channel_post or
event.edited_channel_post
)
if message_obj and message_obj.text:
if self._is_command(message_obj.text):
log_type: str = "CMD"
log_text: str = f"использовал команду '{message_obj.text}'"
else:
log_type: str = "MSG"
log_text: str = f"получено сообщение: {message_obj.text!r}"
elif message_obj:
# Не текстовое сообщение (фото, видео и т.д.)
log_type: str = "MSG"
log_text: str = f"получено сообщение: '{type_msg(message_obj)}'"
elif event.callback_query:
# Обработка callback query
callback: CallbackQuery = event.callback_query
log_type: str = "CBD"
log_text: str = f"получен callback: {callback.data!r}"
if callback.message:
message_obj: Optional[MaybeInaccessibleMessageUnion] = callback.message
# Прямая обработка Message (если мидлварь зарегистрирован на messages)
elif isinstance(event, Message):
message_obj = event
if event.text and self._is_command(event.text):
log_type: str = "CMD"
log_text: str = f"использовал команду '{event.text}'"
elif event.text:
log_type: str = "MSG"
log_text: str = f"получено сообщение: {event.text!r}"
else:
log_type: str = "MSG"
log_text: str = f"получено сообщение типа: {event.content_type}"
# Прямая обработка CallbackQuery (если мидлварь зарегистрирован на callbacks)
elif isinstance(event, CallbackQuery):
log_type: str = "CBD"
log_text: str = f"получен callback: {event.data!r}"
if event.message:
message_obj = event.message
return log_type, log_text, message_obj
@staticmethod
def _extract_user_info(
event: TelegramObject,
message: Optional[Message] = None
) -> str:
"""
Извлекает информацию о пользователе из события.
Args:
event: Объект события (Update, Message или CallbackQuery)
message: Объект Message (если уже определен)
Returns:
Строка с идентификатором пользователя в формате '@username' или 'id<user_id>'
"""
user_str: str = "@System"
# Для CallbackQuery извлекаем пользователя из самого callback'а
if isinstance(event, CallbackQuery) and hasattr(event, 'from_user') and event.from_user:
user = event.from_user
user_str: str = f"@{user.username}" if user.username else f"id{user.id}"
# Для Message извлекаем пользователя из сообщения
elif isinstance(event, Message) and hasattr(event, 'from_user') and event.from_user:
user = event.from_user
user_str: str = f"@{user.username}" if user.username else f"id{user.id}"
# Для Update с callback_query
elif (isinstance(event, Update) and
event.callback_query and
hasattr(event.callback_query, 'from_user') and
event.callback_query.from_user):
user = event.callback_query.from_user
user_str: str = f"@{user.username}" if user.username else f"id{user.id}"
# Для Update с сообщением
elif (isinstance(event, Update) and
(event.message or event.edited_message) and
hasattr(event.message or event.edited_message, 'from_user')):
msg = event.message or event.edited_message
if msg and msg.from_user:
user: Optional[User] = msg.from_user
user_str: str = f"@{user.username}" if user.username else f"id{user.id}"
# Если передан message объект
elif message and hasattr(message, 'from_user') and message.from_user:
user: Optional[User] = message.from_user
user_str: str = f"@{user.username}" if user.username else f"id{user.id}"
return user_str

View File

@@ -0,0 +1,55 @@
import logging
from typing import Callable, Dict, Any, Awaitable
from aiogram import BaseMiddleware
from aiogram.enums import ChatType
from aiogram.types import Message
from database import db
logger = logging.getLogger(__name__)
class MessageCounterMiddleware(BaseMiddleware):
"""
Middleware для подсчёта сообщений в группах и супергруппах.
"""
async def __call__(
self,
handler: Callable[[Message, Dict[str, Any]], Awaitable[Any]],
event: Any,
data: Dict[str, Any]
) -> Any:
if not isinstance(event, Message):
return await handler(event, data)
# Проверяем, что сообщение пришло из группового чата и не от бота
if (event.chat.type in (ChatType.GROUP, ChatType.SUPERGROUP) and
not event.from_user.is_bot):
try:
await self.process_group_message(event)
except Exception as e:
logger.error(msg=f"Ошибка при обработке сообщения: {e}", exc_info=True)
return await handler(event, data)
@staticmethod
async def process_group_message(message: Message) -> None:
"""
Обработка сообщения из группового чата.
"""
user_id = message.from_user.id
message_text = message.text or message.caption or ""
# Добавляем пользователя (если его ещё нет)
await db.add_user(
user_id=user_id,
username=message.from_user.username,
full_name=message.from_user.full_name,
)
# Сохраняем сообщение
await db.add_message(
user_id=user_id,
message_text=message_text,
created_at=message.date,
)
logger.info(f"Сообщение от пользователя {user_id} сохранено в БД")

View File

@@ -0,0 +1,97 @@
from typing import Callable, Awaitable, Any, Dict
from aiogram import BaseMiddleware
from aiogram.types import TelegramObject, Message, CallbackQuery
import time
from collections import defaultdict
from middleware.loggers import loggers # ваш логгер
class RateLimitMiddleware(BaseMiddleware):
"""
Middleware для ограничения частоты запросов от пользователей (анти-спам).
Зачем нужен:
- Защита от DDoS и флуда
- Предотвращение злоупотребления ботом
- Контроль нагрузки на сервер
"""
def __init__(self, rate_limit: int = 10, time_period: float = 2.0):
"""
Инициализация rate limit middleware.
Args:
rate_limit: Максимальное количество запросов за период
time_period: Период времени в секундах
"""
self.rate_limit = rate_limit
self.time_period = time_period
self.user_calls: Dict[int, list[float]] = defaultdict(list)
super().__init__()
async def __call__(
self,
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: Dict[str, Any],
log: bool = False,
) -> Any:
"""
Проверяет rate limit перед обработкой запроса.
"""
# Пропускаем не-сообщения и не-колбэки
if not isinstance(event, (Message, CallbackQuery)):
return await handler(event, data)
user_id: int = event.from_user.id
user_str: str = f"@{event.from_user.username}" if event.from_user.username else f"id{user_id}"
current_time: float = time.time()
# Очищаем старые запросы
self.user_calls[user_id] = [
call_time for call_time in self.user_calls[user_id]
if current_time - call_time < self.time_period
]
# Логируем текущее состояние rate limit
if log:
loggers.debug(
text=f"Rate limit: {len(self.user_calls[user_id])}/{self.rate_limit} за {self.time_period}сек",
log_type="RATE_LIMIT_STATUS",
user=user_str
)
# Проверяем текущий лимит
if len(self.user_calls[user_id]) >= self.rate_limit:
# Логируем попытку спама
if log:
loggers.warning(
text=f"Превышен rate limit ({self.rate_limit}/{self.time_period}сек)",
log_type="RATE_LIMIT_EXCEEDED",
user=user_str
)
# Отправляем сообщение о превышении лимита
if isinstance(event, Message):
await event.answer(
text="⏳ Слишком много запросов! Пожалуйста, подождите немного.",
)
elif isinstance(event, CallbackQuery):
await event.answer(
text="⏳ Подождите немного перед следующим действием.",
show_alert=True
)
return None
# Добавляем текущий запрос и продолжаем обработку
self.user_calls[user_id].append(current_time)
loggers.debug(
text=f"Запрос добавлен в rate limit",
log_type="RATE_LIMIT_ADDED",
user=user_str
)
return await handler(event, data)

View File

@@ -0,0 +1,115 @@
from typing import Callable, Awaitable, Any, Dict
from aiogram import BaseMiddleware, Bot
from aiogram.types import TelegramObject, Message, CallbackQuery
from aiogram.exceptions import TelegramBadRequest
from middleware.loggers import loggers # ваш логгер
class SubscriptionMiddleware(BaseMiddleware):
"""
Middleware для проверки подписки пользователя на необходимые каналы.
Блокирует обработку команд, если пользователь не подписан.
Зачем нужен:
- Автоматическая проверка подписки для всех входящих сообщений
- Единая точка управления подписками
- Предотвращение доступа к функционалу без подписки
"""
def __init__(self, bot: Bot, channel_ids: list[int | str]):
"""
Инициализация middleware проверки подписки.
Args:
bot: Экземпляр бота
channel_ids: Список ID каналов/чатов для проверки подписки
"""
self.bot = bot
self.channel_ids = channel_ids
super().__init__()
async def __call__(
self,
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: Dict[str, Any]
) -> Any:
"""
Проверяет подписку пользователя перед обработкой команды.
"""
# Пропускаем не-сообщения и не-колбэки
if not isinstance(event, (Message, CallbackQuery)):
return await handler(event, data)
user_id: int = event.from_user.id
user_str: str = f"@{event.from_user.username}" if event.from_user.username else f"id{user_id}"
# Логируем начало проверки подписки
loggers.info(
text=f"Проверка подписки для пользователя",
log_type="SUBSCRIPTION_CHECK",
user=user_str
)
# Проверяем подписку на все required каналы
not_subscribed_channels: list[str] = []
for channel_id in self.channel_ids:
try:
member = await self.bot.get_chat_member(
chat_id=channel_id,
user_id=user_id
)
# Проверяем, что пользователь является участником
if member.status not in ['member', 'administrator', 'creator']:
not_subscribed_channels.append(str(channel_id))
except TelegramBadRequest as e:
loggers.error(
text=f"Ошибка проверки подписки на канал {channel_id}: {e}",
log_type="SUBSCRIPTION_ERROR",
user=user_str
)
# Если пользователь не подписан на некоторые каналы
if not_subscribed_channels:
loggers.warning(
text=f"Пользователь не подписан на каналы: {', '.join(not_subscribed_channels)}",
log_type="SUBSCRIPTION_FAILED",
user=user_str
)
warning_text = (
"📢 Для использования бота необходимо подписаться на наши каналы!\n\n"
"После подписки нажмите /start для продолжения."
)
# Создаем кнопку "Проверить подписку"
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
keyboard = InlineKeyboardMarkup(
inline_keyboard=[[
InlineKeyboardButton(
text="✅ Я подписался",
callback_data="check_subscription"
)
]]
)
if isinstance(event, Message):
await event.answer(warning_text, reply_markup=keyboard)
elif isinstance(event, CallbackQuery):
await event.message.answer(warning_text, reply_markup=keyboard)
await event.answer()
return None
# Логируем успешную проверку подписки
loggers.info(
text="Пользователь подписан на все required каналы",
log_type="SUBSCRIPTION_SUCCESS",
user=user_str
)
# Если подписка есть, продолжаем обработку
return await handler(event, data)

View File

@@ -0,0 +1,82 @@
from typing import Callable, Awaitable, Any, Dict
from aiogram import BaseMiddleware
from aiogram.types import TelegramObject, Message, CallbackQuery, Update
from time import time
from middleware.loggers import loggers # ваш логгер
class TimingMiddleware(BaseMiddleware):
"""
Middleware для измерения времени выполнения хендлеров.
Зачем нужен:
- Мониторинг производительности хендлеров
- Выявление медленных запросов
- Оптимизация кода бота
"""
async def __call__(
self,
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: Dict[str, Any],
perm: str = None,
) -> Any:
"""
Измеряет время выполнения хендлера.
"""
start_time: float = time()
try:
result = await handler(event, data)
return result
finally:
execution_time: float = time() - start_time
# Получаем информацию о пользователе безопасным способом
user_str: str = "@System"
# Для Message и CallbackQuery
if isinstance(event, (Message, CallbackQuery)) and hasattr(event, 'from_user') and event.from_user:
user = event.from_user
user_str = f"@{user.username}" if user.username else f"id{user.id}"
# Для Update (который содержит message или callback_query)
elif isinstance(event, Update):
# Пытаемся найти пользователя в различных полях Update
user_object = None
if event.message and event.message.from_user:
user_object = event.message.from_user
elif event.edited_message and event.edited_message.from_user:
user_object = event.edited_message.from_user
elif event.callback_query and event.callback_query.from_user:
user_object = event.callback_query.from_user
elif event.channel_post and event.channel_post.from_user:
user_object = event.channel_post.from_user
elif event.edited_channel_post and event.edited_channel_post.from_user:
user_object = event.edited_channel_post.from_user
if user_object:
user_str = f"@{user_object.username}" if user_object.username else f"id{user_object.id}"
# Логируем время выполнения
if execution_time > 1.0 and perm: # Медленные запросы
loggers.warning(
text=f"Медленный хендлер: {execution_time:.2f}сек",
log_type="SLOW_HANDLER",
user=user_str
)
elif execution_time > 0.5 and perm == "medium": # Средние запросы
loggers.info(
text=f"Среднее время выполнения: {execution_time:.3f}сек",
log_type="HANDLER_TIMING",
user=user_str
)
elif perm == "fast": # Быстрые запросы
loggers.debug(
text=f"Быстрое выполнение: {execution_time:.3f}сек",
log_type="HANDLER_TIMING_FAST",
user=user_str
)

0
bot/states/__init__.py Normal file
View File

View File

@@ -0,0 +1,5 @@
# bot/states/form.py
from aiogram.fsm.state import State, StatesGroup
class StartForm(StatesGroup):
waiting_for_application: State = State()

8
bot/states/new_states.py Normal file
View File

@@ -0,0 +1,8 @@
# bot/states/new_states.py
from aiogram.fsm.state import State, StatesGroup
class NewStates(StatesGroup):
role: State = State()
sorol: State = State()
code_phrase: State = State()
rules: State = State()

View File

@@ -0,0 +1 @@
from .message_callback import *

View File

@@ -0,0 +1,77 @@
from typing import Union
from aiogram.types import FSInputFile, CallbackQuery, Message, ReplyKeyboardMarkup, InlineKeyboardMarkup
from aiogram.utils.keyboard import ReplyKeyboardBuilder, InlineKeyboardBuilder
# Настройка экспорта
__all__ = ('msg', 'msg_photo')
async def msg(message: Message | CallbackQuery,
text: str = "Сообщение отправлено!",
markup: Union[InlineKeyboardBuilder, ReplyKeyboardBuilder, None] = None) -> None:
"""
Шаблон для ответа на сообщение текстом.
:param message: Объект сообщения или callback-запроса.
:param text: Текст отправного сообщения от бота.
:param markup: Кнопки сообщения (инлайн или реплай).
"""
# Преобразуем клавиатуру
reply_markup: Union[InlineKeyboardMarkup, ReplyKeyboardMarkup, None] = None
if markup:
if isinstance(markup, InlineKeyboardBuilder):
reply_markup: InlineKeyboardMarkup = markup.as_markup()
elif isinstance(markup, ReplyKeyboardBuilder):
reply_markup: ReplyKeyboardMarkup = markup.as_markup(resize_keyboard=True)
# Обработчик ответа на сообщение
if isinstance(message, Message):
await message.reply(
text=text,
reply_markup=reply_markup
)
# Обработчик ответа на callback
else:
await message.message.reply(
text=text,
reply_markup=reply_markup
)
async def msg_photo(
message: Message | CallbackQuery,
text: str = "Сообщение отправлено!",
file: str = "assets/default.jpg",
markup: Union[InlineKeyboardBuilder, ReplyKeyboardBuilder, None] = None) -> None:
"""
Шаблон для ответа на сообщение фотографией.
:param message: Объект сообщения или callback-запроса.
:param file: Путь к фотографии для ответа.
:param text: Подпись к фото.
:param markup: Кнопки сообщения (инлайн или реплай).
"""
# Преобразуем клавиатуру
reply_markup: Union[InlineKeyboardMarkup, ReplyKeyboardMarkup, None] = None
if markup:
if isinstance(markup, InlineKeyboardBuilder):
reply_markup = markup.as_markup()
elif isinstance(markup, ReplyKeyboardBuilder):
reply_markup = markup.as_markup(resize_keyboard=True)
# Обработчик ответа на сообщение
if isinstance(message, Message):
await message.reply_photo(
photo=FSInputFile(file),
caption=text,
reply_markup=reply_markup
)
# Обработчик ответа на callback
else:
await message.message.reply_photo(
photo=FSInputFile(file),
caption=text,
reply_markup=reply_markup
)

5
bot/utils/__init__.py Normal file
View File

@@ -0,0 +1,5 @@
from .interesting_facts import *
from .usernames import *
from .pagination import *
from .type_message import *
from .argument import *

59
bot/utils/argument.py Normal file
View File

@@ -0,0 +1,59 @@
from __future__ import annotations
from typing import Optional
from configs import BotSettings
__all__ = ("is_command", "find_argument")
def is_command(message: Optional[str]) -> bool:
"""
Проверяет, является ли сообщение командой.
Сообщение считается командой, если:
1. Оно не пустое;
2. Начинается с префикса команды, указанного в настройках.
Args:
message (Optional[str]): Входное сообщение.
Returns:
bool: True, если сообщение является командой, иначе False.
Пример:
>>> is_command("/start")
True
>>> is_command("hello")
False
"""
if not message:
return False
return message.strip().startswith(BotSettings.PREFIX)
def find_argument(message: Optional[str]) -> Optional[str]:
"""
Извлекает аргумент команды из сообщения.
Аргументом считается текст после первой команды и пробела.
Если аргумента нет — возвращает None.
Args:
message (Optional[str]): Входное сообщение.
Returns:
Optional[str]: Аргумент команды или None, если его нет.
Пример:
>>> find_argument("/start referrer")
'referrer'
>>> find_argument("/start")
None
>>> find_argument("hello")
None
"""
if not is_command(message):
return None
parts = message.strip().split(maxsplit=1)
return parts[1] if len(parts) > 1 else None

View File

@@ -0,0 +1,54 @@
from random import choice
from typing import Dict, List, Optional
from configs.config import Lists
__all__ = ("interesting_fact", "get_best_response",)
def interesting_fact(mode: str = "факт", lists: Optional[list[str]] = None) -> str:
"""
Возвращает случайный факт, анекдот или цитату, в зависимости от режима.
:param mode: строка, определяющая тип контента ("факт", "анекдот", "цитата").
:param lists: необязательный список строк, из которого можно выбирать вручную.
:return: случайный элемент из соответствующего списка.
"""
if lists is not None:
return choice(lists)
mode = mode.lower()
if mode == "анекдот":
source: list[str] = Lists.jokes
elif mode == "цитата":
source: list[str] = Lists.quotes
else:
source: list[str] = Lists.facts
return choice(source)
def get_best_response(
user_text: str,
responses: Dict[str, Dict[str, List[str]]],
random_phrases: List[str],
) -> str:
"""
Подбирает наиболее подходящий ответ на сообщение пользователя.
Сначала ищет ключевые слова и их синонимы, если совпадений нет — выдаёт случайную фразу.
:param user_text: текст сообщения пользователя
:param responses: словарь с ключевыми словами и ответами
:param random_phrases: список случайных фраз, если совпадений нет
:return: строка с ответом
"""
normalized_text: str = user_text.lower()
# Перебор ключевых слов в словаре
for _, data in responses.items():
for keyword in data["keywords"]:
if keyword in normalized_text:
return choice(data["answers"])
# Если совпадений нет — выдаём случайную фразу
return choice(random_phrases)

28
bot/utils/pagination.py Normal file
View File

@@ -0,0 +1,28 @@
from aiogram.types import InlineKeyboardButton
# Настройка экспорта в модули
__all__ = ('pagination_btn',)
def pagination_btn(action: str,
page: int = 0,
total_posts: int = 0,
bt_page: int = 5) -> list[InlineKeyboardButton]:
"""
Создает кнопки для пагинации.
:param action: Действие в котором нужна пангинация.
:param page: Номер начальной страницы, по умолчанию 0.
:param total_posts: Количество постов.
:param bt_page: Количество кнопок на одной странице.
:return: Готовый лист списка инлайн-кнопок.
"""
navigation_buttons: list[InlineKeyboardButton] = []
if page > 0:
navigation_buttons.append(InlineKeyboardButton(
text="", callback_data=f"{action}_page_{page - 1}"
))
if (page + 1) * bt_page < total_posts:
navigation_buttons.append(InlineKeyboardButton(
text="", callback_data=f"{action}_page_{page + 1}"
))
return navigation_buttons

18
bot/utils/random_lists.py Normal file
View File

@@ -0,0 +1,18 @@
from random import choice
def get_best_response(user_text: str) -> str:
"""
Подбирает наиболее подходящий ответ на сообщение пользователя.
Сначала ищет ключевые слова и их синонимы, если совпадений нет — выдаёт случайную фразу.
:param user_text: текст сообщения пользователя
:return: строка с ответом
"""
normalized_text: str = user_text.lower()
for _, data in RESPONSES.items():
for keyword in data["keywords"]:
if keyword in normalized_text:
return choice(data["answers"])
return choice(RANDOM_PHRASES)

85
bot/utils/type_message.py Normal file
View File

@@ -0,0 +1,85 @@
from typing import Final
from aiogram.types import Message
# Настройка экспорта
__all__ = ("CHAT_TYPES", "CONTENT_TYPE_RU", "type_chat", "type_msg")
# Словарь сопоставлений "chat_type -> русское название"
CHAT_TYPES: Final[dict[str, str]] = {
"private": "Личный",
"group": "Группа",
"supergroup": "Группа",
"channel": "Канал",
}
# Словарь сопоставлений "content_type -> русское название"
CONTENT_TYPE_RU: Final[dict[str, str]] = {
"text": "Текст",
"animation": "Гиф",
"audio": "Аудио",
"document": "Файл",
"photo": "Фото",
"sticker": "Стикер",
"video": "Видео",
"video_note": "Видеосообщение",
"voice": "Голосовое сообщение",
"contact": "Контакт",
"dice": "Кубик",
"game": "Игра",
"poll": "Опрос",
"venue": "Место",
"location": "Локация",
"new_chat_members": "Новые участники чата",
"left_chat_member": "Участник вышел",
"new_chat_title": "Новое название чата",
"new_chat_photo": "Новая картинка чата",
"delete_chat_photo": "Удалена картинка чата",
"group_chat_created": "Создана группа",
"supergroup_chat_created": "Создана супергруппа",
"channel_chat_created": "Создан канал",
"message_auto_delete_timer_changed": "Изменён автоудалитель",
"migrate_to_chat_id": "Группа → супергруппа",
"migrate_from_chat_id": "Супергруппа → группа",
"pinned_message": "Закреплённое сообщение",
"invoice": "Счёт",
"successful_payment": "Успешный платёж",
"connected_website": "Подключённый сайт",
"passport_data": "Данные Telegram Passport",
"proximity_alert_triggered": "Алерт о приближении",
"video_chat_scheduled": "Запланированный видеочат",
"video_chat_started": "Видеочат начался",
"video_chat_ended": "Видеочат завершён",
"video_chat_participants_invited": "Приглашены участники видеочата",
"web_app_data": "Данные из веб-приложения",
"forum_topic_created": "Создана тема форума",
"forum_topic_edited": "Изменена тема форума",
"forum_topic_closed": "Тема форума закрыта",
"forum_topic_reopened": "Тема форума открыта",
"general_forum_topic_hidden": "Общая тема скрыта",
"general_forum_topic_unhidden": "Общая тема снова отображается",
"giveaway_created": "Создан розыгрыш",
"giveaway": "Розыгрыш",
"giveaway_completed": "Розыгрыш завершён",
"message_reaction": "Реакция на сообщение",
}
def type_msg(message: Message) -> str:
"""
Определяет и возвращает тип сообщения на русском языке.
:param message: объект Message от aiogram
:return: строка с типом сообщения
"""
return CONTENT_TYPE_RU.get(message.content_type, f"Неизвестный тип ({message.content_type})")
def type_chat(message: Message) -> str:
"""
Преобразует информацию о чате в его тип на русском языке.
:param message: Объект сообщения из aiogram, содержащий информацию о чате.
:return: Тип чата строкой.
"""
return CHAT_TYPES.get(message.chat.type, f"Неизвестный тип чата {message.chat.type}")

21
bot/utils/usernames.py Normal file
View File

@@ -0,0 +1,21 @@
from aiogram.types import Message
# Настройка экспорта в модули
__all__ = ('username', )
# Функция получения юзера или ID пользователя
def username(message: Message) -> str:
"""
Возвращает юзернейм пользователя из сообщения, или ID, если юзернейм не указан.
:param message: Объект сообщения из aiogram.
:return: Строка с юзернеймом пользователя или его ID.
:raises ValueError: Если в сообщении отсутствует информация о пользователе.
"""
try:
if message.from_user:
return f"@{message.from_user.username}" if message.from_user.username else f"@{message.from_user.id}"
raise ValueError("Информация о пользователе отсутствует в сообщении.")
except ValueError as e:
raise e # Перебрасываем ошибку выше для дальнейшей обработки

3
configs/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
from .config import *
from .cmd_list import *
from .roles import *

86
configs/cmd_list.py Normal file
View File

@@ -0,0 +1,86 @@
from typing import Final
# Список команд по ключу
COMMANDS: Final[dict[str, list[str]]] = {
"start": [
"start", "старт", "почати",
"ыефке", "cnfhn", "on", "вкл", "щт", "drk",
],
"help": [
"help", "помощь", "допомога",
"рудзщь", "dopomoga", "?",
],
"menu": [
"menu", "меню", "менюшка",
"ьщкф", "menyu",
],
"create": [
"create", "создать", "створити",
"сщзду", "sozdat", "stvoriti",
],
"report": [
"report", "репорт", "скарга",
"кщзщтв", "repert",
],
"mute": [
"mute", "заглушить", "заглушити",
"угуыщцук", "zaglushit",
],
"kick": [
"kick", "кик", "викинути",
"куиф", "vikynuty",
],
"ban": [
"ban", "бан", "забанити",
"ьфд", "zabanyty",
],
"stats": [
"stats", "статистика", "статистика",
"ыпщз", "statystyka",
],
"settings": [
"settings", "настройки", "налаштування",
"гшеукефьз", "nastroyky",
],
"info": [
"info", "инфо", "інфо",
"шкещ", "info",
],
"feedback": [
"feedback", "обратная связь", "зворотній зв’язок",
"гуеекфьз", "obratnaia_svyaz",
],
"subscribe": [
"subscribe", "подписаться", "підписатися",
"подписатсь", "pidpysatysia",
],
"unsubscribe": [
"unsubscribe", "отписаться", "відписатися",
"отписаться", "vidpysatysia",
],
"language": [
"language", "язык", "мова",
"йцукефь", "mova",
],
"cancel": [
"cancel", "отмена", "скасувати",
"утпщге", "skasuvaty",
],
"list": [
"list", "список", "список",
"дшззщк", "spysok",
],
"forward": [
"forward", "переслать", "переслати",
"дшпекщву", "pereslaty",
],
"new": [
"new", "туц", "вступление",
"cnegktybt", "ym.", "нью",
],
"active": [
"active",
]
}

394
configs/config.py Normal file
View File

@@ -0,0 +1,394 @@
from pathlib import Path
from urllib.parse import urlparse, ParseResult
from typing import ClassVar, Final, Optional, Any
from pydantic import field_validator, model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
from aiogram.types import ChatAdministratorRights
class Settings(BaseSettings):
"""Улучшенный класс настроек с комплексной валидацией"""
# Конфигурация загрузки переменных окружения
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
extra="ignore",
case_sensitive=False,
validate_default=True,
)
# Режимы и базовые параметры
PYTHONUNBUFFERED: str = "1"
LOCALE_PATH: str = "locales"
DEBUG: bool = False
OWNER: str = "@verdise"
# Токены бота
BOT_TOKEN: Optional[str] = None
BOT_DEBUG_TOKEN: Optional[str] = None
# Параметры сообщений
PARSE_MODE: str = "HTML"
ENCOD: str = "utf-8"
TIME_FORMAT: str = "%Y-%m-%d %H:%M:%S"
PREFIX: str = "/!.&?"
BOT_LANGUAGE: str = "Aiogram3"
# Настройки сообщений
DISABLE_NOTIFICATION: bool = False
PROTECT_CONTENT: bool = False
ALLOW_SENDING_WITHOUT_REPLY: bool = True
LINK_PREVIEW_IS_DISABLED: bool = False
LINK_PREVIEW_PREFER_SMALL_MEDIA: bool = False
LINK_PREVIEW_PREFER_LARGE_MEDIA: bool = True
LINK_PREVIEW_SHOW_ABOVE_TEXT: bool = True
SHOW_CAPTION_ABOVE_MEDIA: bool = False
# Разрешения и логирование
BOT_EDIT: bool = True
START_INFO_CONSOLE: bool = True
START_INFO_TO_FILE: bool = True
LOG_CONSOLE: bool = True
LOG_FILE: bool = True
LOG_DIR: Path = Path('Logs')
LOG_FILE_INFO: Path = Path('bot_info.log')
# Вебхук
WEBHOOK: bool = False
WEBHOOK_URL: str = "https://bot.primo.dpdns.org/webhook" # публичный HTTPS url
WEBAPP_HOST: str = "0.0.0.0" # адрес, на котором слушает uvicorn внутри контейнера
WEBAPP_PORT: int = 3131
LOG_LEVEL: str = "warning"
ACCES_LOG: bool = False
# API ключи
API_KEY: Optional[str] = None
WEB_API_KEY: Optional[str] = None
WEATHER_API_KEY: Optional[str] = None
# Пользовательские данные
TG_API_UID: int = 0
TG_API_HASH: Optional[str] = None
# Идентификаторы
ADMIN_ID: list[int] = []
MODERATOR_ID: int = 0
IMPORTANT_ID: int = 0
IMPORTANT_GROUP_ID: int = 0
IMPORTANT_CHANNEL_ID: int = 0
SUPPORT_CHAT_ID: int = 0
# Настройки бота
PROJECT_NAME: str = "PRIMO"
BOT_NAME: str = "Первозданная Жемчужина"
BOT_DESCRIPTION: Optional[str] = None
BOT_SHORT_DESCRIPTION: Optional[str] = None
# Ролевой проект
RP_NAME: Optional[str] = "𝘗𝘳𝘪𝘮𝘰 𝘞𝘰𝘳𝘭𝘥"
INFO_URL: Optional[str] = "https://t.me/PrimoWorldRP"
FLUD_URL: Optional[str] = "https://t.me/PrimoWorldRP"
RP_URL: Optional[str] = "https://t.me/PrimoWorldRP"
LIFE_URL: Optional[str] = "https://t.me/PrimoWorldRP"
RP_OWNER: Optional[str] = None
ROLES: list[str] = ["Альбедо", "Чжун Ли", "Кэйа"]
# Права администратора
ANONYMOUS: bool = False
MANAGE_CHAT: bool = True
CHANGE_INFO: bool = True
PROMOTE_MEMBERS: bool = True
RESTRICT_MEMBERS: bool = True
POST_MESSAGE: bool = True
MANAGE_TOPICS: bool = True
INVITE_USER: bool = True
DELETE_MESSAGES: bool = True
MANAGE_VIDEO_CHATS: bool = True
EDIT_MESSAGES: bool = True
PIN_MESSAGE: bool = True
POST_STORIES: bool = True
EDIT_STORIES: bool = True
DELETE_STORIES: bool = True
# ================= ВАЛИДАТОРЫ =================
@field_validator('PYTHONUNBUFFERED')
def validate_unbuffered(cls, v: str) -> str:
"""Проверка корректности значения буферизации"""
if v not in ('0', '1'):
raise ValueError("PYTHONUNBUFFERED должен быть '0' или '1'")
return v
@field_validator('PARSE_MODE')
def validate_parse_mode(cls, v: str) -> str:
"""Проверка допустимого режима разметки"""
allowed_modes: set[str] = {"HTML", "Markdown", "MarkdownV2"}
if v not in allowed_modes:
raise ValueError(f"Недопустимый PARSE_MODE. Допустимые значения: {', '.join(allowed_modes)}")
return v
@field_validator('PREFIX')
def validate_prefix(cls, v: str) -> str:
"""Очистка и проверка префиксов команд"""
cleaned: str = ''.join(sorted(set(v), key=v.index)) # Удаление дубликатов с сохранением порядка
if len(cleaned) < 1:
raise ValueError("PREFIX должен содержать хотя бы один символ")
return cleaned
@field_validator('LOG_DIR', 'LOG_FILE_INFO', mode='before')
def validate_paths(cls, v: Any) -> Path:
"""Преобразование путей в объекты Path"""
return Path(v) if isinstance(v, str) else v
@field_validator('TG_API_UID', 'MODERATOR_ID')
def validate_ids(cls, v: int) -> int:
"""Проверка корректности идентификаторов"""
if v < 0:
raise ValueError("ID не может быть отрицательным")
return v
@field_validator('WEBHOOK_URL')
def validate_webhook_url(cls, v: str) -> str:
"""Базовая проверка URL вебхука"""
parsed: ParseResult = urlparse(v)
if not all([parsed.scheme, parsed.netloc]):
raise ValueError("Некорректный URL вебхука")
return v
@field_validator('BOT_NAME', 'PROJECT_NAME', 'OWNER')
def validate_non_empty(cls, v: str) -> str:
"""Проверка непустых строк"""
if not v.strip():
raise ValueError("Поле не может быть пустым")
return v
@model_validator(mode='after')
def validate_bot_token(cls, setting: "Settings") -> "Settings":
"""Проверка наличия необходимых токенов"""
if setting.DEBUG and not setting.BOT_DEBUG_TOKEN:
raise ValueError("Требуется BOT_DEBUG_TOKEN в режиме DEBUG")
if not setting.DEBUG and not setting.BOT_TOKEN:
raise ValueError("Требуется BOT_TOKEN для рабочего режима")
return setting
@model_validator(mode='after')
def validate_webhook_config(cls, setting: "Settings") -> "Settings":
"""Проверка конфигурации вебхука"""
if setting.WEBHOOK and not setting.WEBHOOK_URL:
raise ValueError("WEBHOOK_URL обязателен при включенном WEBHOOK")
return setting
@model_validator(mode='after')
def validate_logging_paths(cls, setting: "Settings") -> "Settings":
"""Создание директорий для логов при необходимости"""
if setting.LOG_FILE and not setting.LOG_DIR.exists():
setting.LOG_DIR.mkdir(parents=True, exist_ok=True)
return setting
@model_validator(mode='after')
def set_dynamic_descriptions(cls, setting: "Settings") -> "Settings":
"""Динамическая установка описаний бота"""
if setting.BOT_DESCRIPTION is None:
setting.BOT_DESCRIPTION = f"Ваш помощник в удивительные миры! Prod. by:『{setting.OWNER}"
if setting.BOT_SHORT_DESCRIPTION is None:
setting.BOT_SHORT_DESCRIPTION = f"Тех.поддержка: {setting.OWNER}"
return setting
# ================= СВОЙСТВА =================
@property
def rights(self) -> ChatAdministratorRights:
"""Права администратора бота"""
return ChatAdministratorRights(
is_anonymous=self.ANONYMOUS,
can_manage_chat=self.MANAGE_CHAT,
can_delete_messages=self.DELETE_MESSAGES,
can_manage_video_chats=self.MANAGE_VIDEO_CHATS,
can_restrict_members=self.RESTRICT_MEMBERS,
can_promote_members=self.PROMOTE_MEMBERS,
can_change_info=self.CHANGE_INFO,
can_invite_users=self.INVITE_USER,
can_post_stories=self.POST_STORIES,
can_edit_stories=self.EDIT_STORIES,
can_delete_stories=self.DELETE_STORIES,
can_post_messages=self.POST_MESSAGE,
can_edit_messages=self.EDIT_MESSAGES,
can_pin_messages=self.PIN_MESSAGE,
can_manage_topics=self.MANAGE_TOPICS,
)
@property
def active_bot_token(self) -> str:
"""Активный токен бота в зависимости от режима"""
token = self.BOT_DEBUG_TOKEN if self.DEBUG else self.BOT_TOKEN
if not token:
raise ValueError("Активный токен бота отсутствует")
return token
@property
def log_dir_absolute(self) -> Path:
"""Абсолютный путь к директории логов"""
return self.LOG_DIR.absolute()
# Инициализация настроек
settings: Settings = Settings()
# Классы для обратной совместимости и удобства использования
class BotSettings:
"""Алиасы для настроек бота."""
DEBUG: Final[bool] = settings.DEBUG
OWNER: Final[str] = settings.OWNER
BOT_TOKEN: Final[str] = settings.active_bot_token
PARSE_MODE: Final[str] = settings.PARSE_MODE
ENCOD: Final[str] = settings.ENCOD
TIME_FORMAT: Final[str] = settings.TIME_FORMAT
PREFIX: Final[str] = settings.PREFIX
BOT_LANGUAGE: Final[str] = settings.BOT_LANGUAGE
DISABLE_NOTIFICATION: Final[bool] = settings.DISABLE_NOTIFICATION
PROTECT_CONTENT: Final[bool] = settings.PROTECT_CONTENT
ALLOW_SENDING_WITHOUT_REPLY: Final[bool] = settings.ALLOW_SENDING_WITHOUT_REPLY
LINK_PREVIEW_IS_DISABLED: Final[bool] = settings.LINK_PREVIEW_IS_DISABLED
LINK_PREVIEW_PREFER_SMALL_MEDIA: Final[bool] = settings.LINK_PREVIEW_PREFER_SMALL_MEDIA
LINK_PREVIEW_PREFER_LARGE_MEDIA: Final[bool] = settings.LINK_PREVIEW_PREFER_LARGE_MEDIA
LINK_PREVIEW_SHOW_ABOVE_TEXT: Final[bool] = settings.LINK_PREVIEW_SHOW_ABOVE_TEXT
SHOW_CAPTION_ABOVE_MEDIA: Final[bool] = settings.SHOW_CAPTION_ABOVE_MEDIA
class Permission:
"""Алиасы для разрешений."""
BOT_EDIT: Final[bool] = settings.BOT_EDIT
START_INFO_CONSOLE: Final[bool] = settings.START_INFO_CONSOLE
START_INFO_TO_FILE: Final[bool] = settings.START_INFO_TO_FILE
class LogConfig:
"""Алиасы для конфигурации логов."""
CONSOLE: Final[bool] = settings.LOG_CONSOLE
FILE: Final[bool] = settings.LOG_FILE
DIR: Final[Path] = settings.LOG_DIR
FILE_INFO: Final[Path] = settings.LOG_FILE_INFO
ROTATION: ClassVar[str] = '100 MB'
RETENTION: ClassVar[str] = '7 days'
class Webhook:
"""Алиасы для вебхука."""
WEBHOOK: Final[bool] = settings.WEBHOOK
WEBHOOK_URL: Final[str] = settings.WEBHOOK_URL
WEBHOOK_HOST: Final[str] = settings.WEBAPP_HOST
WEBHOOK_PORT: Final[int] = settings.WEBAPP_PORT
LOG_LEVEL: Final[str] = settings.LOG_LEVEL
ACCES_LOG: Final[bool] = settings.ACCES_LOG
class APISettings:
"""Алиасы для API."""
API_KEY: Final[Optional[str]] = settings.API_KEY
WEB_API_KEY: Final[Optional[str]] = settings.WEB_API_KEY
WEATHER_API_KEY: Final[Optional[str]] = settings.WEATHER_API_KEY
class UserIn:
"""Алиасы для пользовательских данных."""
TG_API_UID: Final[int] = settings.TG_API_UID
TG_API_HASH: Final[Optional[str]] = settings.TG_API_HASH
class ImportantID:
"""Алиасы для важных ID."""
ADMIN_ID: Final[list[int]] = settings.ADMIN_ID
MODERATOR_ID: Final[int] = settings.MODERATOR_ID
IMPORTANT_ID: Final[int] = settings.IMPORTANT_ID
IMPORTANT_GROUP_ID: Final[int] = settings.IMPORTANT_GROUP_ID
IMPORTANT_CHANNEL_ID: Final[int] = settings.IMPORTANT_CHANNEL_ID
SUPPORT_CHAT_ID: Final[int] = settings.SUPPORT_CHAT_ID
class BotEdit:
"""Алиасы для настроек редактирования бота."""
ALLOW_PERMISSION: Final[bool] = settings.BOT_EDIT
PROJECT_NAME: Final[str] = settings.PROJECT_NAME
NAME: Final[str] = settings.BOT_NAME
DESCRIPTION: Final[str] = settings.BOT_DESCRIPTION
SHORT_DESCRIPTION: Final[str] = settings.BOT_SHORT_DESCRIPTION
ANONYMOUS: Final[bool] = settings.ANONYMOUS
MANAGE_CHAT: Final[bool] = settings.MANAGE_CHAT
CHANGE_INFO: Final[bool] = settings.CHANGE_INFO
PROMOTE_MEMBERS: Final[bool] = settings.PROMOTE_MEMBERS
RESTRICT_MEMBERS: Final[bool] = settings.RESTRICT_MEMBERS
POST_MESSAGE: Final[bool] = settings.POST_MESSAGE
MANAGE_TOPICS: Final[bool] = settings.MANAGE_TOPICS
INVITE_USER: Final[bool] = settings.INVITE_USER
DELETE_MESSAGES: Final[bool] = settings.DELETE_MESSAGES
MANAGE_VIDEO_CHATS: Final[bool] = settings.MANAGE_VIDEO_CHATS
EDIT_MESSAGES: Final[bool] = settings.EDIT_MESSAGES
PIN_MESSAGE: Final[bool] = settings.PIN_MESSAGE
POST_STORIES: Final[bool] = settings.POST_STORIES
EDIT_STORIES: Final[bool] = settings.EDIT_STORIES
DELETE_STORIES: Final[bool] = settings.DELETE_STORIES
RIGHTS: Final[ChatAdministratorRights] = settings.rights
class RpValue:
"""Переменные связанные с ролевым проектом."""
RP_NAME: Final[str] = settings.RP_NAME
INFO_URL: str = settings.INFO_URL
FLUD_URL: str = settings.FLUD_URL
RP_URL: str = settings.RP_URL
LIFE_URL: str = settings.LIFE_URL
RP_OWNER: str = settings.RP_OWNER
ROLES: list[str] = settings.ROLES
class Project:
POSTS_DIR: ClassVar[Path] = Path('posts')
class Lists:
"""Интересные списки фактов, цитат и анекдотов."""
facts: list[str] = [
"Python был создан Гвидо ван Россумом в 1991 году.",
"Имена Python и Monty Python связаны — язык назван в честь шоу.",
"Python — язык с динамической типизацией.",
"В Python всё является объектом, даже функции и типы данных.",
"Списки в Python — это изменяемые коллекции, в отличие от кортежей.",
"Python поддерживает парадигмы ООП, функционального и императивного программирования.",
"Zen of Python можно увидеть, набрав `import this` в интерпретаторе.",
]
jokes: list[str] = [
"1",
"2",
"3",
"4",
]
quotes: list[str] = [
"5",
"6",
"7",
"8",
]
# Экспорт совместимых компонентов
__all__ = (
"BotSettings",
"LogConfig",
"Webhook",
"APISettings",
"UserIn",
"ImportantID",
"Permission",
"BotEdit",
"Project",
"RpValue",
'settings',
'Lists',
)

294
configs/roles.py Normal file
View File

@@ -0,0 +1,294 @@
from database import RoleRegion
# Настройка экспорта
__all__ = ("genshin_roles", "hsr_roles", "all_roles",)
genshin_roles: list = [
# Мондштадт
("Альбедо", RoleRegion.MONDSTADT),
("Барбара", RoleRegion.MONDSTADT),
("Беннет", RoleRegion.MONDSTADT),
("Венти", RoleRegion.MONDSTADT),
("Далия", RoleRegion.MONDSTADT),
("Джинн", RoleRegion.MONDSTADT),
("Дилюк", RoleRegion.MONDSTADT),
("Диона", RoleRegion.MONDSTADT),
("Кли", RoleRegion.MONDSTADT),
("Кэйа", RoleRegion.MONDSTADT),
("Лиза", RoleRegion.MONDSTADT),
("Мика", RoleRegion.MONDSTADT),
("Мона", RoleRegion.MONDSTADT),
("Ноэлль", RoleRegion.MONDSTADT),
("Розария", RoleRegion.MONDSTADT),
("Рэйзор", RoleRegion.MONDSTADT),
("Сахароза", RoleRegion.MONDSTADT),
("Фишль", RoleRegion.MONDSTADT),
("Эмбер", RoleRegion.MONDSTADT),
("Эола", RoleRegion.MONDSTADT),
# Ли Юэ
("Бай Чжу", RoleRegion.LIYUE),
("Бэй Доу", RoleRegion.LIYUE),
("Гань Юй", RoleRegion.LIYUE),
("Е Лань", RoleRegion.LIYUE),
("Ка Мин", RoleRegion.LIYUE),
("Кэ Цин", RoleRegion.LIYUE),
("Лань Янь", RoleRegion.LIYUE),
("Нин Гуан", RoleRegion.LIYUE),
("Син Цю", RoleRegion.LIYUE),
("Синь Янь", RoleRegion.LIYUE),
("Сян Лин", RoleRegion.LIYUE),
("Сянь Юнь", RoleRegion.LIYUE),
("Сяо", RoleRegion.LIYUE),
("Ху Тао", RoleRegion.LIYUE),
("Ци Ци", RoleRegion.LIYUE),
("Чжун Ли", RoleRegion.LIYUE),
("Чун Юнь", RoleRegion.LIYUE),
("Шэнь Хэ", RoleRegion.LIYUE),
("Юнь Цзинь", RoleRegion.LIYUE),
("Янь Фэй", RoleRegion.LIYUE),
("Яо Яо", RoleRegion.LIYUE),
# Инадзума
("Аяка", RoleRegion.INAZUMA),
("Аято", RoleRegion.INAZUMA),
("Горо", RoleRegion.INAZUMA),
("Ёимия", RoleRegion.INAZUMA),
("Итто", RoleRegion.INAZUMA),
("Кадзуха", RoleRegion.INAZUMA),
("Кирара", RoleRegion.INAZUMA),
("Кокоми", RoleRegion.INAZUMA),
("Мидзуки", RoleRegion.INAZUMA),
("Райдэн Макото", RoleRegion.INAZUMA),
("Райдэн Эи", RoleRegion.INAZUMA),
("Сара", RoleRegion.INAZUMA),
("Саю", RoleRegion.INAZUMA),
("Синобу", RoleRegion.INAZUMA),
("Тиори", RoleRegion.INAZUMA),
("Тома", RoleRegion.INAZUMA),
("Хэйдзо", RoleRegion.INAZUMA),
("Яэ Мико", RoleRegion.INAZUMA),
# Сумеру
("Аль-Хайтам", RoleRegion.SUMERU),
("Дори", RoleRegion.SUMERU),
("Дэхья", RoleRegion.SUMERU),
("Кавех", RoleRegion.SUMERU),
("Кандакия", RoleRegion.SUMERU),
("Коллеи", RoleRegion.SUMERU),
("Лайла", RoleRegion.SUMERU),
("Нахида", RoleRegion.SUMERU),
("Нилу", RoleRegion.SUMERU),
("Руккхадевата", RoleRegion.SUMERU),
("Сайно", RoleRegion.SUMERU),
("Сетос", RoleRegion.SUMERU),
("Странник", RoleRegion.SUMERU),
("Тигнари", RoleRegion.SUMERU),
("Фарузан", RoleRegion.SUMERU),
# Фонтейн
("Клоринда", RoleRegion.FONTAINE),
("Линетт", RoleRegion.FONTAINE),
("Лини", RoleRegion.FONTAINE),
("Навия", RoleRegion.FONTAINE),
("Нёвиллет", RoleRegion.FONTAINE),
("Ризли", RoleRegion.FONTAINE),
("Сиджвин", RoleRegion.FONTAINE),
("Фокалорс", RoleRegion.FONTAINE),
("Фремине", RoleRegion.FONTAINE),
("Фурина", RoleRegion.FONTAINE),
("Шарлотта", RoleRegion.FONTAINE),
("Шеврёз", RoleRegion.FONTAINE),
("Эмилия", RoleRegion.FONTAINE),
("Эскофье", RoleRegion.FONTAINE),
# Натлан
("Ахав", RoleRegion.NATLAN),
("Вареса", RoleRegion.NATLAN),
("Иансан", RoleRegion.NATLAN),
("Ифа", RoleRegion.NATLAN),
("Качина", RoleRegion.NATLAN),
("Кинич", RoleRegion.NATLAN),
("Мавуика", RoleRegion.NATLAN),
("Муалани", RoleRegion.NATLAN),
("Оророн", RoleRegion.NATLAN),
("Ситлали", RoleRegion.NATLAN),
("Часка", RoleRegion.NATLAN),
("Шилонен", RoleRegion.NATLAN),
# Снежная
("Арлекино", RoleRegion.SNEZHNAYA),
("Дотторе", RoleRegion.SNEZHNAYA),
("Капитано", RoleRegion.SNEZHNAYA),
("Коломбина", RoleRegion.SNEZHNAYA),
("Панталоне", RoleRegion.SNEZHNAYA),
("Пульчинелла", RoleRegion.SNEZHNAYA),
("Пьеро", RoleRegion.SNEZHNAYA),
("Сандроне", RoleRegion.SNEZHNAYA),
("Синьора", RoleRegion.SNEZHNAYA),
("Царица", RoleRegion.SNEZHNAYA),
("Тарталья", RoleRegion.SNEZHNAYA),
# Каэнри'ах
("Айно", RoleRegion.KHAENRIAH),
("Алиса", RoleRegion.KHAENRIAH),
("Варка", RoleRegion.KHAENRIAH),
("Дурин", RoleRegion.KHAENRIAH),
("Инеффа", RoleRegion.KHAENRIAH),
("Лаума", RoleRegion.KHAENRIAH),
("Нефер", RoleRegion.KHAENRIAH),
("Николь", RoleRegion.KHAENRIAH),
("Флинс", RoleRegion.KHAENRIAH),
("Ягода", RoleRegion.KHAENRIAH),
# Другие (Genshin Impact)
("Дайнслейф", RoleRegion.GENSHIN_OTHER),
("Итэр", RoleRegion.GENSHIN_OTHER),
("Люмин", RoleRegion.GENSHIN_OTHER),
("Паймон", RoleRegion.GENSHIN_OTHER),
("Рэйндоттир", RoleRegion.GENSHIN_OTHER),
("Скирк", RoleRegion.GENSHIN_OTHER),
("Элой", RoleRegion.GENSHIN_OTHER),
]
# Роли для Honkai: Star Rail
hsr_roles: list = [
# Звездный экспресс
("Вельт", RoleRegion.HSR_STAR),
("Дань Хэн", RoleRegion.HSR_STAR),
("Келус", RoleRegion.HSR_STAR),
("Март 7", RoleRegion.HSR_STAR),
("Стелла", RoleRegion.HSR_STAR),
("Химеко", RoleRegion.HSR_STAR),
# Космическая станция Герта
("Арлан", RoleRegion.HSR_GERTA),
("Аста", RoleRegion.HSR_GERTA),
("Великая Герта", RoleRegion.HSR_GERTA),
("Жуань Мэй", RoleRegion.HSR_GERTA),
("Полька Какамонд", RoleRegion.HSR_GERTA),
("Скрюллум", RoleRegion.HSR_GERTA),
("Стивен Ллойд", RoleRegion.HSR_GERTA),
# Ярило-VI
("Броня", RoleRegion.HSR_YARILO),
("Гепард", RoleRegion.HSR_YARILO),
("Зеле", RoleRegion.HSR_YARILO),
("Клара", RoleRegion.HSR_YARILO),
("Коколия", RoleRegion.HSR_YARILO),
("Лука", RoleRegion.HSR_YARILO),
("Наташа", RoleRegion.HSR_YARILO),
("Пела", RoleRegion.HSR_YARILO),
("Рысь", RoleRegion.HSR_YARILO),
("Сампо", RoleRegion.HSR_YARILO),
("Сервал", RoleRegion.HSR_YARILO),
("Хук", RoleRegion.HSR_YARILO),
# Лофу Сяньчжоу
("Байлу", RoleRegion.HSR_LOFU),
("Байхэн", RoleRegion.HSR_LOFU),
("Гуйнайфэнь", RoleRegion.HSR_LOFU),
("Линша", RoleRegion.HSR_LOFU),
("Лоча", RoleRegion.HSR_LOFU),
("Моцзэ", RoleRegion.HSR_LOFU),
("Сушан", RoleRegion.HSR_LOFU),
("Сюэи", RoleRegion.HSR_LOFU),
("Фу Сюань", RoleRegion.HSR_LOFU),
("Фуга", RoleRegion.HSR_LOFU),
("Фэйсяо", RoleRegion.HSR_LOFU),
("Ханья", RoleRegion.HSR_LOFU),
("Хохо", RoleRegion.HSR_LOFU),
("Цзинлю", RoleRegion.HSR_LOFU),
("Цзин Юань", RoleRegion.HSR_LOFU),
("Цзяоцю", RoleRegion.HSR_LOFU),
("Цинцюэ", RoleRegion.HSR_LOFU),
("Юйкун", RoleRegion.HSR_LOFU),
("Юньли", RoleRegion.HSR_LOFU),
("Яньцин", RoleRegion.HSR_LOFU),
# Пенакония
("Ахерон", RoleRegion.HSR_PENACONY),
("Воскресенье", RoleRegion.HSR_PENACONY),
("Галлахер", RoleRegion.HSR_PENACONY),
("Мистер Река", RoleRegion.HSR_PENACONY),
("Зарянка", RoleRegion.HSR_PENACONY),
("Искорка", RoleRegion.HSR_PENACONY),
("Миша", RoleRegion.HSR_PENACONY),
("Рацио", RoleRegion.HSR_PENACONY),
("Чёрный Лебедь", RoleRegion.HSR_PENACONY),
# Амфореус
("Аглая", RoleRegion.HSR_AMPHOREUS),
("Анаксагор", RoleRegion.HSR_AMPHOREUS),
("Гиацина", RoleRegion.HSR_AMPHOREUS),
("Гисиленса", RoleRegion.HSR_AMPHOREUS),
("Кастория", RoleRegion.HSR_AMPHOREUS),
("Керидра", RoleRegion.HSR_AMPHOREUS),
("Кирена", RoleRegion.HSR_AMPHOREUS),
("Ликург", RoleRegion.HSR_AMPHOREUS),
("Мидей", RoleRegion.HSR_AMPHOREUS),
("Трибби", RoleRegion.HSR_AMPHOREUS),
("Фаенон", RoleRegion.HSR_AMPHOREUS),
("Цифер", RoleRegion.HSR_AMPHOREUS),
# Охотники за Стеллар
("Блэйд", RoleRegion.HSR_HUNTER),
("Кафка", RoleRegion.HSR_HUNTER),
("Светлячок", RoleRegion.HSR_HUNTER),
("Серебряный Волк", RoleRegion.HSR_HUNTER),
("Элио", RoleRegion.HSR_HUNTER),
# КММ
("Авантюрин", RoleRegion.HSR_KMM),
("Агат", RoleRegion.HSR_KMM),
("Алмаз", RoleRegion.HSR_KMM),
("Обсидиан", RoleRegion.HSR_KMM),
("Опал", RoleRegion.HSR_KMM),
("Перламутр", RoleRegion.HSR_KMM),
("Сапфир", RoleRegion.HSR_KMM),
("Сугилит", RoleRegion.HSR_KMM),
("Топаз", RoleRegion.HSR_KMM),
("Янтарь", RoleRegion.HSR_KMM),
("Яшма", RoleRegion.HSR_KMM),
# Эоны
("Акивили", RoleRegion.HSR_EONS),
("Аха", RoleRegion.HSR_EONS),
("Клипот", RoleRegion.HSR_EONS),
("Лань", RoleRegion.HSR_EONS),
("Нанук", RoleRegion.HSR_EONS),
("Нус", RoleRegion.HSR_EONS),
("Ороборос", RoleRegion.HSR_EONS),
("Тайззиронт", RoleRegion.HSR_EONS),
("Фили", RoleRegion.HSR_EONS),
("Шипе", RoleRegion.HSR_EONS),
("Эна", RoleRegion.HSR_EONS),
("Яоши", RoleRegion.HSR_EONS),
("IX", RoleRegion.HSR_EONS),
# Вечногорящий особняк
("Акаш", RoleRegion.HSR_FIRE_MANSION),
("Герцог Инферно", RoleRegion.HSR_FIRE_MANSION),
("Дубра", RoleRegion.HSR_FIRE_MANSION),
("Катерина", RoleRegion.HSR_FIRE_MANSION),
("Констанция", RoleRegion.HSR_FIRE_MANSION),
# Лорды Опустошители
("Асат Прамад", RoleRegion.HSR_LORDS),
("Зефиро", RoleRegion.HSR_LORDS),
("Оростелла", RoleRegion.HSR_LORDS),
("Фантилия", RoleRegion.HSR_LORDS),
# Прочие (Honkai: Star Rail)
("Аргенти", RoleRegion.HSR_OTHER),
("Бутхилл", RoleRegion.HSR_OTHER),
("Раппа", RoleRegion.HSR_OTHER),
("Архив Пустоты", RoleRegion.HSR_OTHER),
# Фейт
("Арчер", RoleRegion.HSR_FATE),
("Сейбер", RoleRegion.HSR_FATE),
]
# Общий список ролей
all_roles: list = genshin_roles + hsr_roles

1
database/__init__.py Normal file
View File

@@ -0,0 +1 @@
from .database import *

1171
database/database.py Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -0,0 +1,58 @@
# English translations for Bot Super Project.
# Copyright (C) 2025 ORGANIZATION
# This file is distributed under the same license as the Bot Super Project
# project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2025.
#
msgid ""
msgstr ""
"Project-Id-Version: Bot Super Project 0.1\n"
"Report-Msgid-Bugs-To: john@doe-email.com\n"
"POT-Creation-Date: 2024-01-12 16:11+0500\n"
"PO-Revision-Date: 2025-08-10 19:33+0700\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: en\n"
"Language-Team: en <LL@li.org>\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.17.0\n"
#: bot/handlers/commands/lang.py:43
msgid "Выберите язык:"
msgstr ""
#: bot/handlers/commands/lang.py:50
#, python-brace-format
msgid "Язык {lang} не поддерживается!"
msgstr ""
#: bot/handlers/commands/start.py:29
msgid "Создать пост📔"
msgstr ""
#: bot/handlers/commands/start.py:30
msgid "Посмотреть список📋"
msgstr ""
#: bot/handlers/commands/start.py:31
msgid "Изменить язык🌐"
msgstr ""
#: bot/handlers/commands/start.py:35
#, python-brace-format
msgid ""
"Добро пожаловать, <a href=\"{url}\">{name}</a>!\n"
"\n"
"Мое имя - <b>{bot_name}</b>! Я искусственный интеллект и сказитель ваших "
"историй! \n"
"Моя цель — помочь вам сориентироваться и сделать ваши истории куда "
"интереснее! \n"
"Надеюсь, я смогу вам помочь! Пожалуйста, выберите нужную функцию на "
"клавиатуре!\n"
"\n"
"Интересный факт:\n"
"<blockquote>{fact}</blockquote>\n"
msgstr ""

56
locales/messages.pot Normal file
View File

@@ -0,0 +1,56 @@
# Translations template for PROJECT.
# Copyright (C) 2025 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2025.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: Bot Super Project 0.1\n"
"Report-Msgid-Bugs-To: john@doe-email.com\n"
"POT-Creation-Date: 2024-01-12 16:11+0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.13.1\n"
#: bot/handlers/commands/lang.py:43
msgid "Выберите язык:"
msgstr ""
#: bot/handlers/commands/lang.py:50
#, python-brace-format
msgid "Язык {lang} не поддерживается!"
msgstr ""
#: bot/handlers/commands/start.py:29
msgid "Создать пост📔"
msgstr ""
#: bot/handlers/commands/start.py:30
msgid "Посмотреть список📋"
msgstr ""
#: bot/handlers/commands/start.py:31
msgid "Изменить язык🌐"
msgstr ""
#: bot/handlers/commands/start.py:35
#, python-brace-format
msgid ""
"Добро пожаловать, <a href=\"{url}\">{name}</a>!\n"
"\n"
"Мое имя - <b>{bot_name}</b>! Я искусственный интеллект и сказитель ваших "
"историй! \n"
"Моя цель — помочь вам сориентироваться и сделать ваши истории куда "
"интереснее! \n"
"Надеюсь, я смогу вам помочь! Пожалуйста, выберите нужную функцию на "
"клавиатуре!\n"
"\n"
"Интересный факт:\n"
"<blockquote>{fact}</blockquote>\n"
msgstr ""

Binary file not shown.

View File

@@ -0,0 +1,59 @@
# Russian translations for Bot Super Project.
# Copyright (C) 2025 ORGANIZATION
# This file is distributed under the same license as the Bot Super Project
# project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2025.
#
msgid ""
msgstr ""
"Project-Id-Version: Bot Super Project 0.1\n"
"Report-Msgid-Bugs-To: john@doe-email.com\n"
"POT-Creation-Date: 2024-01-12 16:11+0500\n"
"PO-Revision-Date: 2025-08-10 19:33+0700\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: ru\n"
"Language-Team: ru <LL@li.org>\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.17.0\n"
#: bot/handlers/commands/lang.py:43
msgid "Выберите язык:"
msgstr ""
#: bot/handlers/commands/lang.py:50
#, python-brace-format
msgid "Язык {lang} не поддерживается!"
msgstr ""
#: bot/handlers/commands/start.py:29
msgid "Создать пост📔"
msgstr ""
#: bot/handlers/commands/start.py:30
msgid "Посмотреть список📋"
msgstr ""
#: bot/handlers/commands/start.py:31
msgid "Изменить язык🌐"
msgstr ""
#: bot/handlers/commands/start.py:35
#, python-brace-format
msgid ""
"Добро пожаловать, <a href=\"{url}\">{name}</a>!\n"
"\n"
"Мое имя - <b>{bot_name}</b>! Я искусственный интеллект и сказитель ваших "
"историй! \n"
"Моя цель — помочь вам сориентироваться и сделать ваши истории куда "
"интереснее! \n"
"Надеюсь, я смогу вам помочь! Пожалуйста, выберите нужную функцию на "
"клавиатуре!\n"
"\n"
"Интересный факт:\n"
"<blockquote>{fact}</blockquote>\n"
msgstr ""

Binary file not shown.

View File

@@ -0,0 +1,59 @@
# Ukrainian translations for Bot Super Project.
# Copyright (C) 2025 ORGANIZATION
# This file is distributed under the same license as the Bot Super Project
# project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2025.
#
msgid ""
msgstr ""
"Project-Id-Version: Bot Super Project 0.1\n"
"Report-Msgid-Bugs-To: john@doe-email.com\n"
"POT-Creation-Date: 2024-01-12 16:11+0500\n"
"PO-Revision-Date: 2025-08-10 19:33+0700\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: uk\n"
"Language-Team: uk <LL@li.org>\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.17.0\n"
#: bot/handlers/commands/lang.py:43
msgid "Выберите язык:"
msgstr ""
#: bot/handlers/commands/lang.py:50
#, python-brace-format
msgid "Язык {lang} не поддерживается!"
msgstr ""
#: bot/handlers/commands/start.py:29
msgid "Создать пост📔"
msgstr ""
#: bot/handlers/commands/start.py:30
msgid "Посмотреть список📋"
msgstr ""
#: bot/handlers/commands/start.py:31
msgid "Изменить язык🌐"
msgstr ""
#: bot/handlers/commands/start.py:35
#, python-brace-format
msgid ""
"Добро пожаловать, <a href=\"{url}\">{name}</a>!\n"
"\n"
"Мое имя - <b>{bot_name}</b>! Я искусственный интеллект и сказитель ваших "
"историй! \n"
"Моя цель — помочь вам сориентироваться и сделать ваши истории куда "
"интереснее! \n"
"Надеюсь, я смогу вам помочь! Пожалуйста, выберите нужную функцию на "
"клавиатуре!\n"
"\n"
"Интересный факт:\n"
"<blockquote>{fact}</blockquote>\n"
msgstr ""

75
main.py Normal file
View File

@@ -0,0 +1,75 @@
import asyncio
import sys
from bot import dp, bot, BotInfo, WebhookApp, setup_middlewares, router
from configs.config import Webhook
from database import db
from middleware import setup_logging
async def on_startup() -> None:
"""Действия при запуске бота."""
setup_logging()
# Создание базы данных
await db.init_db()
if not await db.check_connection():
print("Не удалось подключиться к БД!")
return
await db.init_default_roles()
# Настройка информации о боте
await BotInfo.setup(bots=bot)
# Настройка middleware
setup_middlewares(
dp=dp,
bot=bot,
channel_ids=[],
)
# Подключение роутеров
dp.include_router(router)
BotInfo.start_info_out()
async def run_polling() -> None:
"""Запуск в режиме polling."""
try:
await on_startup()
await dp.start_polling(bot, allowed_updates=dp.resolve_used_update_types())
finally:
await bot.session.close()
async def run_webhook() -> None:
"""Запуск в режиме webhook."""
app: WebhookApp = WebhookApp(host=Webhook.WEBHOOK_HOST, port=Webhook.WEBHOOK_PORT)
try:
await on_startup()
await app.start()
# держим процесс живым
while True:
await asyncio.sleep(3600)
finally:
await app.stop()
await bot.session.close()
async def main() -> None:
# Запуск в нужном режиме
if Webhook.WEBHOOK:
await run_webhook()
else:
await run_polling()
if __name__ == "__main__":
# Защита для Windows
if sys.platform.startswith("win"):
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
try:
asyncio.run(main())
except (KeyboardInterrupt, SystemExit):
print("❌ Бот остановлен!")
sys.exit(0)

2
middleware/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
from .loggers import *
from .validators import *

View File

@@ -0,0 +1 @@
from .logs import *

234
middleware/loggers/logs.py Normal file
View File

@@ -0,0 +1,234 @@
from sys import stderr
from pathlib import Path
from functools import wraps
from inspect import iscoroutinefunction
from typing import Any, Callable, Optional, TypeVar, cast, Final
from loguru import logger
from aiogram.types import Message, User
from configs.config import BotEdit, LogConfig
# Экспортируемые объекты
__all__ = ('Logger', 'setup_logging', 'loggers', 'log',)
# Универсальный тип для функций
F: TypeVar = TypeVar('F', bound=Callable[..., Any])
class Logger:
"""
Кастомный логгер с поддержкой декораторов и прямого вызова.
Attributes:
system_name: Имя системы для логирования
_log_format: Формат логов
"""
_log_format: Final[str] = (
'<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> <red>|</red> '
'<blue>{extra[system]}-{extra[log_type]}</blue> <red>| '
'{extra[user]} |</red> <level>{message}</level>'
)
def __init__(self, system_name: str = BotEdit.PROJECT_NAME) -> None:
"""
Инициализация логгера.
:param system_name: Имя системы для логирования
"""
self.system_name = system_name
self._setup_done = False
def setup(self, start: bool = True) -> None:
"""
Настройка обработчиков Loguru: консоль и файлы.
:param start: Если True, сразу логирует запуск проекта
"""
if self._setup_done:
return
# Полная очистка настроек
logger.remove()
# Создание директории для файловых логов
log_dir: Path = Path(getattr(LogConfig, 'DIR', 'logs'))
log_dir.mkdir(parents=True, exist_ok=True)
# Консольный лог
if getattr(LogConfig, 'CONSOLE', False):
logger.add(
sink=stderr,
format=self._log_format,
colorize=True,
level='DEBUG',
filter=lambda rec: rec['extra'].get('log_type') != 'DEBUG'
)
# Файловые логи
if getattr(LogConfig, 'FILE', False):
# Общий лог
logger.add(
sink=log_dir / 'bot.log',
rotation=getattr(LogConfig, 'ROTATION', '100 MB'),
retention=getattr(LogConfig, 'RETENTION', '7 days'),
format=self._log_format,
level='DEBUG',
enqueue=True,
backtrace=True,
diagnose=True
)
# Раздельные логи по уровням
for level_name in ['INFO', 'WARNING', 'ERROR', 'DEBUG', 'CRITICAL']:
logger.add(
sink=log_dir / f'{level_name.lower()}.log',
rotation='10 MB',
retention='7 days',
format=self._log_format,
level=level_name,
filter=lambda rec, lvl=level_name: rec['level'].name == lvl,
enqueue=True
)
self._setup_done = True
# Логируем старт
if start:
self.log_entry(
level='INFO',
text='Запуск проекта...',
log_type='START'
)
@staticmethod
def format_user(message: Optional[Message] = None) -> str:
"""
Форматирует имя пользователя из объекта Message.
:param message: Объект aiogram.types.Message
:return: Строка '@username' или 'id<user_id>'
"""
if message is None or message.from_user is None:
return '@System'
user: User = message.from_user
return f"@{user.username}" if user.username else f"id{user.id}"
def log_entry(
self,
level: str,
text: str,
log_type: str,
user: Optional[str] = None,
message: Optional[Message] = None
) -> None:
"""
Основной метод для записи логов.
:param level: Уровень логирования (например, 'INFO')
:param text: Сообщение для логирования
:param log_type: Кастомный тип лога (например, 'HANDLER')
:param user: Явно указанный пользователь
:param message: Объект Message для извлечения юзера
"""
actual_user: str = user or self.format_user(message)
logger.bind(
system=self.system_name,
user=actual_user,
log_type=log_type
).log(level, text)
def log(
self,
level: str = 'INFO',
log_type: str = '',
text: Optional[str] = None
) -> Callable[[F], F]:
"""
Декоратор для логирования функций.
:param level: Уровень логирования
:param log_type: Категория лога
:param text: Кастомный текст сообщения
:return: Декорированную функцию
"""
def decorator(func: F) -> F:
is_coroutine = iscoroutinefunction(func)
action_text = text or f'Вызов {func.__name__}'
@wraps(func)
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
message = self._find_message(args)
self.log_entry(level, f"[START] {action_text}", log_type, message=message)
try:
result = func(*args, **kwargs)
self.log_entry(level, f"[SUCCESS] {action_text}", log_type, message=message)
return result
except Exception as e:
self.log_entry(
'ERROR',
f"[ERROR] {action_text} | Exception: {e!r}",
log_type,
message=message
)
raise
@wraps(func)
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
message = self._find_message(args)
self.log_entry(level, f"[START] {action_text}", log_type, message=message)
try:
result = await func(*args, **kwargs)
self.log_entry(level, f"[SUCCESS] {action_text}", log_type, message=message)
return result
except Exception as e:
self.log_entry(
'ERROR',
f"[ERROR] {action_text} | Exception: {e!r}",
log_type,
message=message
)
raise
return cast(F, async_wrapper if is_coroutine else sync_wrapper)
return decorator
@staticmethod
def _find_message(args: tuple[Any, ...]) -> Optional[Message]:
"""
Ищет объект Message в аргументах функции.
:param args: Аргументы функции
:return: Найденный Message или None
"""
return next((arg for arg in args if isinstance(arg, Message)), None)
# Методы для прямого вызова
def debug(self, text: str, log_type: str = 'BOT', user: Optional[str] = None,
message: Optional[Message] = None) -> None:
self.log_entry('DEBUG', text, log_type, user, message)
def info(self, text: str, log_type: str = 'BOT', user: Optional[str] = None,
message: Optional[Message] = None) -> None:
self.log_entry('INFO', text, log_type, user, message)
def warning(self, text: str, log_type: str = 'BOT', user: Optional[str] = None,
message: Optional[Message] = None) -> None:
self.log_entry('WARNING', text, log_type, user, message)
def error(self, text: str, log_type: str = 'BOT', user: Optional[str] = None,
message: Optional[Message] = None) -> None:
self.log_entry('ERROR', text, log_type, user, message)
def critical(self, text: str, log_type: str = 'BOT', user: Optional[str] = None,
message: Optional[Message] = None) -> None:
self.log_entry('CRITICAL', text, log_type, user, message)
# Создаем глобальный экземпляр логгера
loggers: Logger = Logger()
# Экспортируемые функции для обратной совместимости
setup_logging = loggers.setup
log = loggers.log

View File

@@ -0,0 +1,2 @@
from .email_vld import *
from .url_vld import *

View File

@@ -0,0 +1,24 @@
from typing import Optional
from email_validator import validate_email, EmailNotValidError, ValidatedEmail
# Настройка экспорта из этого модуля
__all__ = ("valid_email",)
def valid_email(e_mail: str) -> Optional[str]:
"""
Валидация почты через библиотеку.
:param e_mail: Получаемая почта.
:return: Нормализированная почта.
"""
try:
# Провека почты на валидность
email: ValidatedEmail = validate_email(e_mail)
except EmailNotValidError:
return None
# Возвращение строки с нормализированной почтой
return email.normalized

View File

@@ -0,0 +1,42 @@
from re import Pattern, compile
# Настройка экспорта
__all__ = ("valid_url", "url_to_text",)
def valid_url(url: str) -> bool:
"""
Проверяет, является ли строка валидной ссылкой (URL).
:param url: Строка для проверки.
:return: True, если строка является валидным URL, иначе False.
"""
url_pattern: Pattern[str] = compile(
r'^(https?://)?' # Протокол (http или https, необязателен)
r'([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}' # Домен
r'(:\d+)?' # Порт (необязателен)
r'(/[-a-zA-Z0-9@:%_+.~#?&/=]*)?$' # Путь, параметры и фрагменты
)
return bool(url_pattern.match(url))
def url_to_text(text: str, url: str) -> str:
"""
Преобразует текст в HTML ссылку с указанным URL.
Эта функция генерирует HTML-ссылку с переданным текстом и URL, используя тег `<а>`, и делает ссылку жирной.
:param text: Текст, который будет отображаться для ссылки.
:param url: URL, который будет привязан к тексту.
:return: Строка с HTML кодом для ссылки, если URL валиден.
:raises ValueError: Если URL невалиден.
"""
try:
if not valid_url(url): # Проверяем, является ли URL валидным
raise ValueError(f"Переданный URL '{url}' невалиден.")
# Генерация HTML-ссылки
return f'<b><a href="{url}">{text}</a></b>'
except ValueError as e:
raise e # Перебрасываем ошибку выше для дальнейшей обработки или уведомления

42
pyproject.toml Normal file
View File

@@ -0,0 +1,42 @@
[project]
name = "primoexamplebot"
version = "0.1.0"
description = "none"
authors = [
{name = "admin",email = "inkscaper0349@outlook.com"}
]
license = {text = "MIT License"}
readme = "README.md"
requires-python = ">=3.10,<4.0"
dependencies = [
"aiogram (>=3.22.0,<4.0.0)",
"loguru (>=0.7.3,<0.8.0)",
"pydantic-settings (>=2.10.1,<3.0.0)",
"sqlalchemy (>=2.0.43,<3.0.0)",
"babel (>=2.17.0,<3.0.0)",
"aiosqlite (>=0.21.0,<0.22.0)",
"email-validator (>=2.3.0,<3.0.0)",
"apscheduler (>=3.11.0,<4.0.0)",
"fastapi (>=0.116.1,<0.117.0)",
"uvicorn (>=0.35.0,<0.36.0)",
]
[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.poetry]
package-mode = false
[tool.poetry.group.dev.dependencies]
pytest = "^8.4.1"
pytest-asyncio = "^1.1.0"
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
python_files = "test_*.py"
python_classes = "Test*"
python_functions = "test_*"