v1.2.1
This commit is contained in:
@@ -1,36 +1,39 @@
|
|||||||
# .dockerignore: Исключения для Docker сборки
|
# Исключить скрытые системные каталоги, но не всё подряд
|
||||||
# Игнорировать всё, кроме необходимого для production
|
.git/
|
||||||
|
.gitattributes
|
||||||
|
.gitignore
|
||||||
|
|
||||||
**/.git
|
# Виртуальные окружения и Python-кэш
|
||||||
**/.gitignore
|
.venv/
|
||||||
**/.dockerignore
|
__pycache__/
|
||||||
**/Dockerfile
|
*.py[cod]
|
||||||
**/README.md
|
*.pyo
|
||||||
|
|
||||||
# Директории
|
# IDE-файлы
|
||||||
**/__pycache__
|
.idea/
|
||||||
**/.mypy_cache
|
.vscode/
|
||||||
**/.pytest_cache
|
|
||||||
**/.idea
|
|
||||||
**/.vscode
|
|
||||||
**/test
|
|
||||||
**/tests
|
|
||||||
**/docs
|
|
||||||
**/examples
|
|
||||||
|
|
||||||
# Файлы
|
# Тесты и документация
|
||||||
**/*.pyc
|
tests/
|
||||||
**/*.pyo
|
test/
|
||||||
**/*.pyd
|
docs/
|
||||||
**/*.egg-info
|
examples/
|
||||||
**/*.log
|
|
||||||
**/*.logs
|
|
||||||
**/*.sqlite
|
|
||||||
**/*.db
|
|
||||||
config/.env
|
|
||||||
**/docker-compose*
|
|
||||||
|
|
||||||
# Артефакты сборки
|
# Логи и артефакты сборки
|
||||||
**/build
|
*.log
|
||||||
**/dist
|
*.logs
|
||||||
**/node_modules
|
Logs/
|
||||||
|
Log/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Примеры и шаблоны
|
||||||
|
env_example
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Опционально (если не нужны в образе):
|
||||||
|
docker-compose.yml
|
||||||
|
poetry.lock
|
||||||
|
pyproject.toml
|
||||||
|
README.md
|
||||||
|
LICENSE
|
||||||
|
|||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -3,8 +3,8 @@
|
|||||||
|
|
||||||
### Python ###
|
### Python ###
|
||||||
# Виртуальные окружения и настройки
|
# Виртуальные окружения и настройки
|
||||||
config/.env
|
configs/.env
|
||||||
../../../../Desktop/PostBot/.venv
|
.venv
|
||||||
venv/
|
venv/
|
||||||
env/
|
env/
|
||||||
ENV/
|
ENV/
|
||||||
@@ -65,3 +65,4 @@ htmlcov/
|
|||||||
.nox/
|
.nox/
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
.mypy_cache/
|
.mypy_cache/
|
||||||
|
/.env
|
||||||
|
|||||||
12
.idea/PRIMOSTORYFINAL.iml
generated
12
.idea/PRIMOSTORYFINAL.iml
generated
@@ -6,9 +6,19 @@
|
|||||||
<sourceFolder url="file://$MODULE_DIR$/BotCode/handlers/commands" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/BotCode/handlers/commands" isTestSource="false" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/BotCode/loggers" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/BotCode/loggers" isTestSource="false" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/BotCode/utils" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/BotCode/utils" isTestSource="false" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/bot" isTestSource="false" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/configs" isTestSource="true" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/middleware" isTestSource="false" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||||
</content>
|
</content>
|
||||||
<orderEntry type="jdk" jdkName="Python 3.13 (PRIMOSTORYFINAL)" jdkType="Python SDK" />
|
<orderEntry type="jdk" jdkName="Python 3.13 (PrimoStoryBot)" jdkType="Python SDK" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
</component>
|
</component>
|
||||||
|
<component name="TemplatesService">
|
||||||
|
<option name="TEMPLATE_FOLDERS">
|
||||||
|
<list>
|
||||||
|
<option value="$MODULE_DIR$/locales" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
</module>
|
</module>
|
||||||
111
.idea/workspace.xml
generated
111
.idea/workspace.xml
generated
@@ -5,40 +5,57 @@
|
|||||||
</component>
|
</component>
|
||||||
<component name="ChangeListManager">
|
<component name="ChangeListManager">
|
||||||
<list default="true" id="976a6336-6952-45ae-989f-7b10c5e394d4" name="Changes" comment="">
|
<list default="true" id="976a6336-6952-45ae-989f-7b10c5e394d4" name="Changes" comment="">
|
||||||
<change afterPath="$PROJECT_DIR$/.dockerignore" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/bot/bots.py" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/.env" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/bot/core/__init__.py" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/.gitattributes" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/bot/handlers/commands/__init__.py" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/.gitignore" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/bot/handlers/commands/help.py" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/.idea/PRIMOSTORYFINAL.iml" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/bot/handlers/commands/start.py" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/.idea/inspectionProfiles/profiles_settings.xml" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/bot/handlers/post/__init__.py" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/.idea/modules.xml" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/bot/handlers/post/create_posts.py" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/.idea/vcs.xml" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/bot/keyboards/__init__.py" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/bot/keyboards/inline/__init__.py" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/BotCode/__init__.py" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/bot/keyboards/inline/decision.py" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/BotCode/config.py" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/bot/keyboards/reply/__init__.py" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/BotCode/core/__init__.py" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/bot/templates/__init__.py" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/BotCode/core/storage.py" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/bot/templates/message_callback.py" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/BotCode/handlers/__init__.py" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/bot/utils/interesting_facts.py" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/BotCode/handlers/callback.py" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/bot/utils/pagination.py" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/BotCode/handlers/commands/__init__.py" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/configs/__init__.py" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/BotCode/handlers/commands/start_cmd.py" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/configs/cmd_list.py" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/BotCode/handlers/inline.py" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/configs/config.py" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/BotCode/handlers/post/__init__.py" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/docker-compose.yml" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/BotCode/handlers/post/create_posts.py" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/env_example" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/BotCode/handlers/post/post_list.py" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/middleware/loggers/__init__.py" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/BotCode/loggers/__init__.py" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/middleware/loggers/logs.py" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/BotCode/loggers/logs.py" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/.dockerignore" beforeDir="false" afterPath="$PROJECT_DIR$/.dockerignore" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/BotCode/utils/__init__.py" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/.env" beforeDir="false" afterPath="$PROJECT_DIR$/.env" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/BotCode/utils/md2_escape.py" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/.gitattributes" beforeDir="false" afterPath="$PROJECT_DIR$/.gitattributes" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/BotCode/utils/pagination.py" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/.gitignore" beforeDir="false" afterPath="$PROJECT_DIR$/.gitignore" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/BotCode/utils/usernames.py" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/BotCode/__init__.py" beforeDir="false" afterPath="$PROJECT_DIR$/bot/__init__.py" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/Dockerfile" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/BotCode/config.py" beforeDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/LICENSE" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/BotCode/core/__init__.py" beforeDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/README.md" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/BotCode/core/storage.py" beforeDir="false" afterPath="$PROJECT_DIR$/bot/core/storage.py" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/assets/start.jpg" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/BotCode/handlers/__init__.py" beforeDir="false" afterPath="$PROJECT_DIR$/bot/handlers/__init__.py" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/main.py" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/BotCode/handlers/callback.py" beforeDir="false" afterPath="$PROJECT_DIR$/bot/handlers/callback.py" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/postsOLD/posts_6751720805.json" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/BotCode/handlers/commands/__init__.py" beforeDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/pyproject.toml" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/BotCode/handlers/commands/start_cmd.py" beforeDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/BotCode/handlers/inline.py" beforeDir="false" afterPath="$PROJECT_DIR$/bot/handlers/inline.py" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/BotCode/handlers/post/__init__.py" beforeDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/BotCode/handlers/post/create_posts.py" beforeDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/BotCode/handlers/post/post_list.py" beforeDir="false" afterPath="$PROJECT_DIR$/bot/handlers/post/post_list.py" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/BotCode/loggers/__init__.py" beforeDir="false" afterPath="$PROJECT_DIR$/bot/loggers/__init__.py" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/BotCode/loggers/logs.py" beforeDir="false" afterPath="$PROJECT_DIR$/bot/loggers/logs.py" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/BotCode/utils/__init__.py" beforeDir="false" afterPath="$PROJECT_DIR$/bot/utils/__init__.py" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/BotCode/utils/md2_escape.py" beforeDir="false" afterPath="$PROJECT_DIR$/bot/utils/md2_escape.py" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/BotCode/utils/pagination.py" beforeDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/BotCode/utils/usernames.py" beforeDir="false" afterPath="$PROJECT_DIR$/bot/utils/usernames.py" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/Dockerfile" beforeDir="false" afterPath="$PROJECT_DIR$/Dockerfile" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/LICENSE" beforeDir="false" afterPath="$PROJECT_DIR$/LICENSE" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/README.md" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/assets/start.jpg" beforeDir="false" afterPath="$PROJECT_DIR$/assets/start.jpg" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/main.py" beforeDir="false" afterPath="$PROJECT_DIR$/main.py" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/posts/posts_6751720805.json" beforeDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/pyproject.toml" beforeDir="false" />
|
||||||
</list>
|
</list>
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
@@ -55,9 +72,21 @@
|
|||||||
<component name="Git.Settings">
|
<component name="Git.Settings">
|
||||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectColorInfo"><![CDATA[{
|
<component name="GitHubPullRequestSearchHistory"><![CDATA[{
|
||||||
"associatedIndex": 2
|
"lastFilter": {
|
||||||
|
"state": "OPEN",
|
||||||
|
"assignee": "Whyverum"
|
||||||
|
}
|
||||||
}]]></component>
|
}]]></component>
|
||||||
|
<component name="GithubPullRequestsUISettings"><![CDATA[{
|
||||||
|
"selectedUrlAndAccountId": {
|
||||||
|
"url": "https://github.com/Whyverum/PrimoStoryBot.git",
|
||||||
|
"accountId": "352bed01-4b87-43a7-83c1-cb6af2ca3770"
|
||||||
|
}
|
||||||
|
}]]></component>
|
||||||
|
<component name="ProjectColorInfo">{
|
||||||
|
"associatedIndex": 2
|
||||||
|
}</component>
|
||||||
<component name="ProjectId" id="2xL1onhKjANEmVgLSfNYMKmFWc4" />
|
<component name="ProjectId" id="2xL1onhKjANEmVgLSfNYMKmFWc4" />
|
||||||
<component name="ProjectViewState">
|
<component name="ProjectViewState">
|
||||||
<option name="hideEmptyMiddlePackages" value="true" />
|
<option name="hideEmptyMiddlePackages" value="true" />
|
||||||
@@ -68,6 +97,7 @@
|
|||||||
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
||||||
"Python.main.executor": "Run",
|
"Python.main.executor": "Run",
|
||||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||||
|
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager": "true",
|
||||||
"RunOnceActivity.git.unshallow": "true",
|
"RunOnceActivity.git.unshallow": "true",
|
||||||
"git-widget-placeholder": "master",
|
"git-widget-placeholder": "master",
|
||||||
"node.js.detected.package.eslint": "true",
|
"node.js.detected.package.eslint": "true",
|
||||||
@@ -94,8 +124,9 @@
|
|||||||
<env name="PYTHONUNBUFFERED" value="1" />
|
<env name="PYTHONUNBUFFERED" value="1" />
|
||||||
</envs>
|
</envs>
|
||||||
<option name="SDK_HOME" value="" />
|
<option name="SDK_HOME" value="" />
|
||||||
|
<option name="SDK_NAME" value="Python 3.13 (PrimoStoryBot)" />
|
||||||
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
|
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
|
||||||
<option name="IS_MODULE_SDK" value="true" />
|
<option name="IS_MODULE_SDK" value="false" />
|
||||||
<option name="ADD_CONTENT_ROOTS" value="true" />
|
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||||
<option name="ADD_SOURCE_ROOTS" value="true" />
|
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||||
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
||||||
@@ -117,8 +148,8 @@
|
|||||||
<component name="SharedIndexes">
|
<component name="SharedIndexes">
|
||||||
<attachedChunks>
|
<attachedChunks>
|
||||||
<set>
|
<set>
|
||||||
<option value="bundled-js-predefined-d6986cc7102b-6a121458b545-JavaScript-PY-251.25410.122" />
|
<option value="bundled-js-predefined-d6986cc7102b-09060db00ec0-JavaScript-PY-251.26927.90" />
|
||||||
<option value="bundled-python-sdk-880ecab49056-36ea0e71a18c-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-251.25410.122" />
|
<option value="bundled-python-sdk-41e8cd69c857-64d779b69b7a-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-251.26927.90" />
|
||||||
</set>
|
</set>
|
||||||
</attachedChunks>
|
</attachedChunks>
|
||||||
</component>
|
</component>
|
||||||
@@ -130,6 +161,7 @@
|
|||||||
<option name="presentableId" value="Default" />
|
<option name="presentableId" value="Default" />
|
||||||
<updated>1747702717100</updated>
|
<updated>1747702717100</updated>
|
||||||
<workItem from="1747702718164" duration="4191000" />
|
<workItem from="1747702718164" duration="4191000" />
|
||||||
|
<workItem from="1754837986649" duration="1081000" />
|
||||||
</task>
|
</task>
|
||||||
<servers />
|
<servers />
|
||||||
</component>
|
</component>
|
||||||
@@ -138,5 +170,6 @@
|
|||||||
</component>
|
</component>
|
||||||
<component name="com.intellij.coverage.CoverageDataManagerImpl">
|
<component name="com.intellij.coverage.CoverageDataManagerImpl">
|
||||||
<SUITE FILE_PATH="coverage/PRIMOSTORYFINAL$main.coverage" NAME="main Coverage Results" MODIFIED="1747706991539" 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/PRIMOSTORYFINAL$main.coverage" NAME="main Coverage Results" MODIFIED="1747706991539" 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/PrimoStoryBot$main.coverage" NAME="main Coverage Results" MODIFIED="1754838961735" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# BotCode/config.py
|
|
||||||
from os import getenv
|
|
||||||
from ast import literal_eval
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
|
|
||||||
# Загружаем переменные из файла .env
|
|
||||||
load_dotenv()
|
|
||||||
|
|
||||||
BOT_TOKEN: str|None = getenv('BOT_TOKEN', None)
|
|
||||||
BOT_DEBUG_TOKEN: str|None = getenv('BOT_DEBUG_TOKEN', None)
|
|
||||||
|
|
||||||
ADMIN_ID: tuple[int] = literal_eval(getenv('ADMIN_ID', '[6751720805]'))
|
|
||||||
|
|
||||||
PARSE_MODE: str = getenv('PARSE_MODE', "HTML")
|
|
||||||
|
|
||||||
LOGGING_TO_CONSOLE: bool = getenv('LOGGING_TO_CONSOLE', "False").lower() == 'true'
|
|
||||||
DEBUG_MODE: bool = getenv('DEBUG_MODE', "False").lower() == 'true'
|
|
||||||
|
|
||||||
POSTS_DIR: str = getenv('POSTS_DIR', "posts")
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
from aiogram import Router
|
|
||||||
from .start_cmd import router as start_cmd_router
|
|
||||||
|
|
||||||
__all__ = ('router',)
|
|
||||||
router = Router(name="post_router")
|
|
||||||
|
|
||||||
router.include_routers(start_cmd_router,)
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
# BotCode/handlers/commands/start_cmd.py
|
|
||||||
from aiogram import Router, types
|
|
||||||
from aiogram.filters import CommandStart
|
|
||||||
|
|
||||||
router = Router(name=__name__)
|
|
||||||
__all__ = ("router",)
|
|
||||||
|
|
||||||
|
|
||||||
@router.message(CommandStart())
|
|
||||||
async def start_cmd(message: types.Message) -> None:
|
|
||||||
"""
|
|
||||||
Обработчик команды /start.
|
|
||||||
|
|
||||||
:param message: Объект сообщения и информации о нем.
|
|
||||||
:return: Вывод сообщения для администратора, о выборе режимов работы.
|
|
||||||
"""
|
|
||||||
from BotCode.loggers import logs
|
|
||||||
from BotCode.utils import textmd2
|
|
||||||
logs.info(text="использовал(а) команду /start", log_type="Start", message=message)
|
|
||||||
|
|
||||||
if message.from_user.id:
|
|
||||||
# Создаем клавиатурный билдер
|
|
||||||
from aiogram.utils.keyboard import ReplyKeyboardBuilder
|
|
||||||
rkb: ReplyKeyboardBuilder = ReplyKeyboardBuilder()
|
|
||||||
rkb.row(types.KeyboardButton(text="Создать пост📔"))
|
|
||||||
rkb.row(types.KeyboardButton(text="Посмотреть список📋"))
|
|
||||||
|
|
||||||
# Отправка фотографии с текстом и клавиатурой
|
|
||||||
from aiogram.types.input_file import FSInputFile
|
|
||||||
await message.reply_photo(
|
|
||||||
photo=FSInputFile('assets/start.jpg'),
|
|
||||||
caption=textmd2("Добро пожаловать в систему, Босс!"),
|
|
||||||
reply_markup=rkb.as_markup(resize_keyboard=True)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
await message.reply(text=textmd2("Простите, вы не мой Босс!❌\nОбратитесь к @verdise!"))
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
from aiogram import Router
|
|
||||||
from .create_posts import router as posts_router
|
|
||||||
from .post_list import router as post_list_router
|
|
||||||
|
|
||||||
router = Router(name="post_router")
|
|
||||||
|
|
||||||
router.include_routers(
|
|
||||||
posts_router,
|
|
||||||
post_list_router,
|
|
||||||
)
|
|
||||||
@@ -1,210 +0,0 @@
|
|||||||
import uuid
|
|
||||||
from threading import Lock
|
|
||||||
|
|
||||||
from aiogram import Router, F
|
|
||||||
from aiogram.types import (
|
|
||||||
Message, CallbackQuery,
|
|
||||||
InlineKeyboardButton, InlineKeyboardMarkup
|
|
||||||
)
|
|
||||||
from aiogram.fsm.state import State, StatesGroup
|
|
||||||
from aiogram.fsm.context import FSMContext
|
|
||||||
|
|
||||||
from BotCode.core.storage import storage
|
|
||||||
from BotCode.utils import textmd2
|
|
||||||
|
|
||||||
router = Router()
|
|
||||||
|
|
||||||
class PostState(StatesGroup):
|
|
||||||
waiting_for_text = State()
|
|
||||||
waiting_for_privacy = State()
|
|
||||||
waiting_for_id = State()
|
|
||||||
waiting_for_image = State()
|
|
||||||
waiting_for_buttons = State()
|
|
||||||
|
|
||||||
post_id_lock = Lock()
|
|
||||||
|
|
||||||
# --- Utility functions ---
|
|
||||||
def make_inline_markup(rows: list[list[InlineKeyboardButton]]) -> InlineKeyboardMarkup:
|
|
||||||
return InlineKeyboardMarkup(inline_keyboard=rows)
|
|
||||||
|
|
||||||
def cancel_button() -> InlineKeyboardMarkup:
|
|
||||||
return make_inline_markup([[InlineKeyboardButton(text="Отмена", callback_data="cancel_creation")]])
|
|
||||||
|
|
||||||
def privacy_markup(is_private: bool) -> InlineKeyboardMarkup:
|
|
||||||
toggle = InlineKeyboardButton(
|
|
||||||
text="🔒 Приватный" if is_private else "🔓 Публичный",
|
|
||||||
callback_data="toggle_privacy"
|
|
||||||
)
|
|
||||||
cont = InlineKeyboardButton(text="Продолжить ➡️", callback_data="continue_creation")
|
|
||||||
return make_inline_markup([[toggle], [cont]])
|
|
||||||
|
|
||||||
def parse_buttons(text: str) -> list[list[dict]]:
|
|
||||||
rows: list[list[dict]] = []
|
|
||||||
current: list[dict] = []
|
|
||||||
for raw in text.splitlines():
|
|
||||||
line = raw.strip()
|
|
||||||
if not line:
|
|
||||||
if current:
|
|
||||||
rows.append(current)
|
|
||||||
current = []
|
|
||||||
continue
|
|
||||||
if '|' not in line:
|
|
||||||
raise ValueError(f"Неверный формат кнопки: '{line}'")
|
|
||||||
label, action = map(str.strip, line.split('|', 1))
|
|
||||||
btn: dict = {"text": label}
|
|
||||||
if action.startswith('notification:'):
|
|
||||||
btn['notification'] = action.split(':', 1)[1]
|
|
||||||
btn['show_alert'] = True
|
|
||||||
elif action.startswith('copy:'):
|
|
||||||
btn['callback_data'] = f"copy_{uuid.uuid4().hex}"
|
|
||||||
btn['copy_text'] = action.split(':', 1)[1]
|
|
||||||
elif action.startswith('switch_inline:'):
|
|
||||||
btn['switch_inline_query'] = action.split(':', 1)[1]
|
|
||||||
elif action.startswith('switch_inline_current:'):
|
|
||||||
btn['switch_inline_query_current_chat'] = action.split(':', 1)[1]
|
|
||||||
elif action.startswith('switch_inline_chosen:'):
|
|
||||||
btn['switch_inline_query_chosen_chat'] = action.split(':', 1)[1]
|
|
||||||
elif action.startswith(('http://', 'https://')):
|
|
||||||
btn['url'] = action
|
|
||||||
else:
|
|
||||||
btn['callback_data'] = action
|
|
||||||
current.append(btn)
|
|
||||||
if current:
|
|
||||||
rows.append(current)
|
|
||||||
return rows
|
|
||||||
|
|
||||||
# --- Handlers ---
|
|
||||||
@router.message(F.text == "Создать пост📔")
|
|
||||||
async def start_creation(message: Message, state: FSMContext):
|
|
||||||
await state.set_state(PostState.waiting_for_text)
|
|
||||||
await state.update_data(private=False, buttons=[])
|
|
||||||
await message.reply(
|
|
||||||
textmd2(
|
|
||||||
"""Отправьте текст вашего поста:
|
|
||||||
Тест для проверки @userbotname
|
|
||||||
<b>Жирный</b>
|
|
||||||
<i>Курсив</i>
|
|
||||||
<u>Подчёркнутый</u>
|
|
||||||
<s>Зачёркнутый</s>
|
|
||||||
<code>Моноширинный</code>
|
|
||||||
<pre>Предварительно отформатированный</pre>
|
|
||||||
<a href=\"https://example.com\">Ссылка</a>
|
|
||||||
"""
|
|
||||||
),
|
|
||||||
reply_markup=cancel_button(), parse_mode=None
|
|
||||||
)
|
|
||||||
|
|
||||||
@router.message(PostState.waiting_for_text)
|
|
||||||
async def got_text(message: Message, state: FSMContext):
|
|
||||||
await state.update_data(text=message.text or message.caption or "")
|
|
||||||
await state.set_state(PostState.waiting_for_privacy)
|
|
||||||
data = await state.get_data()
|
|
||||||
await message.reply(
|
|
||||||
"Выберите приватность поста:",
|
|
||||||
reply_markup=privacy_markup(data.get('private', False))
|
|
||||||
)
|
|
||||||
|
|
||||||
@router.callback_query(lambda c: c.data == "toggle_privacy")
|
|
||||||
async def toggle_privacy(cq: CallbackQuery, state: FSMContext):
|
|
||||||
data = await state.get_data()
|
|
||||||
is_priv = not data.get('private', False)
|
|
||||||
await state.update_data(private=is_priv)
|
|
||||||
await cq.message.edit_reply_markup(
|
|
||||||
reply_markup=privacy_markup(is_priv)
|
|
||||||
)
|
|
||||||
await cq.answer()
|
|
||||||
|
|
||||||
@router.callback_query(lambda c: c.data == "continue_creation")
|
|
||||||
async def continue_to_id(cq: CallbackQuery, state: FSMContext):
|
|
||||||
await state.set_state(PostState.waiting_for_id)
|
|
||||||
await cq.message.edit_text("Введите уникальный ID поста (латиница, цифры, подчёрки):")
|
|
||||||
await cq.answer()
|
|
||||||
|
|
||||||
@router.message(PostState.waiting_for_id)
|
|
||||||
async def got_id(message: Message, state: FSMContext):
|
|
||||||
pid = message.text.strip()
|
|
||||||
if not pid.replace('_', '').isalnum():
|
|
||||||
await message.reply(
|
|
||||||
"ID должен содержать только латиницу, цифры и подчёркивания.",
|
|
||||||
reply_markup=cancel_button()
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
with post_id_lock:
|
|
||||||
if not storage.is_post_available(pid):
|
|
||||||
await message.reply(
|
|
||||||
text="Этот ID уже занят, введите другой:",
|
|
||||||
reply_markup=cancel_button()
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
await state.update_data(post_id=pid)
|
|
||||||
await state.set_state(PostState.waiting_for_image)
|
|
||||||
await message.reply(
|
|
||||||
text="Отправьте ссылку на изображение или 'нет':\n"
|
|
||||||
"Пример: https://img4.teletype.in/files/f2/47/...",
|
|
||||||
reply_markup=cancel_button()
|
|
||||||
)
|
|
||||||
|
|
||||||
@router.message(PostState.waiting_for_image)
|
|
||||||
async def got_image(message: Message, state: FSMContext):
|
|
||||||
img = message.text.strip()
|
|
||||||
if img.lower() in ('нет', 'no', 'none'):
|
|
||||||
img = ''
|
|
||||||
await state.update_data(image=img)
|
|
||||||
await state.set_state(PostState.waiting_for_buttons)
|
|
||||||
await message.reply(
|
|
||||||
textmd2(
|
|
||||||
"""Отправьте кнопки по шаблону:
|
|
||||||
Кнопка заглушка | void
|
|
||||||
Уведомление | notification:Для вас!
|
|
||||||
Кнопка ссылка | https://google.com
|
|
||||||
Копирование | copy:Копирование текста!
|
|
||||||
Для одного | callback_data | allowed_ids=123 | unauthorized_message=Нет доступа
|
|
||||||
|
|
||||||
Пустая строка — новый ряд. /done — закончить."""
|
|
||||||
),
|
|
||||||
reply_markup=cancel_button(), parse_mode=None
|
|
||||||
)
|
|
||||||
|
|
||||||
@router.message(PostState.waiting_for_buttons)
|
|
||||||
async def got_buttons(message: Message, state: FSMContext):
|
|
||||||
text = message.text.strip()
|
|
||||||
data = await state.get_data()
|
|
||||||
uid = message.from_user.id
|
|
||||||
pid = data['post_id']
|
|
||||||
try:
|
|
||||||
if text.lower() in ('/done', 'none'):
|
|
||||||
btns = data.get('buttons', []) if text == '/done' else []
|
|
||||||
posts = storage.load_user_posts(uid)
|
|
||||||
posts[pid] = {
|
|
||||||
'user_id': uid,
|
|
||||||
'text': data['text'],
|
|
||||||
'image': data['image'],
|
|
||||||
'buttons': btns,
|
|
||||||
'private': data.get('private', False)
|
|
||||||
}
|
|
||||||
storage.save_user_posts(uid, posts)
|
|
||||||
await message.reply(
|
|
||||||
f"✅ Пост создан! ID: {pid}\n"
|
|
||||||
f"{'🔒 Приватный' if data.get('private') else '🔓 Публичный'}\n"
|
|
||||||
f"Используйте: <code>@{(await message.bot.me()).username} {pid}</code>"
|
|
||||||
)
|
|
||||||
await state.clear()
|
|
||||||
return
|
|
||||||
|
|
||||||
rows = parse_buttons(text)
|
|
||||||
existing = data.get('buttons', [])
|
|
||||||
await state.update_data(buttons=existing + rows)
|
|
||||||
await message.reply(
|
|
||||||
text="✅ Кнопки добавлены. Добавьте ещё или /done для окончания.",
|
|
||||||
reply_markup=cancel_button()
|
|
||||||
)
|
|
||||||
except ValueError as err:
|
|
||||||
await message.reply(f"❌ {err}")
|
|
||||||
|
|
||||||
@router.callback_query(lambda c: c.data == "cancel_creation")
|
|
||||||
async def cancel(cq: CallbackQuery, state: FSMContext):
|
|
||||||
await state.clear()
|
|
||||||
await cq.message.reply(textmd2("Процесс создания поста отменён."))
|
|
||||||
await cq.answer()
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
# BotCode/utils/pagination.py
|
|
||||||
from typing import List
|
|
||||||
from aiogram.types import InlineKeyboardButton
|
|
||||||
|
|
||||||
# Настройка экспорта в модули
|
|
||||||
__all__ = ('create_pagination_buttons',)
|
|
||||||
|
|
||||||
def create_pagination_buttons(action: str,
|
|
||||||
page: int = 0,
|
|
||||||
total_posts: int = 0,
|
|
||||||
bt_page: int = 5) -> List[InlineKeyboardButton]:
|
|
||||||
"""Создает кнопки для пагинации."""
|
|
||||||
navigation_buttons = []
|
|
||||||
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
|
|
||||||
25
Dockerfile
25
Dockerfile
@@ -1,10 +1,23 @@
|
|||||||
FROM mwalbeck/python-poetry:2.1-3.11
|
# Используем официальный облегчённый образ Python 3.11
|
||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
WORKDIR /PostBot
|
# Задаём рабочую директорию внутри контейнера
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
COPY pyproject.toml poetry.lock ./
|
# Обновляем pip для актуальной версии (необязательно, но рекомендуется)
|
||||||
RUN poetry install --no-interaction --no-root --only main
|
RUN pip install --upgrade pip
|
||||||
|
|
||||||
COPY ../../../../Desktop/PostBot .
|
# Копируем файл зависимостей в контейнер
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
CMD ["poetry", "run", "python", "-m", "main"]
|
# Устанавливаем зависимости из requirements.txt
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Копируем весь код проекта в рабочую директорию контейнера
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Опционально: задаём переменную окружения для оптимизации работы Python
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
# Указываем команду запуска бота (можно изменить под ваш файл)
|
||||||
|
CMD ["python", "main.py"]
|
||||||
|
|||||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) [2025] [Лейн]
|
Copyright (c) [2025] [Verum]
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
78
README.md
78
README.md
@@ -1,31 +1,47 @@
|
|||||||
# Создание поста
|
PROJECT/
|
||||||
new_post = {
|
├── config/
|
||||||
"id": "cat_post",
|
│ ├── __init__.py
|
||||||
"author_id": 123,
|
│ ├── settings.py # Основные настройки
|
||||||
"mod": "HTML",
|
│ └── roles_config.py # Конфиг ролей и прав
|
||||||
"type": "photo",
|
├── data/
|
||||||
"text": "Мой котик!",
|
│ ├── database.db # SQLite база (или папка для миграций если PostgreSQL)
|
||||||
"media": "cat.jpg",
|
│ ├── lists/ # JSON/CSV файлы списков (игроков, персонажей и т.д.)
|
||||||
"private": True,
|
│ └── templates/ # Шаблоны сообщений
|
||||||
"allowed_users": [456, 789],
|
├── handlers/
|
||||||
"buttons": [[{
|
│ ├── __init__.py
|
||||||
"type": "share",
|
│ ├── private/ # Обработчики ЛС
|
||||||
"name": "Поделиться",
|
│ │ ├── commands.py
|
||||||
"params": {"message": "Посмотрите этого котика!"}
|
│ │ ├── faq.py
|
||||||
}]]
|
│ │ ├── reports.py
|
||||||
}
|
│ │ └── notifications.py
|
||||||
|
│ ├── groups/ # Обработчики групповых чатов
|
||||||
post_id = storage.create_post(new_post)
|
│ │ ├── flood.py
|
||||||
|
│ │ ├── roleplay.py
|
||||||
# Получение поста
|
│ │ └── moderation.py
|
||||||
post = storage.get_post(post_id, user_id=456) # Доступ разрешен
|
│ └── channels/ # Обработчики каналов
|
||||||
post = storage.get_post(post_id, user_id=000) # Доступ запрещен
|
│ ├── info_updater.py
|
||||||
|
│ └── life_news.py
|
||||||
# Поиск постов
|
├── middlewares/
|
||||||
results = storage.search_posts("котик", user_id=456)
|
│ ├── __init__.py
|
||||||
|
│ ├── throttling.py # Анти-спам
|
||||||
# Обновление поста
|
│ ├── database.py # Интеграция БД
|
||||||
storage.update_post(post_id, updater_id=123, updates={"text": "Новый текст"})
|
│ └── mode_switcher.py # Переключение режимов
|
||||||
|
├── services/
|
||||||
# Удаление поста
|
│ ├── __init__.py
|
||||||
storage.delete_post(post_id, deleter_id=123)
|
│ ├── database.py # CRUD операции
|
||||||
|
│ ├── stats.py # Статистика сообщений
|
||||||
|
│ ├── list_manager.py # Управление списками
|
||||||
|
│ ├── notifier.py # Уведомления
|
||||||
|
│ └── antispam.py # Система спам-фильтрации
|
||||||
|
├── utils/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── parsers.py # Парсинг сообщений
|
||||||
|
│ ├── keyboards.py # Генерация клавиатур
|
||||||
|
│ └── helpers.py # Вспомогательные функции
|
||||||
|
├── states/ # FSM состояния
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── user_registration.py
|
||||||
|
│ └── report_states.py
|
||||||
|
├── .env # Переменные окружения
|
||||||
|
├── requirements.txt # Зависимости
|
||||||
|
└── main.py # Точка входа
|
||||||
BIN
assets/start.jpg
BIN
assets/start.jpg
Binary file not shown.
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 32 KiB |
@@ -1,4 +1,3 @@
|
|||||||
from .config import *
|
|
||||||
from .handlers import *
|
from .handlers import *
|
||||||
from .utils import *
|
from .utils import *
|
||||||
from .config import *
|
from .bots import *
|
||||||
203
bot/bots.py
Normal file
203
bot/bots.py
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
from aiogram import Bot, Dispatcher
|
||||||
|
from aiogram.client.default import DefaultBotProperties
|
||||||
|
from aiogram.fsm.storage.memory import MemoryStorage
|
||||||
|
from aiogram.types import User, ChatAdministratorRights, BotDescription, BotShortDescription
|
||||||
|
from aiogram.utils.i18n import ConstI18nMiddleware, I18n
|
||||||
|
|
||||||
|
from loggers.logs import loggers
|
||||||
|
from configs.config import BotSettings, BotEdit, Webhook
|
||||||
|
from middleware.loggers import log
|
||||||
|
|
||||||
|
# Экспортируем объекты модуля
|
||||||
|
__all__ = ("dp", "bot", "BotInfo", "i18n",)
|
||||||
|
|
||||||
|
# Инициализация i18n
|
||||||
|
i18n: I18n = I18n(path="locales", default_locale="ru", domain="bot")
|
||||||
|
|
||||||
|
# Диспетчер бота, языковых настроек и его хранилища
|
||||||
|
storage: MemoryStorage = MemoryStorage()
|
||||||
|
dp: Dispatcher = Dispatcher(storage=storage)
|
||||||
|
dp.message.outer_middleware(ConstI18nMiddleware(locale='ru', i18n=i18n))
|
||||||
|
dp["is_active"]: bool = True
|
||||||
|
|
||||||
|
# Экземпляр бота с настройками по умолчанию
|
||||||
|
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
|
||||||
|
url: str = None
|
||||||
|
first_name: str = None
|
||||||
|
last_name: str = None
|
||||||
|
username: str = None
|
||||||
|
description: str = None
|
||||||
|
short_description: str = 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, delete_webhook: bool = Webhook.WEBHOOK) -> None:
|
||||||
|
"""
|
||||||
|
Удаление или установка вебхука.
|
||||||
|
|
||||||
|
:param bots: Объект бота для управления.
|
||||||
|
:param delete_webhook: Статус удаления, поумолчанию (true).
|
||||||
|
"""
|
||||||
|
if delete_webhook:
|
||||||
|
await bots.delete_webhook()
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@log(level='INFO', log_type='BOT', text='Получение информации о боте')
|
||||||
|
async def info(cls, bots: Bot = bot) -> dict:
|
||||||
|
"""
|
||||||
|
Получает и сохраняет информацию о боте.
|
||||||
|
|
||||||
|
:param bots: Объект бота для управления.
|
||||||
|
:return: Словарь с персональными данными о боте.
|
||||||
|
"""
|
||||||
|
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.description = getattr(bot_info, 'description', '')
|
||||||
|
cls.short_description = getattr(bot_info, 'short_description', '')
|
||||||
|
cls.language_code = bot_info.language_code
|
||||||
|
cls.is_premium = bot_info.is_premium
|
||||||
|
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,
|
||||||
|
'description': cls.description,
|
||||||
|
'short_description': cls.short_description,
|
||||||
|
'language_code': cls.language_code,
|
||||||
|
'prefix': cls.prefix,
|
||||||
|
'bot_owner': cls.bot_owner,
|
||||||
|
'is_premium': cls.is_premium,
|
||||||
|
'added_to_attachment_menu': cls.added_to_attachment_menu,
|
||||||
|
'supports_inline_queries': cls.supports_inline_queries,
|
||||||
|
'can_connect_to_business': cls.can_connect_to_business,
|
||||||
|
'has_main_web_app': cls.has_main_web_app,
|
||||||
|
'can_join_groups': cls.can_join_groups,
|
||||||
|
'can_read_all_group_messages': cls.can_read_all_group_messages,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@log(level='INFO', log_type='BOT', text='Установка прав администратора')
|
||||||
|
async def set_administrator_rights(bots: Bot = bot, rights: ChatAdministratorRights = BotEdit.RIGHTS) -> None:
|
||||||
|
"""
|
||||||
|
Устанавливает права администратора по умолчанию.
|
||||||
|
|
||||||
|
:param bots: Объект бота для управления.
|
||||||
|
:param rights: Заданные права администратора бота, по умолчанию словарь из конфигов.
|
||||||
|
"""
|
||||||
|
bot_rights: ChatAdministratorRights = await bots.get_my_default_administrator_rights()
|
||||||
|
|
||||||
|
if bot_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:
|
||||||
|
"""
|
||||||
|
Устанавливает имя бота из конфига.
|
||||||
|
|
||||||
|
:param bots: Объект бота для управления.
|
||||||
|
:param new_name: Новое имя бота, по умолчанию из конфигов.
|
||||||
|
"""
|
||||||
|
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:
|
||||||
|
"""
|
||||||
|
Устанавливает полное описание бота.
|
||||||
|
|
||||||
|
:param bots: Объект бота для управления.
|
||||||
|
:param new_description: Новое описание бота, по умолчанию из конфигов.
|
||||||
|
"""
|
||||||
|
current_description: BotDescription = await bots.get_my_description()
|
||||||
|
|
||||||
|
if not (0 < len(new_description) <= 255):
|
||||||
|
raise ValueError("Описание должно быть от 1 до 255 символов.")
|
||||||
|
|
||||||
|
if current_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:
|
||||||
|
"""
|
||||||
|
Устанавливает короткое описание виджета.
|
||||||
|
|
||||||
|
:param bots: Объект бота для управления.
|
||||||
|
:param new_short: Новое короткое описание бота, по умолчанию из конфигов.
|
||||||
|
"""
|
||||||
|
current_short: BotShortDescription = await bots.get_my_short_description()
|
||||||
|
|
||||||
|
if not (0 < len(new_short) <= 512):
|
||||||
|
raise ValueError("Короткое описание должно быть от 1 до 512 символов.")
|
||||||
|
|
||||||
|
if current_short != new_short:
|
||||||
|
await bots.set_my_short_description(short_description=new_short)
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@log(level='INFO', log_type='START', text=f'Процесс запуска бота!!!!!')
|
||||||
|
async def setup(cls, bots: Bot = bot):
|
||||||
|
"""
|
||||||
|
Выполняет полную настройку бота.
|
||||||
|
|
||||||
|
:param bots: Объект бота для управления.
|
||||||
|
"""
|
||||||
|
await cls.webhook(bots=bots)
|
||||||
|
await cls.info(bots=bots)
|
||||||
|
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)
|
||||||
|
loggers.info(text=f"Бот @{BotInfo.username} запущен!!!")
|
||||||
1
bot/core/__init__.py
Normal file
1
bot/core/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .storage import *
|
||||||
@@ -1,14 +1,16 @@
|
|||||||
import json
|
import json
|
||||||
from os import path, makedirs, listdir
|
from os import path, makedirs, listdir
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
from BotCode.config import POSTS_DIR
|
from configs.config import Project
|
||||||
from BotCode.loggers import logs
|
from bot.loggers import logs
|
||||||
|
|
||||||
|
# Настройки экспорта
|
||||||
|
__all__ = ("storage", )
|
||||||
|
|
||||||
class PostStorage:
|
class PostStorage:
|
||||||
"""Класс для управления хранением постов и связанных уведомлений."""
|
"""Класс для управления хранением постов и связанных уведомлений."""
|
||||||
|
|
||||||
def __init__(self, posts_dir: str = POSTS_DIR):
|
def __init__(self, posts_dir: str = Project.POSTS_DIR):
|
||||||
self.posts_dir = posts_dir
|
self.posts_dir = posts_dir
|
||||||
self.global_posts: Dict[str, Dict[str, Any]] = {}
|
self.global_posts: Dict[str, Dict[str, Any]] = {}
|
||||||
self.notifications: Dict[str, Dict[str, Any]] = {}
|
self.notifications: Dict[str, Dict[str, Any]] = {}
|
||||||
@@ -231,4 +233,4 @@ class PostStorage:
|
|||||||
|
|
||||||
|
|
||||||
# Инициализация хранилища при импорте модуля
|
# Инициализация хранилища при импорте модуля
|
||||||
storage = PostStorage()
|
storage: PostStorage = PostStorage()
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
from aiogram import Router
|
from aiogram import Router
|
||||||
from .post import router as post_routers
|
from .post import router as post_routers
|
||||||
from .commands import router as cmd_routers
|
from .commands import router as cmd_routers
|
||||||
|
|
||||||
from .callback import router as callback_router
|
from .callback import router as callback_router
|
||||||
from .inline import router as inline_router
|
from .inline import router as inline_router
|
||||||
|
|
||||||
router = Router(name=__name__)
|
# Настройка экспорта и роутера
|
||||||
|
__all__ = ("router",)
|
||||||
|
router: Router = Router(name="handlers_router")
|
||||||
|
|
||||||
# Include routers with different priorities
|
# Подключение роутеров
|
||||||
router.include_routers(
|
router.include_routers(
|
||||||
cmd_routers,
|
cmd_routers,
|
||||||
callback_router,
|
callback_router,
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
# BotCode/handlers/callback.py
|
from typing import Optional
|
||||||
|
|
||||||
from aiogram import Router, F
|
from aiogram import Router, F
|
||||||
from aiogram.types import CallbackQuery
|
from aiogram.types import CallbackQuery
|
||||||
from BotCode.core.storage import storage
|
from bot.core import storage
|
||||||
|
|
||||||
router = Router(name="callback_router")
|
router: Router = Router(name="callback_router")
|
||||||
|
|
||||||
@router.callback_query(F.data.startswith("bt_"))
|
@router.callback_query(F.data.startswith("bt_"))
|
||||||
@router.callback_query(F.data.startswith("show_alert_"))
|
@router.callback_query(F.data.startswith("show_alert_"))
|
||||||
async def handle_button_alert(callback_query: CallbackQuery) -> None:
|
async def handle_button_alert(callback_query: CallbackQuery) -> None:
|
||||||
key = callback_query.data
|
key: Optional[str] = callback_query.data
|
||||||
user_id = callback_query.from_user.id
|
user_id: int = callback_query.from_user.id
|
||||||
|
|
||||||
# Получаем уведомление через хранилище
|
# Получаем уведомление через хранилище
|
||||||
notif = storage.get_notification(key)
|
notif = storage.get_notification(key)
|
||||||
@@ -29,11 +30,8 @@ async def handle_button_alert(callback_query: CallbackQuery) -> None:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
await callback_query.answer(text=text, show_alert=show_alert)
|
await callback_query.answer(text=text, show_alert=show_alert)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
try:
|
await callback_query.answer(text="Произошла ошибка при отображении уведомления.", show_alert=True)
|
||||||
await callback_query.answer(text="Произошла ошибка при отображении уведомления.", show_alert=True)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(F.data == "void")
|
@router.callback_query(F.data == "void")
|
||||||
@@ -44,5 +42,5 @@ async def handle_void_callback(callback_query: CallbackQuery) -> None:
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
await callback_query.answer()
|
await callback_query.answer()
|
||||||
except Exception as e:
|
except Exception:
|
||||||
return
|
return
|
||||||
12
bot/handlers/commands/__init__.py
Normal file
12
bot/handlers/commands/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from aiogram import Router
|
||||||
|
from .start import router as start_cmd_router
|
||||||
|
from .help import router as help_cmd_router
|
||||||
|
|
||||||
|
# Настройка экспорта и роутера
|
||||||
|
__all__ = ('router',)
|
||||||
|
router: Router = Router(name="cmd_router")
|
||||||
|
|
||||||
|
# Подготовка роутера команд
|
||||||
|
router.include_routers(start_cmd_router,
|
||||||
|
help_cmd_router,
|
||||||
|
)
|
||||||
47
bot/handlers/commands/help.py
Normal file
47
bot/handlers/commands/help.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
from aiogram import Router, F
|
||||||
|
from aiogram.filters import Command
|
||||||
|
from aiogram.fsm.context import FSMContext
|
||||||
|
from aiogram.types import Message, CallbackQuery, KeyboardButton
|
||||||
|
from aiogram.utils.keyboard import ReplyKeyboardBuilder
|
||||||
|
from aiogram.utils.i18n import gettext as _
|
||||||
|
|
||||||
|
from bot.templates import msg_photo
|
||||||
|
from bot.utils.interesting_facts import interesting_fact
|
||||||
|
from middleware.loggers import log
|
||||||
|
from bot.bots import BotInfo
|
||||||
|
from configs import COMMANDS, BotEdit
|
||||||
|
|
||||||
|
# Настройки экспорта и роутера
|
||||||
|
__all__ = ("router",)
|
||||||
|
CMD: str = "help".lower()
|
||||||
|
router: Router = Router(name=f"{CMD}_cmd_router")
|
||||||
|
|
||||||
|
|
||||||
|
@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 help_cmd(message: Message | CallbackQuery, state: FSMContext) -> None:
|
||||||
|
"""
|
||||||
|
Обработчик команды /help
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message (Message | CallbackQuery): Сообщение или callback-запрос от пользователя.
|
||||||
|
state (FSMContext): Состояние пользователя бота.
|
||||||
|
"""
|
||||||
|
await state.clear()
|
||||||
|
|
||||||
|
# Создаем клавиатуру с кнопками
|
||||||
|
rkb: ReplyKeyboardBuilder = ReplyKeyboardBuilder()
|
||||||
|
rkb.row(KeyboardButton(text=_("Создать пост📔")))
|
||||||
|
rkb.row(KeyboardButton(text=_("Посмотреть список📋")))
|
||||||
|
|
||||||
|
# Формируем приветственное сообщение
|
||||||
|
text: str = _(
|
||||||
|
"""Добро пожаловать, <a href="{url}">{name}</a>!"""
|
||||||
|
).format(
|
||||||
|
url=message.from_user.url if message.from_user else "",
|
||||||
|
name=message.from_user.first_name if message.from_user else "пользователь",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Отправляем сообщение
|
||||||
|
await msg_photo(message=message, text=text, file='assets/start.jpg', markup=rkb)
|
||||||
57
bot/handlers/commands/start.py
Normal file
57
bot/handlers/commands/start.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
from aiogram import Router, F
|
||||||
|
from aiogram.filters import Command
|
||||||
|
from aiogram.fsm.context import FSMContext
|
||||||
|
from aiogram.types import Message, CallbackQuery, KeyboardButton
|
||||||
|
from aiogram.utils.keyboard import ReplyKeyboardBuilder
|
||||||
|
from aiogram.utils.i18n import gettext as _
|
||||||
|
|
||||||
|
from bot.templates import msg_photo
|
||||||
|
from bot.utils.interesting_facts import interesting_fact
|
||||||
|
from middleware.loggers import log
|
||||||
|
from bot.bots import BotInfo
|
||||||
|
from configs import COMMANDS, BotEdit
|
||||||
|
|
||||||
|
# Настройки экспорта и роутера
|
||||||
|
__all__ = ("router",)
|
||||||
|
CMD: str = "start".lower()
|
||||||
|
router: Router = Router(name=f"{CMD}_cmd_router")
|
||||||
|
|
||||||
|
|
||||||
|
@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 start_cmd(message: Message | CallbackQuery, state: FSMContext) -> None:
|
||||||
|
"""
|
||||||
|
Обработчик команды /start
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message (Message | CallbackQuery): Сообщение или callback-запрос от пользователя.
|
||||||
|
state (FSMContext): Состояние пользователя бота.
|
||||||
|
"""
|
||||||
|
await state.clear()
|
||||||
|
|
||||||
|
# Создаем клавиатуру с кнопками
|
||||||
|
rkb: ReplyKeyboardBuilder = ReplyKeyboardBuilder()
|
||||||
|
rkb.row(KeyboardButton(text=_("Создать пост📔")))
|
||||||
|
rkb.row(KeyboardButton(text=_("Посмотреть список📋")))
|
||||||
|
|
||||||
|
# Формируем приветственное сообщение
|
||||||
|
text: str = _(
|
||||||
|
"""Добро пожаловать, <a href="{url}">{name}</a>!
|
||||||
|
|
||||||
|
Мое имя - <b>{bot_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 "пользователь",
|
||||||
|
bot_name=BotEdit.PROJECT_NAME,
|
||||||
|
fact=interesting_fact(),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Отправляем сообщение
|
||||||
|
await msg_photo(message=message, text=text, file='assets/start.jpg', markup=rkb)
|
||||||
@@ -11,12 +11,10 @@ from aiogram.types import (
|
|||||||
)
|
)
|
||||||
from aiogram.utils.markdown import hide_link
|
from aiogram.utils.markdown import hide_link
|
||||||
|
|
||||||
from BotCode.core.storage import storage
|
from bot.core import storage
|
||||||
from BotCode.utils import textmd2
|
from bot.loggers import logs
|
||||||
from BotCode.config import PARSE_MODE
|
|
||||||
from BotCode.loggers import logs
|
|
||||||
|
|
||||||
router = Router(name="inline_send")
|
router: Router = Router(name="inline_send")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -140,7 +138,7 @@ async def inline_query_handler(inline_query: InlineQuery):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Тело сообщения
|
# Тело сообщения
|
||||||
text = textmd2(post.get("text", ""))
|
text = post.get("text", "")
|
||||||
image = post.get("image", "")
|
image = post.get("image", "")
|
||||||
if image and image.startswith("http"):
|
if image and image.startswith("http"):
|
||||||
text = f"{hide_link(image)}{text}"
|
text = f"{hide_link(image)}{text}"
|
||||||
@@ -154,10 +152,7 @@ async def inline_query_handler(inline_query: InlineQuery):
|
|||||||
title=f"Пост {post_id}",
|
title=f"Пост {post_id}",
|
||||||
description=(post.get("text", "")[:100] + "...") if len(post.get("text", "")) > 100 else post.get(
|
description=(post.get("text", "")[:100] + "...") if len(post.get("text", "")) > 100 else post.get(
|
||||||
"text", ""),
|
"text", ""),
|
||||||
input_message_content=InputTextMessageContent(
|
input_message_content=InputTextMessageContent(message_text=text),
|
||||||
message_text=text,
|
|
||||||
parse_mode=PARSE_MODE
|
|
||||||
),
|
|
||||||
reply_markup=markup
|
reply_markup=markup
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
13
bot/handlers/post/__init__.py
Normal file
13
bot/handlers/post/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from aiogram import Router
|
||||||
|
from .create_posts import router as posts_router
|
||||||
|
from .post_list import router as post_list_router
|
||||||
|
|
||||||
|
# Настройки экспорта и роутера
|
||||||
|
__all__ = ("router", )
|
||||||
|
router: Router = Router(name="post_router")
|
||||||
|
|
||||||
|
# Подключение роутеров
|
||||||
|
router.include_routers(
|
||||||
|
post_list_router,
|
||||||
|
posts_router,
|
||||||
|
)
|
||||||
420
bot/handlers/post/create_posts.py
Normal file
420
bot/handlers/post/create_posts.py
Normal file
@@ -0,0 +1,420 @@
|
|||||||
|
# bot/modules/create_post.py
|
||||||
|
import re
|
||||||
|
import uuid
|
||||||
|
from threading import Lock
|
||||||
|
|
||||||
|
from aiogram import Router, F
|
||||||
|
from aiogram.types import (
|
||||||
|
Message, CallbackQuery,
|
||||||
|
InlineKeyboardButton, InlineKeyboardMarkup
|
||||||
|
)
|
||||||
|
from aiogram.fsm.state import State, StatesGroup
|
||||||
|
from aiogram.fsm.context import FSMContext
|
||||||
|
|
||||||
|
from bot.core import storage
|
||||||
|
|
||||||
|
router: Router = Router(name="create_post_router")
|
||||||
|
|
||||||
|
|
||||||
|
class PostState(StatesGroup):
|
||||||
|
waiting_for_text = State()
|
||||||
|
waiting_for_privacy = State()
|
||||||
|
waiting_for_id = State()
|
||||||
|
waiting_for_image = State()
|
||||||
|
waiting_for_buttons = State()
|
||||||
|
preview = State()
|
||||||
|
editing_choice = State()
|
||||||
|
|
||||||
|
|
||||||
|
post_id_lock: Lock = Lock()
|
||||||
|
|
||||||
|
|
||||||
|
# --- Utility functions ---
|
||||||
|
def make_inline_markup(rows: list[list[InlineKeyboardButton]]) -> InlineKeyboardMarkup:
|
||||||
|
return InlineKeyboardMarkup(inline_keyboard=rows)
|
||||||
|
|
||||||
|
|
||||||
|
def cancel_button() -> InlineKeyboardMarkup:
|
||||||
|
return make_inline_markup([[InlineKeyboardButton(text="Отмена", callback_data="cancel_creation")]])
|
||||||
|
|
||||||
|
|
||||||
|
def privacy_markup(is_private: bool) -> InlineKeyboardMarkup:
|
||||||
|
toggle = InlineKeyboardButton(
|
||||||
|
text="🔒 Приватный" if is_private else "🔓 Публичный",
|
||||||
|
callback_data="toggle_privacy"
|
||||||
|
)
|
||||||
|
cont = InlineKeyboardButton(text="Продолжить ➡️", callback_data="continue_creation")
|
||||||
|
return make_inline_markup([[toggle], [cont]])
|
||||||
|
|
||||||
|
|
||||||
|
def parse_buttons(text: str, post_id: str) -> list[list[dict]]:
|
||||||
|
"""
|
||||||
|
Поддерживается синтаксис:
|
||||||
|
Текст | msg:Только для боссов | 123,456 | msg:Для всех остальных
|
||||||
|
Текст | ntf:Без алерта | 789 | msg:Нет доступа
|
||||||
|
"""
|
||||||
|
rows: list[list[dict]] = []
|
||||||
|
button_index = 0
|
||||||
|
|
||||||
|
for line in text.splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# каждая строка может содержать несколько кнопок через ';'
|
||||||
|
btn_texts = [b.strip() for b in line.split(';') if b.strip()]
|
||||||
|
row: list[dict] = []
|
||||||
|
|
||||||
|
for raw in btn_texts:
|
||||||
|
parts = [p.strip() for p in raw.split('|')]
|
||||||
|
if len(parts) < 2:
|
||||||
|
raise ValueError(f"Неверный формат кнопки: '{raw}'")
|
||||||
|
|
||||||
|
btn = {"text": parts[0]}
|
||||||
|
primary_notification = None
|
||||||
|
primary_alert = False
|
||||||
|
allowed_ids = None
|
||||||
|
unauthorized_message = None
|
||||||
|
|
||||||
|
# обрабатываем параметры слева направо
|
||||||
|
for part in parts[1:]:
|
||||||
|
# URL / void
|
||||||
|
if part == "void":
|
||||||
|
btn["url"] = "http://void"
|
||||||
|
elif part.startswith("http") or part.startswith("tg://"):
|
||||||
|
btn["url"] = part
|
||||||
|
|
||||||
|
# первое уведомление (msg: — с алертом)
|
||||||
|
elif part.startswith("msg:") and primary_notification is None:
|
||||||
|
primary_notification = part.split(":", 1)[1]
|
||||||
|
primary_alert = True
|
||||||
|
|
||||||
|
# первое уведомление без алерта
|
||||||
|
elif part.startswith(("ntf:", "notification:")) and primary_notification is None:
|
||||||
|
primary_notification = part.split(":", 1)[1]
|
||||||
|
primary_alert = False
|
||||||
|
|
||||||
|
# список разрешённых ID
|
||||||
|
elif re.fullmatch(r'\d+(?:\s*,\s*\d+)*', part):
|
||||||
|
allowed_ids = [int(x.strip()) for x in part.split(",")]
|
||||||
|
|
||||||
|
# второе сообщение — для неавторизованных
|
||||||
|
elif part.startswith("msg:") and primary_notification is not None and allowed_ids is not None:
|
||||||
|
unauthorized_message = part.split(":", 1)[1]
|
||||||
|
|
||||||
|
# копирование текста
|
||||||
|
elif part.startswith("copy:"):
|
||||||
|
btn["callback_data"] = f"copy_{uuid.uuid4().hex}"
|
||||||
|
btn["copy_text"] = part.split(":", 1)[1]
|
||||||
|
|
||||||
|
# inline-параметры
|
||||||
|
elif part.startswith("inline:"):
|
||||||
|
btn["switch_inline_query"] = part.split(":", 1)[1]
|
||||||
|
elif part.startswith("inline_current:"):
|
||||||
|
btn["switch_inline_query_current_chat"] = part.split(":", 1)[1]
|
||||||
|
elif part.startswith("inline_chosen:"):
|
||||||
|
btn["switch_inline_query_chosen_chat"] = part.split(":", 1)[1]
|
||||||
|
|
||||||
|
# произвольный callback_data (если ещё не задан)
|
||||||
|
else:
|
||||||
|
if "callback_data" not in btn and "url" not in btn:
|
||||||
|
btn["callback_data"] = part
|
||||||
|
|
||||||
|
# если было уведомление — добавляем поля
|
||||||
|
if primary_notification is not None:
|
||||||
|
btn["callback_data"] = f"bt_{post_id}_{button_index}"
|
||||||
|
button_index += 1
|
||||||
|
btn["notification"] = primary_notification
|
||||||
|
btn["show_alert"] = primary_alert
|
||||||
|
|
||||||
|
if allowed_ids is not None:
|
||||||
|
btn["allowed_ids"] = allowed_ids
|
||||||
|
if unauthorized_message is not None:
|
||||||
|
btn["unauthorized_message"] = unauthorized_message
|
||||||
|
|
||||||
|
# финализируем кнопку
|
||||||
|
row.append(btn)
|
||||||
|
|
||||||
|
if row:
|
||||||
|
rows.append(row)
|
||||||
|
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
# --- Handlers ---
|
||||||
|
@router.message(F.text == "Создать пост📔")
|
||||||
|
async def start_creation(message: Message, state: FSMContext) -> None:
|
||||||
|
await state.set_state(PostState.waiting_for_text)
|
||||||
|
await state.update_data(private=False, buttons=[])
|
||||||
|
await message.reply(
|
||||||
|
text="Отправьте текст вашего поста:\n<i>Вы также можете использовать разметку</i>(<b>жирный</b>, <i>курсив</i> и <u>прочие</u>)!",
|
||||||
|
reply_markup=cancel_button()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(PostState.waiting_for_text)
|
||||||
|
async def got_text(message: Message, state: FSMContext) -> None:
|
||||||
|
html_text = message.html_text or message.text or message.caption or ""
|
||||||
|
await state.update_data(text=html_text)
|
||||||
|
await show_preview(message, state)
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(F.data == "toggle_privacy")
|
||||||
|
async def toggle_privacy(cq: CallbackQuery, state: FSMContext) -> None:
|
||||||
|
data = await state.get_data()
|
||||||
|
is_priv = not data.get('private', False)
|
||||||
|
await state.update_data(private=is_priv)
|
||||||
|
await cq.message.edit_reply_markup(reply_markup=privacy_markup(is_priv))
|
||||||
|
await cq.answer()
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(F.data == "continue_creation")
|
||||||
|
async def continue_to_id(cq: CallbackQuery, state: FSMContext) -> None:
|
||||||
|
await state.set_state(PostState.waiting_for_id)
|
||||||
|
await cq.message.edit_text(
|
||||||
|
"Введите уникальный ID поста (латиница, цифры, подчёрки):\n<i>Совет: инициалыРП_роль_тип_номер</i>\nПример: sgrp_dottore_post_4")
|
||||||
|
await cq.answer()
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(PostState.waiting_for_id)
|
||||||
|
async def got_id(message: Message, state: FSMContext) -> None:
|
||||||
|
pid = message.text.strip()
|
||||||
|
if not pid.replace('_', '').isalnum():
|
||||||
|
await message.reply(text="ID должен содержать только латиницу, цифры и подчёркивания.",
|
||||||
|
reply_markup=cancel_button())
|
||||||
|
return
|
||||||
|
with post_id_lock:
|
||||||
|
if not storage.is_post_available(pid):
|
||||||
|
await message.reply(text="Этот ID уже занят, введите другой:", reply_markup=cancel_button())
|
||||||
|
return
|
||||||
|
|
||||||
|
# Создаем клавиатуру с кнопкой "Без изображения"
|
||||||
|
image_markup = InlineKeyboardMarkup(inline_keyboard=[
|
||||||
|
[InlineKeyboardButton(text="🚫 Без изображения", callback_data="no_image")],
|
||||||
|
[InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_creation")]
|
||||||
|
])
|
||||||
|
|
||||||
|
await state.update_data(post_id=pid)
|
||||||
|
await state.set_state(PostState.waiting_for_image)
|
||||||
|
await message.reply(
|
||||||
|
text="Отправьте ссылку на изображение:\nПример: https://img4.teletype.in/files/f2/47/...\n\nСовет! Сохраняйте фотографии в teletype, а после копируйте ссылку на фотографию!\n\nИли нажмите 'Без изображения'.",
|
||||||
|
reply_markup=image_markup
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(F.data == "no_image", PostState.waiting_for_image)
|
||||||
|
async def no_image_callback(cq: CallbackQuery, state: FSMContext):
|
||||||
|
await state.update_data(image='')
|
||||||
|
await state.set_state(PostState.waiting_for_buttons)
|
||||||
|
await cq.message.delete()
|
||||||
|
|
||||||
|
buttons_markup = InlineKeyboardMarkup(inline_keyboard=[
|
||||||
|
[InlineKeyboardButton(text="🚫 Без кнопок", callback_data="no_buttons")],
|
||||||
|
[InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_creation")]
|
||||||
|
])
|
||||||
|
|
||||||
|
await cq.message.answer(
|
||||||
|
text="""Отправьте кнопки по шаблону:
|
||||||
|
Кнопка заглушка | void
|
||||||
|
Уведомление | msg:Для вас!
|
||||||
|
Уведомление в закрепе | ntf:Сообщение
|
||||||
|
Кнопка ссылка | https://google.com
|
||||||
|
Копирование | copy:Текст для копирования
|
||||||
|
|
||||||
|
Для уведомлений с ограничением:
|
||||||
|
Уведомление | msg:Для вас! | 123,456 | msg:Для всех остальных!
|
||||||
|
Уведомление без алерта | ntf:Сообщение | 789 | msg:Нет доступа
|
||||||
|
|
||||||
|
Разделять кнопки через ;
|
||||||
|
Кнопка1 | void ; Кнопка2 | void ; ....
|
||||||
|
|
||||||
|
Или нажмите "Без кнопок".""",
|
||||||
|
reply_markup=buttons_markup,
|
||||||
|
parse_mode=None
|
||||||
|
)
|
||||||
|
await cq.answer()
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(PostState.waiting_for_image)
|
||||||
|
async def got_image(message: Message, state: FSMContext) -> None:
|
||||||
|
img: str = message.text.strip()
|
||||||
|
if img.lower() in ('нет', 'no', 'none', 'без изображения'):
|
||||||
|
img: str = ''
|
||||||
|
|
||||||
|
await state.update_data(image=img)
|
||||||
|
await show_preview(message, state)
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(PostState.waiting_for_buttons, F.data == "no_buttons")
|
||||||
|
async def no_buttons_handler(callback: CallbackQuery, state: FSMContext) -> None:
|
||||||
|
await state.update_data(buttons=[])
|
||||||
|
await show_preview(callback.message, state)
|
||||||
|
await callback.answer()
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(PostState.waiting_for_buttons, F.data == "finish_buttons")
|
||||||
|
async def finish_buttons_handler(callback: CallbackQuery, state: FSMContext) -> None:
|
||||||
|
data = await state.get_data()
|
||||||
|
|
||||||
|
# Формируем финальные кнопки
|
||||||
|
final = []
|
||||||
|
for row in data.get('buttons', []):
|
||||||
|
final_row = []
|
||||||
|
for b in row:
|
||||||
|
btn = {"text": b["text"]}
|
||||||
|
if "url" in b:
|
||||||
|
btn["url"] = b["url"]
|
||||||
|
if "switch_inline_query" in b:
|
||||||
|
btn["switch_inline_query"] = b["switch_inline_query"]
|
||||||
|
if "callback_data" in b:
|
||||||
|
btn["callback_data"] = b["callback_data"]
|
||||||
|
if "notification" in b:
|
||||||
|
btn["notification"] = b["notification"]
|
||||||
|
btn["show_alert"] = b.get("show_alert", False)
|
||||||
|
final_row.append(btn)
|
||||||
|
final.append(final_row)
|
||||||
|
|
||||||
|
await state.update_data(buttons=final)
|
||||||
|
await show_preview(callback.message, state)
|
||||||
|
await callback.answer()
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(F.data == "cancel_creation")
|
||||||
|
async def cancel_handler(callback: CallbackQuery, state: FSMContext):
|
||||||
|
await state.clear()
|
||||||
|
await callback.message.edit_text("❌ Создание поста отменено")
|
||||||
|
await callback.answer()
|
||||||
|
|
||||||
|
|
||||||
|
# --- Preview and Edit Handlers ---
|
||||||
|
async def show_preview(message: Message, state: FSMContext) -> None:
|
||||||
|
data = await state.get_data()
|
||||||
|
text = data.get('text', '')
|
||||||
|
image = data.get('image', '')
|
||||||
|
buttons = data.get('buttons', [])
|
||||||
|
private = data.get('private', False)
|
||||||
|
post_id = data.get('post_id', '')
|
||||||
|
|
||||||
|
# Формируем текст предпросмотра
|
||||||
|
preview_text = f"<b>ПРЕДПРОСМОТР ПОСТА</b>\n\n{text}\n\n"
|
||||||
|
preview_text += f"🆔 ID: <code>{post_id}</code>\n"
|
||||||
|
preview_text += f"🔒 Приватность: {'Приватный' if private else 'Публичный'}\n"
|
||||||
|
|
||||||
|
if image:
|
||||||
|
preview_text += f"🖼 Изображение: {image}\n"
|
||||||
|
else:
|
||||||
|
preview_text += f"🖼 Изображение: отсутствует\n"
|
||||||
|
|
||||||
|
if buttons:
|
||||||
|
preview_text += "\n🔘 Кнопки:\n"
|
||||||
|
for row in buttons:
|
||||||
|
preview_text += " | ".join([btn['text'] for btn in row]) + "\n"
|
||||||
|
else:
|
||||||
|
preview_text += "\n🔘 Кнопки: отсутствуют\n"
|
||||||
|
|
||||||
|
# Клавиатура предпросмотра
|
||||||
|
preview_markup = InlineKeyboardMarkup(inline_keyboard=[
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(text="Изменить", callback_data="edit_post"),
|
||||||
|
InlineKeyboardButton(text="Подтвердить", callback_data="confirm_post")
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(text="Отменить создание", callback_data="cancel_creation")
|
||||||
|
]
|
||||||
|
])
|
||||||
|
|
||||||
|
await state.set_state(PostState.preview)
|
||||||
|
await message.answer(preview_text, reply_markup=preview_markup, disable_web_page_preview=True)
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(PostState.preview, F.data == "edit_post")
|
||||||
|
async def edit_post_handler(cq: CallbackQuery, state: FSMContext) -> None:
|
||||||
|
# Клавиатура выбора поля для редактирования
|
||||||
|
edit_markup = InlineKeyboardMarkup(inline_keyboard=[
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(text="Текст", callback_data="edit_field:text"),
|
||||||
|
InlineKeyboardButton(text="Изображение", callback_data="edit_field:image"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(text="Кнопки", callback_data="edit_field:buttons"),
|
||||||
|
InlineKeyboardButton(text="ID", callback_data="edit_field:id"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(text="Приватность", callback_data="edit_field:privacy"),
|
||||||
|
InlineKeyboardButton(text="Назад", callback_data="back_to_preview"),
|
||||||
|
]
|
||||||
|
])
|
||||||
|
|
||||||
|
await cq.message.edit_text("Выберите что изменить:", reply_markup=edit_markup)
|
||||||
|
await state.set_state(PostState.editing_choice)
|
||||||
|
await cq.answer()
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(PostState.editing_choice, F.data == "back_to_preview")
|
||||||
|
async def back_to_preview(cq: CallbackQuery, state: FSMContext) -> None:
|
||||||
|
await show_preview(cq.message, state)
|
||||||
|
await cq.answer()
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(PostState.editing_choice, F.data.startswith("edit_field:"))
|
||||||
|
async def handle_field_edit(cq: CallbackQuery, state: FSMContext) -> None:
|
||||||
|
field = cq.data.split(":")[1]
|
||||||
|
|
||||||
|
if field == "text":
|
||||||
|
await state.set_state(PostState.waiting_for_text)
|
||||||
|
await cq.message.edit_text("Введите новый текст поста:", reply_markup=cancel_button())
|
||||||
|
|
||||||
|
elif field == "image":
|
||||||
|
await state.set_state(PostState.waiting_for_image)
|
||||||
|
markup = InlineKeyboardMarkup(inline_keyboard=[
|
||||||
|
[InlineKeyboardButton(text="🚫 Без изображения", callback_data="no_image")],
|
||||||
|
[InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_creation")]
|
||||||
|
])
|
||||||
|
await cq.message.edit_text(
|
||||||
|
"Отправьте новую ссылку на изображение или нажмите 'Без изображения':",
|
||||||
|
reply_markup=markup
|
||||||
|
)
|
||||||
|
|
||||||
|
elif field == "buttons":
|
||||||
|
await state.set_state(PostState.waiting_for_buttons)
|
||||||
|
markup = InlineKeyboardMarkup(inline_keyboard=[
|
||||||
|
[InlineKeyboardButton(text="🚫 Без кнопок", callback_data="no_buttons")],
|
||||||
|
[InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_creation")]
|
||||||
|
])
|
||||||
|
await cq.message.edit_text(
|
||||||
|
"Отправьте новые кнопки по шаблону или нажмите 'Без кнопок':",
|
||||||
|
reply_markup=markup
|
||||||
|
)
|
||||||
|
|
||||||
|
elif field == "id":
|
||||||
|
await state.set_state(PostState.waiting_for_id)
|
||||||
|
await cq.message.edit_text("Введите новый ID поста:", reply_markup=cancel_button())
|
||||||
|
|
||||||
|
elif field == "privacy":
|
||||||
|
data = await state.get_data()
|
||||||
|
await state.set_state(PostState.waiting_for_privacy)
|
||||||
|
await cq.message.edit_text(
|
||||||
|
"Измените приватность поста:",
|
||||||
|
reply_markup=privacy_markup(data.get('private', False))
|
||||||
|
)
|
||||||
|
|
||||||
|
await cq.answer()
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(PostState.preview, F.data == "confirm_post")
|
||||||
|
async def confirm_post_handler(cq: CallbackQuery, state: FSMContext) -> None:
|
||||||
|
data = await state.get_data()
|
||||||
|
post_id = data['post_id']
|
||||||
|
|
||||||
|
# Сохранение поста в хранилище
|
||||||
|
storage.save_post(post_id, {
|
||||||
|
'text': data['text'],
|
||||||
|
'image': data.get('image', ''),
|
||||||
|
'buttons': data.get('buttons', []),
|
||||||
|
'private': data['private'],
|
||||||
|
'post_id': post_id
|
||||||
|
})
|
||||||
|
|
||||||
|
await cq.message.edit_text(f"✅ Пост успешно создан с ID: <code>{post_id}</code>")
|
||||||
|
await state.clear()
|
||||||
|
await cq.answer()
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
from math import ceil
|
from math import ceil
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
from aiogram import Router, F
|
from aiogram import Router, F
|
||||||
|
from aiogram.fsm.context import FSMContext
|
||||||
from aiogram.types import (
|
from aiogram.types import (
|
||||||
Message, CallbackQuery,
|
Message, CallbackQuery,
|
||||||
InlineKeyboardButton, InlineKeyboardMarkup,
|
InlineKeyboardButton, InlineKeyboardMarkup,
|
||||||
@@ -8,14 +11,12 @@ from aiogram.types import (
|
|||||||
from aiogram.exceptions import TelegramBadRequest
|
from aiogram.exceptions import TelegramBadRequest
|
||||||
from aiogram.utils.markdown import hide_link
|
from aiogram.utils.markdown import hide_link
|
||||||
|
|
||||||
from BotCode.core.storage import storage
|
from bot.core import storage
|
||||||
from BotCode.utils.pagination import create_pagination_buttons
|
from bot.utils import pagination_btn
|
||||||
from BotCode.utils import textmd2
|
|
||||||
from BotCode.config import PARSE_MODE
|
|
||||||
|
|
||||||
router = Router(name="posts_manager")
|
router: Router = Router(name="posts_manager_router")
|
||||||
|
|
||||||
PAGE_SIZE = 5
|
PAGE_SIZE: Final[int] = 5
|
||||||
|
|
||||||
async def send_posts_list(
|
async def send_posts_list(
|
||||||
message: Message = None,
|
message: Message = None,
|
||||||
@@ -54,7 +55,7 @@ async def send_posts_list(
|
|||||||
rows.append([btn])
|
rows.append([btn])
|
||||||
|
|
||||||
# Пагинация
|
# Пагинация
|
||||||
nav_buttons = create_pagination_buttons(
|
nav_buttons = pagination_btn(
|
||||||
action="open_post_list",
|
action="open_post_list",
|
||||||
page=page,
|
page=page,
|
||||||
total_posts=total,
|
total_posts=total,
|
||||||
@@ -67,7 +68,7 @@ async def send_posts_list(
|
|||||||
rows.append([InlineKeyboardButton(text="Закрыть❌", callback_data="cancel_list")])
|
rows.append([InlineKeyboardButton(text="Закрыть❌", callback_data="cancel_list")])
|
||||||
|
|
||||||
keyboard = InlineKeyboardMarkup(inline_keyboard=rows)
|
keyboard = InlineKeyboardMarkup(inline_keyboard=rows)
|
||||||
header = "Список ваших постов:"
|
header: str = "Список ваших постов:"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if callback_query:
|
if callback_query:
|
||||||
@@ -83,21 +84,25 @@ async def send_posts_list(
|
|||||||
|
|
||||||
# --- Хендлеры списка ---
|
# --- Хендлеры списка ---
|
||||||
@router.message(F.text.lower() == "посмотреть список📋")
|
@router.message(F.text.lower() == "посмотреть список📋")
|
||||||
async def cmd_list(message: Message):
|
async def cmd_list(message: Message, state: FSMContext):
|
||||||
|
# Сбрасываем состояние перед показом списка
|
||||||
|
await state.clear()
|
||||||
await send_posts_list(message=message)
|
await send_posts_list(message=message)
|
||||||
|
|
||||||
@router.callback_query(F.data == "open_post_list")
|
@router.callback_query(F.data == "open_post_list")
|
||||||
async def cb_open_list(cq: CallbackQuery):
|
async def cb_open_list(cq: CallbackQuery, state: FSMContext):
|
||||||
|
await state.clear()
|
||||||
await send_posts_list(callback_query=cq)
|
await send_posts_list(callback_query=cq)
|
||||||
await cq.answer()
|
await cq.answer()
|
||||||
|
|
||||||
@router.callback_query(lambda c: c.data and c.data.startswith("open_post_list_page_"))
|
@router.callback_query(F.data.startswith("open_post_list_page_"))
|
||||||
async def cb_paginate(cq: CallbackQuery):
|
async def cb_paginate(cq: CallbackQuery, state: FSMContext):
|
||||||
try:
|
try:
|
||||||
page = int(cq.data.rsplit("_", 1)[-1])
|
page = int(cq.data.rsplit("_", 1)[-1])
|
||||||
except ValueError:
|
except ValueError:
|
||||||
await cq.answer("Некорректная страница", show_alert=True)
|
await cq.answer("Некорректная страница", show_alert=True)
|
||||||
return
|
return
|
||||||
|
await state.clear()
|
||||||
await send_posts_list(callback_query=cq, page=page)
|
await send_posts_list(callback_query=cq, page=page)
|
||||||
await cq.answer()
|
await cq.answer()
|
||||||
|
|
||||||
@@ -106,9 +111,9 @@ async def cb_cancel(cq: CallbackQuery):
|
|||||||
await cq.message.delete()
|
await cq.message.delete()
|
||||||
await cq.answer()
|
await cq.answer()
|
||||||
|
|
||||||
# --- Просмотр отдельного поста ---
|
@router.callback_query(F.data.startswith("view_post_"))
|
||||||
@router.callback_query(lambda c: c.data and c.data.startswith("view_post_"))
|
|
||||||
async def view_post_callback(cq: CallbackQuery):
|
async def view_post_callback(cq: CallbackQuery):
|
||||||
|
"""Просмотр отдельного поста"""
|
||||||
pid = cq.data.replace("view_post_", "")
|
pid = cq.data.replace("view_post_", "")
|
||||||
uid = cq.from_user.id
|
uid = cq.from_user.id
|
||||||
posts = storage.load_user_posts(uid)
|
posts = storage.load_user_posts(uid)
|
||||||
@@ -117,7 +122,7 @@ async def view_post_callback(cq: CallbackQuery):
|
|||||||
return
|
return
|
||||||
|
|
||||||
post = posts[pid]
|
post = posts[pid]
|
||||||
text = textmd2(post.get("text", ""))
|
text = post.get("text", "")
|
||||||
img = post.get("image", "")
|
img = post.get("image", "")
|
||||||
if img.startswith("http"):
|
if img.startswith("http"):
|
||||||
text = f"{hide_link(img)}{text}"
|
text = f"{hide_link(img)}{text}"
|
||||||
@@ -181,20 +186,24 @@ async def view_post_callback(cq: CallbackQuery):
|
|||||||
InlineKeyboardButton(text="Удалить❌", callback_data=f"delete_post_{pid}"),
|
InlineKeyboardButton(text="Удалить❌", callback_data=f"delete_post_{pid}"),
|
||||||
InlineKeyboardButton(text="Назад◀️", callback_data="open_post_list")
|
InlineKeyboardButton(text="Назад◀️", callback_data="open_post_list")
|
||||||
])
|
])
|
||||||
|
rows.append(
|
||||||
|
[InlineKeyboardButton(text="Отправить↪️", switch_inline_query=f"{pid}")])
|
||||||
|
|
||||||
keyboard = InlineKeyboardMarkup(inline_keyboard=rows)
|
keyboard = InlineKeyboardMarkup(inline_keyboard=rows)
|
||||||
|
|
||||||
await cq.message.answer(text=text, reply_markup=keyboard, parse_mode=PARSE_MODE)
|
await cq.message.answer(text=text, reply_markup=keyboard)
|
||||||
await cq.message.delete()
|
await cq.message.delete()
|
||||||
await cq.answer()
|
await cq.answer()
|
||||||
|
|
||||||
# --- Удаление поста ---
|
|
||||||
@router.callback_query(lambda c: c.data and c.data.startswith("delete_post_"))
|
@router.callback_query(lambda c: c.data and c.data.startswith("delete_post_"))
|
||||||
async def delete_post_callback(cq: CallbackQuery):
|
async def delete_post_callback(cq: CallbackQuery, state: FSMContext):
|
||||||
|
"""Удаление поста."""
|
||||||
pid = cq.data.replace("delete_post_", "")
|
pid = cq.data.replace("delete_post_", "")
|
||||||
uid = cq.from_user.id
|
uid = cq.from_user.id
|
||||||
if storage.delete_user_post(uid, pid):
|
if storage.delete_user_post(uid, pid):
|
||||||
await cq.answer(f"Пост {pid} удалён")
|
await cq.answer(f"Пост {pid} удалён")
|
||||||
|
await state.clear()
|
||||||
await send_posts_list(callback_query=cq)
|
await send_posts_list(callback_query=cq)
|
||||||
else:
|
else:
|
||||||
await cq.answer("Не удалось удалить пост", show_alert=True)
|
await cq.answer(text="Не удалось удалить пост", show_alert=True)
|
||||||
1
bot/keyboards/__init__.py
Normal file
1
bot/keyboards/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# bot/keyboards/__init__.py
|
||||||
3
bot/keyboards/inline/__init__.py
Normal file
3
bot/keyboards/inline/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# bot/keyboards/inline/__init__.py
|
||||||
|
|
||||||
|
from .decision import *
|
||||||
22
bot/keyboards/inline/decision.py
Normal file
22
bot/keyboards/inline/decision.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# bot/keyboards/decision.py
|
||||||
|
|
||||||
|
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||||
|
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||||
|
|
||||||
|
# Настройка экспорта
|
||||||
|
__all__ = ("get_decision_keyboard",)
|
||||||
|
|
||||||
|
def get_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()
|
||||||
2
bot/keyboards/reply/__init__.py
Normal file
2
bot/keyboards/reply/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# bot/keyboards/reply/__init__.py
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ from loguru import logger
|
|||||||
from aiogram.types import Message, User
|
from aiogram.types import Message, User
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from config import LogConfig
|
from configs.config import LogConfig
|
||||||
except ImportError:
|
except ImportError:
|
||||||
class LogConfig:
|
class LogConfig:
|
||||||
"""Запасные настройки логирования, если config недоступен."""
|
"""Запасные настройки логирования, если config недоступен."""
|
||||||
1
bot/templates/__init__.py
Normal file
1
bot/templates/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .message_callback import *
|
||||||
77
bot/templates/message_callback.py
Normal file
77
bot/templates/message_callback.py
Normal 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 = markup.as_markup()
|
||||||
|
elif isinstance(markup, ReplyKeyboardBuilder):
|
||||||
|
reply_markup = 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,
|
||||||
|
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
|
||||||
|
)
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from .interesting_facts import *
|
||||||
from .md2_escape import *
|
from .md2_escape import *
|
||||||
from .usernames import *
|
from .usernames import *
|
||||||
from .pagination import *
|
from .pagination import *
|
||||||
29
bot/utils/interesting_facts.py
Normal file
29
bot/utils/interesting_facts.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
from random import choice
|
||||||
|
|
||||||
|
from configs.config import Lists
|
||||||
|
|
||||||
|
# Настройки экспорта
|
||||||
|
__all__ = ("interesting_fact",)
|
||||||
|
|
||||||
|
|
||||||
|
def interesting_fact(mode: str = "факт", lists: list[str] = None) -> str:
|
||||||
|
"""
|
||||||
|
Возвращает случайный факт, анекдот или цитату, в зависимости от режима.
|
||||||
|
|
||||||
|
:param mode: Строка, определяющая тип контента ("факт", "анекдот", "цитата").
|
||||||
|
:param lists: Необязательный список строк, из которого можно выбирать вручную.
|
||||||
|
:return: Случайный элемент из соответствующего списка.
|
||||||
|
"""
|
||||||
|
if lists is not None:
|
||||||
|
return choice(lists)
|
||||||
|
|
||||||
|
mode: str = 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)
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
# BotCode/utils/md2_escape.py
|
from re import sub, escape
|
||||||
from BotCode.config import PARSE_MODE
|
from configs.config import BotSettings
|
||||||
|
|
||||||
# Настройка экспорта в модули
|
# Настройка экспорта в модули
|
||||||
__all__ = ("textmd2",)
|
__all__ = ("textmd2",)
|
||||||
|
|
||||||
def textmd2(msg: str,
|
def textmd2(msg: str,
|
||||||
parse_mode: str = PARSE_MODE,
|
parse_mode: str = BotSettings.PARSE_MODE,
|
||||||
special_chars: str = r"_*[]()~`>#+-=|{}.!") -> str:
|
special_chars: str = r"_*[]()~`>#+-=|{}.!") -> str:
|
||||||
"""
|
"""
|
||||||
Экранирует специальные символы MarkdownV2 в переданном тексте.
|
Экранирует специальные символы MarkdownV2 в переданном тексте.
|
||||||
@@ -18,7 +18,6 @@ def textmd2(msg: str,
|
|||||||
:raises TypeError: Если передан не строковый тип данных.
|
:raises TypeError: Если передан не строковый тип данных.
|
||||||
:raises ValueError: Если parse_mode задан некорректно.
|
:raises ValueError: Если parse_mode задан некорректно.
|
||||||
"""
|
"""
|
||||||
from re import sub, escape
|
|
||||||
|
|
||||||
if not isinstance(msg, str):
|
if not isinstance(msg, str):
|
||||||
raise TypeError(f"Ожидается строка, но получено {type(msg).__name__}")
|
raise TypeError(f"Ожидается строка, но получено {type(msg).__name__}")
|
||||||
28
bot/utils/pagination.py
Normal file
28
bot/utils/pagination.py
Normal 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
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
# BotCode/utils/username.py
|
|
||||||
from aiogram.types import Message
|
from aiogram.types import Message
|
||||||
|
|
||||||
# Настройка экспорта в модули
|
# Настройка экспорта в модули
|
||||||
2
configs/__init__.py
Normal file
2
configs/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
from .config import *
|
||||||
|
from .cmd_list import *
|
||||||
77
configs/cmd_list.py
Normal file
77
configs/cmd_list.py
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
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"
|
||||||
|
],
|
||||||
|
}
|
||||||
381
configs/config.py
Normal file
381
configs/config.py
Normal file
@@ -0,0 +1,381 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
from typing import ClassVar, Final, Optional, Any
|
||||||
|
|
||||||
|
from pydantic import field_validator, model_validator, HttpUrl
|
||||||
|
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 = False
|
||||||
|
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_HOST: str = "https://bot_1.primo.dpdns.org"
|
||||||
|
WEBHOOK_PATH: str = "/webhook"
|
||||||
|
WEBHOOK_URL: str = f"{WEBHOOK_HOST}{WEBHOOK_PATH}"
|
||||||
|
|
||||||
|
# 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: int = 0
|
||||||
|
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[HttpUrl] = None
|
||||||
|
RP_OWNER: Optional[str] = None
|
||||||
|
|
||||||
|
# Права администратора
|
||||||
|
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 = {"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 = ''.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', 'ADMIN_ID', '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 = 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) # Исправлено: setting вместо settings
|
||||||
|
return setting
|
||||||
|
|
||||||
|
@model_validator(mode='after')
|
||||||
|
def set_dynamic_descriptions(cls, setting: "Settings") -> "Settings":
|
||||||
|
"""Динамическая установка описаний бота"""
|
||||||
|
if setting.BOT_DESCRIPTION is None:
|
||||||
|
# Исправлено: setting вместо settings
|
||||||
|
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_HOST = settings.WEBHOOK_HOST
|
||||||
|
WEBHOOK_PATH = settings.WEBHOOK_PATH
|
||||||
|
WEBHOOK_URL = settings.WEBHOOK_URL
|
||||||
|
|
||||||
|
|
||||||
|
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[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
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
RP_OWNER: str = settings.RP_OWNER
|
||||||
|
|
||||||
|
|
||||||
|
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',
|
||||||
|
)
|
||||||
8
docker-compose.yml
Normal file
8
docker-compose.yml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
services:
|
||||||
|
app:
|
||||||
|
build: .
|
||||||
|
container_name: SystemReverseRPBot
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
working_dir: /app
|
||||||
|
command: python main.py
|
||||||
92
env_example
Normal file
92
env_example
Normal 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
|
||||||
33
main.py
33
main.py
@@ -1,30 +1,25 @@
|
|||||||
# main.py
|
# main.py
|
||||||
from aiogram import Bot, Dispatcher
|
# Основной код проекта, который и соединяет в себе все его возможности
|
||||||
from aiogram.client.default import DefaultBotProperties
|
|
||||||
from BotCode.config import BOT_TOKEN, BOT_DEBUG_TOKEN, DEBUG_MODE, PARSE_MODE
|
|
||||||
|
|
||||||
dp: Dispatcher = Dispatcher()
|
from asyncio import run
|
||||||
TOKEN: str = BOT_DEBUG_TOKEN if DEBUG_MODE else BOT_TOKEN
|
from middleware.loggers import setup_logging
|
||||||
bot: Bot = Bot(
|
from bot import *
|
||||||
token=TOKEN,
|
|
||||||
default=DefaultBotProperties(
|
|
||||||
parse_mode=PARSE_MODE,
|
|
||||||
link_preview_show_above_text=True,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def main() -> None:
|
async def main() -> None:
|
||||||
from aiogram.types import User
|
"""Входная точка проекта. Запуск бота."""
|
||||||
from BotCode.loggers import logs
|
# Запуск логирования
|
||||||
from BotCode.handlers import router as main_router
|
setup_logging()
|
||||||
|
|
||||||
bot_info: User = await bot.get_me()
|
# Получение информации о боте
|
||||||
logs.start(text=f"Бот @{bot_info.username} запущен!")
|
await BotInfo.setup(bot)
|
||||||
|
|
||||||
dp.include_router(main_router)
|
# Подключение главного маршрутизатора
|
||||||
|
dp.include_router(router)
|
||||||
|
|
||||||
|
# Включение опроса бота
|
||||||
await dp.start_polling(bot)
|
await dp.start_polling(bot)
|
||||||
|
|
||||||
|
|
||||||
|
# Вечная загрузка бота
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
from asyncio import run
|
|
||||||
run(main())
|
run(main())
|
||||||
|
|||||||
1
middleware/loggers/__init__.py
Normal file
1
middleware/loggers/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .logs import *
|
||||||
234
middleware/loggers/logs.py
Normal file
234
middleware/loggers/logs.py
Normal 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
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,19 +0,0 @@
|
|||||||
[project]
|
|
||||||
name = "primostorybot"
|
|
||||||
version = "1.4"
|
|
||||||
description = "Бот для отправки постов с кнопками и разметкой сообщений"
|
|
||||||
authors = [
|
|
||||||
{name = "Verum",email = "sergeyzavalin@outlook.com"}
|
|
||||||
]
|
|
||||||
license = {text = "None"}
|
|
||||||
readme = "README.md"
|
|
||||||
requires-python = ">=3.10,<4.0"
|
|
||||||
dependencies = [
|
|
||||||
"aiogram (>=3.20.0.post0,<4.0.0)",
|
|
||||||
"loguru (>=0.7.3,<0.8.0)",
|
|
||||||
"dotenv (>=0.9.9,<0.10.0)"
|
|
||||||
]
|
|
||||||
|
|
||||||
[build-system]
|
|
||||||
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
||||||
build-backend = "poetry.core.masonry.api"
|
|
||||||
BIN
requirements.txt
Normal file
BIN
requirements.txt
Normal file
Binary file not shown.
Reference in New Issue
Block a user