Версия 1.0
This commit is contained in:
36
.dockerignore
Normal file
36
.dockerignore
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# .dockerignore: Исключения для Docker сборки
|
||||||
|
# Игнорировать всё, кроме необходимого для production
|
||||||
|
|
||||||
|
**/.git
|
||||||
|
**/.gitignore
|
||||||
|
**/.dockerignore
|
||||||
|
**/Dockerfile
|
||||||
|
**/README.md
|
||||||
|
|
||||||
|
# Директории
|
||||||
|
**/__pycache__
|
||||||
|
**/.mypy_cache
|
||||||
|
**/.pytest_cache
|
||||||
|
**/.idea
|
||||||
|
**/.vscode
|
||||||
|
**/test
|
||||||
|
**/tests
|
||||||
|
**/docs
|
||||||
|
**/examples
|
||||||
|
|
||||||
|
# Файлы
|
||||||
|
**/*.pyc
|
||||||
|
**/*.pyo
|
||||||
|
**/*.pyd
|
||||||
|
**/*.egg-info
|
||||||
|
**/*.log
|
||||||
|
**/*.logs
|
||||||
|
**/*.sqlite
|
||||||
|
**/*.db
|
||||||
|
config/.env
|
||||||
|
**/docker-compose*
|
||||||
|
|
||||||
|
# Артефакты сборки
|
||||||
|
**/build
|
||||||
|
**/dist
|
||||||
|
**/node_modules
|
||||||
10
.env
Normal file
10
.env
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
BOT_TOKEN=7694271285:AAEp9AbA72NRPNIJShDfvcL34awHD67Uvug
|
||||||
|
BOT_DEBUG_TOKEN=7403842222:AAGUFZEQiICZhsvRHSzjHhQp8YXqKb8jL6I
|
||||||
|
|
||||||
|
ADMIN_ID=[6751720805,1686743480,1979597550,1191474440]
|
||||||
|
|
||||||
|
PARSE_MODE=HTML
|
||||||
|
POST_DIR=posts
|
||||||
|
|
||||||
|
LOGGING_TO_CONSOLE=False
|
||||||
|
DEBUG_MODE=False
|
||||||
75
.gitattributes
vendored
Normal file
75
.gitattributes
vendored
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# .gitattributes
|
||||||
|
# === Git LFS (для больших бинарных файлов, ИИ-моделей, архивов и т.п.) ===
|
||||||
|
*.7z filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.arrow filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.bin filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.ftz filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.gz filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.h5 filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.joblib filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.model filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.npy filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.npz filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.onnx filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.ot filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.parquet filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.pb filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.pickle filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.pkl filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.pt filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.pth filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.rar filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
||||||
|
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.tar filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.tflite filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.tgz filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.wasm filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.xz filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.zip filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.zst filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|
||||||
|
# === Конец строки и автоопределение текста ===
|
||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
# === Текстовые файлы (принудительно) ===
|
||||||
|
*.html text
|
||||||
|
*.css text
|
||||||
|
*.js text
|
||||||
|
*.json text
|
||||||
|
*.md text
|
||||||
|
*.yml text
|
||||||
|
*.yaml text
|
||||||
|
*.xml text
|
||||||
|
*.txt text
|
||||||
|
|
||||||
|
# === Изображения ===
|
||||||
|
*.png binary
|
||||||
|
*.jpg binary
|
||||||
|
*.jpeg binary
|
||||||
|
*.gif binary
|
||||||
|
*.bmp binary
|
||||||
|
*.webp binary
|
||||||
|
*.ico binary
|
||||||
|
*.svg text # SVG можно диффить
|
||||||
|
|
||||||
|
# === Шрифты ===
|
||||||
|
*.eot binary
|
||||||
|
*.ttf binary
|
||||||
|
*.woff binary
|
||||||
|
*.woff2 binary
|
||||||
|
*.otf binary
|
||||||
|
|
||||||
|
# === GitHub Linguist (для языка проекта на GitHub) ===
|
||||||
|
*.html linguist-language=HTML
|
||||||
|
*.css linguist-language=CSS
|
||||||
|
*.js linguist-language=JavaScript
|
||||||
|
*.json linguist-language=JSON
|
||||||
|
*.md linguist-language=Markdown
|
||||||
67
.gitignore
vendored
Normal file
67
.gitignore
vendored
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# .gitignore: Игнорируемые файлы для Python проектов
|
||||||
|
# Подробнее: https://github.com/github/gitignore/blob/main/Python.gitignore
|
||||||
|
|
||||||
|
### Python ###
|
||||||
|
# Виртуальные окружения и настройки
|
||||||
|
config/.env
|
||||||
|
../../../../Desktop/PostBot/.venv
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Кэш интерпретатора
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# Пакеты и сборки
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
*.eggs
|
||||||
|
|
||||||
|
# Poetry
|
||||||
|
poetry.lock
|
||||||
|
.pypoetry/
|
||||||
|
|
||||||
|
### Логи и БД ###
|
||||||
|
*.log
|
||||||
|
*.logs
|
||||||
|
*.log.*
|
||||||
|
*.logs.*
|
||||||
|
log/
|
||||||
|
logs/
|
||||||
|
*.sqlite
|
||||||
|
*.db
|
||||||
|
|
||||||
|
### IDE ###
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.sublime-*
|
||||||
|
|
||||||
|
### OS ###
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
### Тестирование ###
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.pytest_cache/
|
||||||
|
.mypy_cache/
|
||||||
14
.idea/PRIMOSTORYFINAL.iml
generated
Normal file
14
.idea/PRIMOSTORYFINAL.iml
generated
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="PYTHON_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/BotCode" 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/utils" isTestSource="false" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="jdk" jdkName="Python 3.13 (PRIMOSTORYFINAL)" jdkType="Python SDK" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<settings>
|
||||||
|
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||||
|
<version value="1.0" />
|
||||||
|
</settings>
|
||||||
|
</component>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/PRIMOSTORYFINAL.iml" filepath="$PROJECT_DIR$/.idea/PRIMOSTORYFINAL.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
142
.idea/workspace.xml
generated
Normal file
142
.idea/workspace.xml
generated
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AutoImportSettings">
|
||||||
|
<option name="autoReloadType" value="SELECTIVE" />
|
||||||
|
</component>
|
||||||
|
<component name="ChangeListManager">
|
||||||
|
<list default="true" id="976a6336-6952-45ae-989f-7b10c5e394d4" name="Changes" comment="">
|
||||||
|
<change afterPath="$PROJECT_DIR$/.dockerignore" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/.env" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/.gitattributes" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/.gitignore" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/.idea/PRIMOSTORYFINAL.iml" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/.idea/inspectionProfiles/profiles_settings.xml" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/.idea/modules.xml" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/.idea/vcs.xml" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/BotCode/__init__.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/BotCode/config.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/BotCode/core/__init__.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/BotCode/core/storage.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/BotCode/handlers/__init__.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/BotCode/handlers/callback.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/BotCode/handlers/commands/__init__.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/BotCode/handlers/commands/start_cmd.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/BotCode/handlers/inline.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/BotCode/handlers/post/__init__.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/BotCode/handlers/post/create_posts.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/BotCode/handlers/post/post_list.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/BotCode/loggers/__init__.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/BotCode/loggers/logs.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/BotCode/utils/__init__.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/BotCode/utils/md2_escape.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/BotCode/utils/pagination.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/BotCode/utils/usernames.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/Dockerfile" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/LICENSE" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/README.md" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/assets/start.jpg" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/main.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/postsOLD/posts_6751720805.json" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/pyproject.toml" afterDir="false" />
|
||||||
|
</list>
|
||||||
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
|
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||||
|
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||||
|
</component>
|
||||||
|
<component name="FileTemplateManagerImpl">
|
||||||
|
<option name="RECENT_TEMPLATES">
|
||||||
|
<list>
|
||||||
|
<option value="Python Script" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
<component name="Git.Settings">
|
||||||
|
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectColorInfo"><![CDATA[{
|
||||||
|
"associatedIndex": 2
|
||||||
|
}]]></component>
|
||||||
|
<component name="ProjectId" id="2xL1onhKjANEmVgLSfNYMKmFWc4" />
|
||||||
|
<component name="ProjectViewState">
|
||||||
|
<option name="hideEmptyMiddlePackages" value="true" />
|
||||||
|
<option name="showLibraryContents" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PropertiesComponent"><![CDATA[{
|
||||||
|
"keyToString": {
|
||||||
|
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
||||||
|
"Python.main.executor": "Run",
|
||||||
|
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||||
|
"RunOnceActivity.git.unshallow": "true",
|
||||||
|
"git-widget-placeholder": "master",
|
||||||
|
"node.js.detected.package.eslint": "true",
|
||||||
|
"node.js.detected.package.tslint": "true",
|
||||||
|
"node.js.selected.package.eslint": "(autodetect)",
|
||||||
|
"node.js.selected.package.tslint": "(autodetect)",
|
||||||
|
"nodejs_package_manager_path": "npm",
|
||||||
|
"vue.rearranger.settings.migration": "true"
|
||||||
|
}
|
||||||
|
}]]></component>
|
||||||
|
<component name="RecentsManager">
|
||||||
|
<key name="MoveFile.RECENT_KEYS">
|
||||||
|
<recent name="C:\Users\sergey\Documents\Projects\Python\PRIMOSTORYFINAL\BotCode\core" />
|
||||||
|
<recent name="C:\Users\sergey\Documents\Projects\Python\PRIMOSTORYFINAL" />
|
||||||
|
</key>
|
||||||
|
</component>
|
||||||
|
<component name="RunManager">
|
||||||
|
<configuration name="main" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
|
||||||
|
<module name="PRIMOSTORYFINAL" />
|
||||||
|
<option name="ENV_FILES" value="" />
|
||||||
|
<option name="INTERPRETER_OPTIONS" value="" />
|
||||||
|
<option name="PARENT_ENVS" value="true" />
|
||||||
|
<envs>
|
||||||
|
<env name="PYTHONUNBUFFERED" value="1" />
|
||||||
|
</envs>
|
||||||
|
<option name="SDK_HOME" value="" />
|
||||||
|
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
|
||||||
|
<option name="IS_MODULE_SDK" value="true" />
|
||||||
|
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||||
|
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||||
|
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
||||||
|
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/main.py" />
|
||||||
|
<option name="PARAMETERS" value="" />
|
||||||
|
<option name="SHOW_COMMAND_LINE" value="false" />
|
||||||
|
<option name="EMULATE_TERMINAL" value="false" />
|
||||||
|
<option name="MODULE_MODE" value="false" />
|
||||||
|
<option name="REDIRECT_INPUT" value="false" />
|
||||||
|
<option name="INPUT_FILE" value="" />
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
|
<recent_temporary>
|
||||||
|
<list>
|
||||||
|
<item itemvalue="Python.main" />
|
||||||
|
</list>
|
||||||
|
</recent_temporary>
|
||||||
|
</component>
|
||||||
|
<component name="SharedIndexes">
|
||||||
|
<attachedChunks>
|
||||||
|
<set>
|
||||||
|
<option value="bundled-js-predefined-d6986cc7102b-6a121458b545-JavaScript-PY-251.25410.122" />
|
||||||
|
<option value="bundled-python-sdk-880ecab49056-36ea0e71a18c-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-251.25410.122" />
|
||||||
|
</set>
|
||||||
|
</attachedChunks>
|
||||||
|
</component>
|
||||||
|
<component name="TaskManager">
|
||||||
|
<task active="true" id="Default" summary="Default task">
|
||||||
|
<changelist id="976a6336-6952-45ae-989f-7b10c5e394d4" name="Changes" comment="" />
|
||||||
|
<created>1747702717100</created>
|
||||||
|
<option name="number" value="Default" />
|
||||||
|
<option name="presentableId" value="Default" />
|
||||||
|
<updated>1747702717100</updated>
|
||||||
|
<workItem from="1747702718164" duration="4191000" />
|
||||||
|
</task>
|
||||||
|
<servers />
|
||||||
|
</component>
|
||||||
|
<component name="TypeScriptGeneratedFilesManager">
|
||||||
|
<option name="version" value="3" />
|
||||||
|
</component>
|
||||||
|
<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$" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
4
BotCode/__init__.py
Normal file
4
BotCode/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from .config import *
|
||||||
|
from .handlers import *
|
||||||
|
from .utils import *
|
||||||
|
from .config import *
|
||||||
19
BotCode/config.py
Normal file
19
BotCode/config.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# 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")
|
||||||
0
BotCode/core/__init__.py
Normal file
0
BotCode/core/__init__.py
Normal file
234
BotCode/core/storage.py
Normal file
234
BotCode/core/storage.py
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
import json
|
||||||
|
from os import path, makedirs, listdir
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
from BotCode.config import POSTS_DIR
|
||||||
|
from BotCode.loggers import logs
|
||||||
|
|
||||||
|
|
||||||
|
class PostStorage:
|
||||||
|
"""Класс для управления хранением постов и связанных уведомлений."""
|
||||||
|
|
||||||
|
def __init__(self, posts_dir: str = POSTS_DIR):
|
||||||
|
self.posts_dir = posts_dir
|
||||||
|
self.global_posts: Dict[str, Dict[str, Any]] = {}
|
||||||
|
self.notifications: Dict[str, Dict[str, Any]] = {}
|
||||||
|
self.alert_texts: Dict[str, Dict[str, Any]] = {}
|
||||||
|
|
||||||
|
self._ensure_posts_dir()
|
||||||
|
self.load_all_posts()
|
||||||
|
|
||||||
|
def _ensure_posts_dir(self, directory: Optional[str] = None) -> None:
|
||||||
|
"""Создаёт директорию для хранения постов, если она не существует."""
|
||||||
|
dir_path = directory or self.posts_dir
|
||||||
|
if not path.isdir(dir_path):
|
||||||
|
makedirs(dir_path, exist_ok=True)
|
||||||
|
logs.info(
|
||||||
|
f"Created posts directory: {dir_path}",
|
||||||
|
log_type="STORAGE",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_user_posts_file(self, user_id: int) -> str:
|
||||||
|
"""Возвращает путь к файлу с постами пользователя."""
|
||||||
|
return path.join(self.posts_dir, f"posts_{user_id}.json")
|
||||||
|
|
||||||
|
def _update_button_notifications(self, callback_data: str, notification_data: Dict[str, Any]) -> None:
|
||||||
|
"""Регистрирует данные уведомления кнопки во внутренних хранилищах."""
|
||||||
|
if not callback_data:
|
||||||
|
return
|
||||||
|
self.alert_texts[callback_data] = notification_data
|
||||||
|
self.notifications[callback_data] = notification_data
|
||||||
|
|
||||||
|
def _process_buttons(self, post_id: str, buttons: List[Any]) -> None:
|
||||||
|
"""
|
||||||
|
Обрабатывает кнопки поста, нормализует callback_data и регистрирует уведомления.
|
||||||
|
Поддерживает различные типы кнопок: callback, url, copy, inline.
|
||||||
|
"""
|
||||||
|
if not buttons:
|
||||||
|
return
|
||||||
|
|
||||||
|
for row_idx, row in enumerate(buttons):
|
||||||
|
btns = row if isinstance(row, list) else [row]
|
||||||
|
for col_idx, button in enumerate(btns):
|
||||||
|
if not isinstance(button, dict):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if 'callback_data' in button:
|
||||||
|
cb_data = button['callback_data']
|
||||||
|
if not cb_data or not (cb_data.startswith('bt_') or cb_data.startswith('show_alert_')):
|
||||||
|
prefix = 'show_alert_' if button.get('show_alert') else 'bt_'
|
||||||
|
button['callback_data'] = f"{prefix}{post_id}_{row_idx}_{col_idx}"
|
||||||
|
cb_data = button['callback_data']
|
||||||
|
|
||||||
|
if 'notification' in button:
|
||||||
|
notification = {
|
||||||
|
'text': button['notification'],
|
||||||
|
'show_alert': button.get('show_alert', False),
|
||||||
|
'allowed_ids': button.get('allowed_ids'),
|
||||||
|
'unauthorized_message': button.get('unauthorized_message')
|
||||||
|
}
|
||||||
|
self._update_button_notifications(cb_data, notification)
|
||||||
|
logs.debug(
|
||||||
|
f"Registered notification for {cb_data}",
|
||||||
|
log_type="STORAGE",
|
||||||
|
)
|
||||||
|
|
||||||
|
def load_user_posts(self, user_id: int) -> Dict[str, Any]:
|
||||||
|
"""Загружает посты пользователя из файла."""
|
||||||
|
file_path = self._get_user_posts_file(user_id)
|
||||||
|
try:
|
||||||
|
if path.isfile(file_path):
|
||||||
|
with open(file_path, 'r', encoding='utf-8') as f:
|
||||||
|
posts = json.load(f)
|
||||||
|
if isinstance(posts, dict):
|
||||||
|
return posts
|
||||||
|
logs.warning(
|
||||||
|
f"Invalid posts format in {file_path}",
|
||||||
|
log_type="STORAGE",
|
||||||
|
)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
logs.error(
|
||||||
|
f"JSON decode error in {file_path}: {str(e)}",
|
||||||
|
log_type="STORAGE",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logs.error(
|
||||||
|
f"Error loading posts from {file_path}: {str(e)}",
|
||||||
|
log_type="STORAGE",
|
||||||
|
)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def save_user_posts(self, user_id: int, posts: Dict[str, Any]) -> None:
|
||||||
|
"""
|
||||||
|
Сохраняет посты пользователя в файл и обновляет внутренние хранилища.
|
||||||
|
Обрабатывает кнопки и уведомления перед сохранением.
|
||||||
|
"""
|
||||||
|
if not isinstance(posts, dict):
|
||||||
|
logs.error(
|
||||||
|
"Invalid posts format, expected dict",
|
||||||
|
log_type="STORAGE",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
for post_id, post in posts.items():
|
||||||
|
if isinstance(post, dict) and 'buttons' in post:
|
||||||
|
self._process_buttons(post_id, post['buttons'])
|
||||||
|
|
||||||
|
file_path = self._get_user_posts_file(user_id)
|
||||||
|
try:
|
||||||
|
with open(file_path, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(posts, f, ensure_ascii=False, indent=4)
|
||||||
|
logs.info(
|
||||||
|
f"Saved posts for user {user_id}",
|
||||||
|
log_type="STORAGE",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logs.error(
|
||||||
|
f"Error saving posts to {file_path}: {str(e)}",
|
||||||
|
log_type="STORAGE",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Обновление кэша: перезагружаем записи этого пользователя
|
||||||
|
# Удаляем старые записи
|
||||||
|
for pid in list(self.global_posts):
|
||||||
|
if pid in posts:
|
||||||
|
self.global_posts.pop(pid, None)
|
||||||
|
# Загружаем свежие
|
||||||
|
fresh = self.load_user_posts(user_id)
|
||||||
|
for pid, post in fresh.items():
|
||||||
|
if isinstance(post, dict) and 'buttons' in post:
|
||||||
|
self._process_buttons(pid, post['buttons'])
|
||||||
|
self.global_posts[pid] = post
|
||||||
|
|
||||||
|
def delete_user_post(self, user_id: int, post_id: str) -> bool:
|
||||||
|
"""Удаляет пост пользователя и связанные уведомления. Возвращает статус операции."""
|
||||||
|
user_posts = self.load_user_posts(user_id)
|
||||||
|
if post_id not in user_posts:
|
||||||
|
logs.warning(
|
||||||
|
f"Post {post_id} not found for user {user_id}",
|
||||||
|
log_type="STORAGE",
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
post = user_posts.pop(post_id)
|
||||||
|
notification_count = 0
|
||||||
|
if isinstance(post.get('buttons'), list):
|
||||||
|
for row in post['buttons']:
|
||||||
|
btns = row if isinstance(row, list) else [row]
|
||||||
|
for button in btns:
|
||||||
|
if isinstance(button, dict):
|
||||||
|
cb = button.get('callback_data')
|
||||||
|
if cb and cb in self.alert_texts:
|
||||||
|
self.alert_texts.pop(cb)
|
||||||
|
self.notifications.pop(cb, None)
|
||||||
|
notification_count += 1
|
||||||
|
logs.debug(
|
||||||
|
f"Removed {notification_count} notifications for post {post_id}",
|
||||||
|
log_type="STORAGE",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Сохраняем и обновляем кэш
|
||||||
|
self.save_user_posts(user_id, user_posts)
|
||||||
|
self.global_posts.pop(post_id, None)
|
||||||
|
logs.info(
|
||||||
|
f"Deleted post {post_id} for user {user_id}",
|
||||||
|
log_type="STORAGE",
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def is_post_available(self, post_id: str) -> bool:
|
||||||
|
"""Проверяет доступность идентификатора поста."""
|
||||||
|
return post_id not in self.global_posts
|
||||||
|
|
||||||
|
def load_all_posts(self) -> None:
|
||||||
|
"""Загружает все посты из файлов в рабочей директории."""
|
||||||
|
self.global_posts.clear()
|
||||||
|
self.alert_texts.clear()
|
||||||
|
self.notifications.clear()
|
||||||
|
|
||||||
|
self._ensure_posts_dir()
|
||||||
|
loaded_files = 0
|
||||||
|
loaded_posts = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
for filename in listdir(self.posts_dir):
|
||||||
|
if filename.endswith('.json'):
|
||||||
|
user_id_str = filename[len('posts_'):-len('.json')]
|
||||||
|
try:
|
||||||
|
user_id = int(user_id_str)
|
||||||
|
except ValueError:
|
||||||
|
logs.warning(
|
||||||
|
f"Invalid filename format: {filename}",
|
||||||
|
log_type="STORAGE",
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
posts = self.load_user_posts(user_id)
|
||||||
|
for pid, post in posts.items():
|
||||||
|
if isinstance(post, dict) and 'buttons' in post:
|
||||||
|
self._process_buttons(pid, post['buttons'])
|
||||||
|
self.global_posts[pid] = post
|
||||||
|
loaded_posts += 1
|
||||||
|
loaded_files += 1
|
||||||
|
except Exception as e:
|
||||||
|
logs.error(
|
||||||
|
f"Error loading all posts: {str(e)}",
|
||||||
|
log_type="STORAGE",
|
||||||
|
)
|
||||||
|
|
||||||
|
logs.info(
|
||||||
|
f"Loaded {loaded_posts} posts from {loaded_files} files",
|
||||||
|
log_type="STORAGE",
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_post(self, post_id: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Возвращает пост по идентификатору или None если не найден."""
|
||||||
|
return self.global_posts.get(post_id)
|
||||||
|
|
||||||
|
def get_notification(self, callback_data: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Возвращает данные уведомления для указанного callback."""
|
||||||
|
return self.notifications.get(callback_data)
|
||||||
|
|
||||||
|
|
||||||
|
# Инициализация хранилища при импорте модуля
|
||||||
|
storage = PostStorage()
|
||||||
16
BotCode/handlers/__init__.py
Normal file
16
BotCode/handlers/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
from aiogram import Router
|
||||||
|
from .post import router as post_routers
|
||||||
|
from .commands import router as cmd_routers
|
||||||
|
|
||||||
|
from .callback import router as callback_router
|
||||||
|
from .inline import router as inline_router
|
||||||
|
|
||||||
|
router = Router(name=__name__)
|
||||||
|
|
||||||
|
# Include routers with different priorities
|
||||||
|
router.include_routers(
|
||||||
|
cmd_routers,
|
||||||
|
callback_router,
|
||||||
|
post_routers,
|
||||||
|
inline_router
|
||||||
|
)
|
||||||
48
BotCode/handlers/callback.py
Normal file
48
BotCode/handlers/callback.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# BotCode/handlers/callback.py
|
||||||
|
from aiogram import Router, F
|
||||||
|
from aiogram.types import CallbackQuery
|
||||||
|
from BotCode.core.storage import storage
|
||||||
|
|
||||||
|
router = Router(name="callback_router")
|
||||||
|
|
||||||
|
@router.callback_query(F.data.startswith("bt_"))
|
||||||
|
@router.callback_query(F.data.startswith("show_alert_"))
|
||||||
|
async def handle_button_alert(callback_query: CallbackQuery) -> None:
|
||||||
|
key = callback_query.data
|
||||||
|
user_id = callback_query.from_user.id
|
||||||
|
|
||||||
|
# Получаем уведомление через хранилище
|
||||||
|
notif = storage.get_notification(key)
|
||||||
|
if not notif:
|
||||||
|
await callback_query.answer()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Проверяем права доступа
|
||||||
|
allowed = notif.get("allowed_ids")
|
||||||
|
if allowed and user_id not in allowed:
|
||||||
|
msg = notif.get("unauthorized_message", "У вас нет доступа к этому уведомлению.")
|
||||||
|
await callback_query.answer(text=msg, show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
text = notif.get("text", "")
|
||||||
|
show_alert = notif.get("show_alert", False)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await callback_query.answer(text=text, show_alert=show_alert)
|
||||||
|
except Exception as e:
|
||||||
|
try:
|
||||||
|
await callback_query.answer(text="Произошла ошибка при отображении уведомления.", show_alert=True)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(F.data == "void")
|
||||||
|
async def handle_void_callback(callback_query: CallbackQuery) -> None:
|
||||||
|
"""
|
||||||
|
Обработка пустых callback-запросов (void).
|
||||||
|
Просто отвечает на callback без уведомления.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
await callback_query.answer()
|
||||||
|
except Exception as e:
|
||||||
|
return
|
||||||
7
BotCode/handlers/commands/__init__.py
Normal file
7
BotCode/handlers/commands/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
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,)
|
||||||
36
BotCode/handlers/commands/start_cmd.py
Normal file
36
BotCode/handlers/commands/start_cmd.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# 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!"))
|
||||||
179
BotCode/handlers/inline.py
Normal file
179
BotCode/handlers/inline.py
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
# BotCode/handlers/inline.py
|
||||||
|
from aiogram import Router
|
||||||
|
from aiogram.types import (
|
||||||
|
InlineKeyboardButton,
|
||||||
|
InlineKeyboardMarkup,
|
||||||
|
InlineQuery,
|
||||||
|
InputTextMessageContent,
|
||||||
|
InlineQueryResultArticle,
|
||||||
|
SwitchInlineQueryChosenChat,
|
||||||
|
CopyTextButton,
|
||||||
|
)
|
||||||
|
from aiogram.utils.markdown import hide_link
|
||||||
|
|
||||||
|
from BotCode.core.storage import storage
|
||||||
|
from BotCode.utils import textmd2
|
||||||
|
from BotCode.config import PARSE_MODE
|
||||||
|
from BotCode.loggers import logs
|
||||||
|
|
||||||
|
router = Router(name="inline_send")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def build_markup(buttons_def: list[list[dict]]) -> InlineKeyboardMarkup | None:
|
||||||
|
"""
|
||||||
|
Создаёт InlineKeyboardMarkup из списка описаний кнопок.
|
||||||
|
Поддерживает URL, callback, inline-моды.
|
||||||
|
Обрабатывает "void"-кнопки как callback_data="void".
|
||||||
|
Для switch_inline_query_chosen_chat устанавливает хотя бы один allow_* True.
|
||||||
|
"""
|
||||||
|
if not buttons_def:
|
||||||
|
return None
|
||||||
|
|
||||||
|
rows: list[list[InlineKeyboardButton]] = []
|
||||||
|
for row_idx, row in enumerate(buttons_def):
|
||||||
|
if not isinstance(row, list):
|
||||||
|
logs.warning(f"Некорректный формат ряда кнопок: {row}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
kb_row: list[InlineKeyboardButton] = []
|
||||||
|
for col_idx, b in enumerate(row):
|
||||||
|
if not isinstance(b, dict):
|
||||||
|
logs.warning(f"Некорректный формат кнопки в ряду {row_idx}: {b}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
text = b.get("text", "")
|
||||||
|
if not text:
|
||||||
|
logs.warning(f"Пустой текст кнопки в ряду {row_idx}, колонке {col_idx}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
btn = None
|
||||||
|
try:
|
||||||
|
if "url" in b:
|
||||||
|
url = b["url"]
|
||||||
|
if url.lower().endswith("void"):
|
||||||
|
btn = InlineKeyboardButton(text=text, callback_data="void")
|
||||||
|
else:
|
||||||
|
btn = InlineKeyboardButton(text=text, url=url)
|
||||||
|
elif "switch_inline_query" in b:
|
||||||
|
btn = InlineKeyboardButton(
|
||||||
|
text=text,
|
||||||
|
switch_inline_query=b["switch_inline_query"]
|
||||||
|
)
|
||||||
|
elif "switch_inline_query_current_chat" in b:
|
||||||
|
btn = InlineKeyboardButton(
|
||||||
|
text=text,
|
||||||
|
switch_inline_query_current_chat=b["switch_inline_query_current_chat"]
|
||||||
|
)
|
||||||
|
elif "switch_inline_query_chosen_chat" in b:
|
||||||
|
query = b["switch_inline_query_chosen_chat"]
|
||||||
|
if isinstance(query, dict):
|
||||||
|
siqcc = SwitchInlineQueryChosenChat(
|
||||||
|
query=query.get("query", ""),
|
||||||
|
allow_user_chats=query.get("allow_user_chats", True),
|
||||||
|
allow_group_chats=query.get("allow_group_chats", True),
|
||||||
|
allow_channel_chats=query.get("allow_channel_chats", True),
|
||||||
|
allow_bot_chats=query.get("allow_bot_chats", False),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
siqcc = SwitchInlineQueryChosenChat(
|
||||||
|
query=query,
|
||||||
|
allow_user_chats=True,
|
||||||
|
allow_group_chats=True,
|
||||||
|
allow_channel_chats=True,
|
||||||
|
allow_bot_chats=False,
|
||||||
|
)
|
||||||
|
btn = InlineKeyboardButton(
|
||||||
|
text=text,
|
||||||
|
switch_inline_query_chosen_chat=siqcc
|
||||||
|
)
|
||||||
|
elif "copy_text" in b:
|
||||||
|
btn = InlineKeyboardButton(
|
||||||
|
text=text,
|
||||||
|
copy_text=CopyTextButton(text=b["copy_text"])
|
||||||
|
)
|
||||||
|
elif "callback_data" in b:
|
||||||
|
btn = InlineKeyboardButton(
|
||||||
|
text=text,
|
||||||
|
callback_data=b["callback_data"]
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logs.error(f"Ошибка при создании кнопки в ряду {row_idx}, колонке {col_idx}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if btn:
|
||||||
|
kb_row.append(btn)
|
||||||
|
|
||||||
|
if kb_row:
|
||||||
|
rows.append(kb_row)
|
||||||
|
|
||||||
|
if not rows:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return InlineKeyboardMarkup(inline_keyboard=rows)
|
||||||
|
|
||||||
|
|
||||||
|
@router.inline_query()
|
||||||
|
async def inline_query_handler(inline_query: InlineQuery):
|
||||||
|
"""
|
||||||
|
Обрабатывает инлайн-запросы для поиска и отправки постов.
|
||||||
|
Фильтрует посты по приватности и поисковому запросу.
|
||||||
|
"""
|
||||||
|
# Перезагружаем все посты из файлов на случай изменений
|
||||||
|
storage.load_all_posts()
|
||||||
|
|
||||||
|
query = inline_query.query or ""
|
||||||
|
user_id = inline_query.from_user.id
|
||||||
|
username = inline_query.from_user.username or f"user_{user_id}"
|
||||||
|
|
||||||
|
logs.debug(f"Получен инлайн-запрос от {username} (ID: {user_id}): {query}")
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for post_id, post in storage.global_posts.items():
|
||||||
|
try:
|
||||||
|
# Проверка приватности
|
||||||
|
if post.get("private") and post.get("user_id") != user_id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Проверка поискового запроса
|
||||||
|
if query and query.lower() not in post_id.lower():
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Тело сообщения
|
||||||
|
text = textmd2(post.get("text", ""))
|
||||||
|
image = post.get("image", "")
|
||||||
|
if image and image.startswith("http"):
|
||||||
|
text = f"{hide_link(image)}{text}"
|
||||||
|
|
||||||
|
# Клавиатура
|
||||||
|
markup = build_markup(post.get("buttons", []))
|
||||||
|
|
||||||
|
results.append(
|
||||||
|
InlineQueryResultArticle(
|
||||||
|
id=post_id,
|
||||||
|
title=f"Пост {post_id}",
|
||||||
|
description=(post.get("text", "")[:100] + "...") if len(post.get("text", "")) > 100 else post.get(
|
||||||
|
"text", ""),
|
||||||
|
input_message_content=InputTextMessageContent(
|
||||||
|
message_text=text,
|
||||||
|
parse_mode=PARSE_MODE
|
||||||
|
),
|
||||||
|
reply_markup=markup
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logs.error(f"Ошибка при обработке поста {post_id}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
logs.info(f"Отправлено {len(results)} результатов для запроса '{query}' от {username} (ID: {user_id})")
|
||||||
|
|
||||||
|
try:
|
||||||
|
await inline_query.answer(results, cache_time=0, is_personal=True)
|
||||||
|
except Exception as e:
|
||||||
|
logs.error(f"Ошибка при отправке результатов инлайн-запроса: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'router',
|
||||||
|
'inline_query_handler'
|
||||||
|
]
|
||||||
10
BotCode/handlers/post/__init__.py
Normal file
10
BotCode/handlers/post/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
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,
|
||||||
|
)
|
||||||
210
BotCode/handlers/post/create_posts.py
Normal file
210
BotCode/handlers/post/create_posts.py
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
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()
|
||||||
200
BotCode/handlers/post/post_list.py
Normal file
200
BotCode/handlers/post/post_list.py
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
from math import ceil
|
||||||
|
from aiogram import Router, F
|
||||||
|
from aiogram.types import (
|
||||||
|
Message, CallbackQuery,
|
||||||
|
InlineKeyboardButton, InlineKeyboardMarkup,
|
||||||
|
SwitchInlineQueryChosenChat, CopyTextButton
|
||||||
|
)
|
||||||
|
from aiogram.exceptions import TelegramBadRequest
|
||||||
|
from aiogram.utils.markdown import hide_link
|
||||||
|
|
||||||
|
from BotCode.core.storage import storage
|
||||||
|
from BotCode.utils.pagination import create_pagination_buttons
|
||||||
|
from BotCode.utils import textmd2
|
||||||
|
from BotCode.config import PARSE_MODE
|
||||||
|
|
||||||
|
router = Router(name="posts_manager")
|
||||||
|
|
||||||
|
PAGE_SIZE = 5
|
||||||
|
|
||||||
|
async def send_posts_list(
|
||||||
|
message: Message = None,
|
||||||
|
callback_query: CallbackQuery = None,
|
||||||
|
page: int = 0
|
||||||
|
) -> None:
|
||||||
|
"""Отправляет список постов пользователя с пагинацией."""
|
||||||
|
user_id = message.from_user.id if message else callback_query.from_user.id
|
||||||
|
posts = storage.load_user_posts(user_id)
|
||||||
|
|
||||||
|
if not posts:
|
||||||
|
msg = "Нет сохранённых постов."
|
||||||
|
if message:
|
||||||
|
await message.answer(msg)
|
||||||
|
else:
|
||||||
|
await callback_query.answer(msg, show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
post_ids = list(posts.keys())
|
||||||
|
total = len(post_ids)
|
||||||
|
pages = ceil(total / PAGE_SIZE)
|
||||||
|
page = max(0, min(page, pages - 1))
|
||||||
|
|
||||||
|
start = page * PAGE_SIZE
|
||||||
|
end = start + PAGE_SIZE
|
||||||
|
current_ids = post_ids[start:end]
|
||||||
|
|
||||||
|
rows: list[list[InlineKeyboardButton]] = []
|
||||||
|
for pid in current_ids:
|
||||||
|
post = posts[pid]
|
||||||
|
priv = "🔒" if post.get("private") else "🔓"
|
||||||
|
btn = InlineKeyboardButton(
|
||||||
|
text=f"{priv} Пост {pid}",
|
||||||
|
callback_data=f"view_post_{pid}"
|
||||||
|
)
|
||||||
|
rows.append([btn])
|
||||||
|
|
||||||
|
# Пагинация
|
||||||
|
nav_buttons = create_pagination_buttons(
|
||||||
|
action="open_post_list",
|
||||||
|
page=page,
|
||||||
|
total_posts=total,
|
||||||
|
bt_page=PAGE_SIZE
|
||||||
|
)
|
||||||
|
if nav_buttons:
|
||||||
|
rows.append(nav_buttons)
|
||||||
|
|
||||||
|
# Кнопка закрытия
|
||||||
|
rows.append([InlineKeyboardButton(text="Закрыть❌", callback_data="cancel_list")])
|
||||||
|
|
||||||
|
keyboard = InlineKeyboardMarkup(inline_keyboard=rows)
|
||||||
|
header = "Список ваших постов:"
|
||||||
|
|
||||||
|
try:
|
||||||
|
if callback_query:
|
||||||
|
await callback_query.message.edit_text(header, reply_markup=keyboard)
|
||||||
|
else:
|
||||||
|
await message.answer(header, reply_markup=keyboard)
|
||||||
|
except TelegramBadRequest:
|
||||||
|
if callback_query:
|
||||||
|
await callback_query.message.delete()
|
||||||
|
await callback_query.message.answer(header, reply_markup=keyboard)
|
||||||
|
else:
|
||||||
|
await message.answer(header, reply_markup=keyboard)
|
||||||
|
|
||||||
|
# --- Хендлеры списка ---
|
||||||
|
@router.message(F.text.lower() == "посмотреть список📋")
|
||||||
|
async def cmd_list(message: Message):
|
||||||
|
await send_posts_list(message=message)
|
||||||
|
|
||||||
|
@router.callback_query(F.data == "open_post_list")
|
||||||
|
async def cb_open_list(cq: CallbackQuery):
|
||||||
|
await send_posts_list(callback_query=cq)
|
||||||
|
await cq.answer()
|
||||||
|
|
||||||
|
@router.callback_query(lambda c: c.data and c.data.startswith("open_post_list_page_"))
|
||||||
|
async def cb_paginate(cq: CallbackQuery):
|
||||||
|
try:
|
||||||
|
page = int(cq.data.rsplit("_", 1)[-1])
|
||||||
|
except ValueError:
|
||||||
|
await cq.answer("Некорректная страница", show_alert=True)
|
||||||
|
return
|
||||||
|
await send_posts_list(callback_query=cq, page=page)
|
||||||
|
await cq.answer()
|
||||||
|
|
||||||
|
@router.callback_query(F.data == "cancel_list")
|
||||||
|
async def cb_cancel(cq: CallbackQuery):
|
||||||
|
await cq.message.delete()
|
||||||
|
await cq.answer()
|
||||||
|
|
||||||
|
# --- Просмотр отдельного поста ---
|
||||||
|
@router.callback_query(lambda c: c.data and c.data.startswith("view_post_"))
|
||||||
|
async def view_post_callback(cq: CallbackQuery):
|
||||||
|
pid = cq.data.replace("view_post_", "")
|
||||||
|
uid = cq.from_user.id
|
||||||
|
posts = storage.load_user_posts(uid)
|
||||||
|
if pid not in posts:
|
||||||
|
await cq.answer("Пост не найден", show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
post = posts[pid]
|
||||||
|
text = textmd2(post.get("text", ""))
|
||||||
|
img = post.get("image", "")
|
||||||
|
if img.startswith("http"):
|
||||||
|
text = f"{hide_link(img)}{text}"
|
||||||
|
|
||||||
|
rows: list[list[InlineKeyboardButton]] = []
|
||||||
|
for row in post.get("buttons", []):
|
||||||
|
btns: list[InlineKeyboardButton] = []
|
||||||
|
for b in row:
|
||||||
|
if "copy_text" in b:
|
||||||
|
btns.append(
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=b["text"],
|
||||||
|
copy_text=CopyTextButton(text=b["copy_text"])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif "switch_inline_query" in b:
|
||||||
|
btns.append(
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=b["text"],
|
||||||
|
switch_inline_query=b["switch_inline_query"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif "switch_inline_query_current_chat" in b:
|
||||||
|
btns.append(
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=b["text"],
|
||||||
|
switch_inline_query_current_chat=b["switch_inline_query_current_chat"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif "switch_inline_query_chosen_chat" in b:
|
||||||
|
raw = b["switch_inline_query_chosen_chat"]
|
||||||
|
cfg = raw if isinstance(raw, dict) else {
|
||||||
|
"query": raw,
|
||||||
|
"allow_user_chats": True
|
||||||
|
}
|
||||||
|
btns.append(
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=b["text"],
|
||||||
|
switch_inline_query_chosen_chat=SwitchInlineQueryChosenChat(**cfg)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif "url" in b:
|
||||||
|
url = b["url"]
|
||||||
|
if url.lower().endswith("void"):
|
||||||
|
btns.append(
|
||||||
|
InlineKeyboardButton(text=b["text"], callback_data="void")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
btns.append(
|
||||||
|
InlineKeyboardButton(text=b["text"], url=url)
|
||||||
|
)
|
||||||
|
elif "callback_data" in b:
|
||||||
|
btns.append(
|
||||||
|
InlineKeyboardButton(text=b["text"], callback_data=b["callback_data"])
|
||||||
|
)
|
||||||
|
if btns:
|
||||||
|
rows.append(btns)
|
||||||
|
|
||||||
|
# Удалить / назад
|
||||||
|
rows.append([
|
||||||
|
InlineKeyboardButton(text="Удалить❌", callback_data=f"delete_post_{pid}"),
|
||||||
|
InlineKeyboardButton(text="Назад◀️", callback_data="open_post_list")
|
||||||
|
])
|
||||||
|
|
||||||
|
keyboard = InlineKeyboardMarkup(inline_keyboard=rows)
|
||||||
|
|
||||||
|
await cq.message.answer(text=text, reply_markup=keyboard, parse_mode=PARSE_MODE)
|
||||||
|
await cq.message.delete()
|
||||||
|
await cq.answer()
|
||||||
|
|
||||||
|
# --- Удаление поста ---
|
||||||
|
@router.callback_query(lambda c: c.data and c.data.startswith("delete_post_"))
|
||||||
|
async def delete_post_callback(cq: CallbackQuery):
|
||||||
|
pid = cq.data.replace("delete_post_", "")
|
||||||
|
uid = cq.from_user.id
|
||||||
|
if storage.delete_user_post(uid, pid):
|
||||||
|
await cq.answer(f"Пост {pid} удалён")
|
||||||
|
await send_posts_list(callback_query=cq)
|
||||||
|
else:
|
||||||
|
await cq.answer("Не удалось удалить пост", show_alert=True)
|
||||||
5
BotCode/loggers/__init__.py
Normal file
5
BotCode/loggers/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# BotLibrary/loggers/__init__.py
|
||||||
|
# Инициализация модуля loggers, для настройки логеров
|
||||||
|
|
||||||
|
# Экспортирование модулей во внешние слои проекта
|
||||||
|
from .logs import *
|
||||||
147
BotCode/loggers/logs.py
Normal file
147
BotCode/loggers/logs.py
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
"""
|
||||||
|
Модуль логирования для Telegram-бота.
|
||||||
|
|
||||||
|
Особенности:
|
||||||
|
* Вывод логов в консоль и/или файл
|
||||||
|
* Автоматическая ротация и удержание
|
||||||
|
* Форматирование с информацией о системе, типе события и пользователе
|
||||||
|
* Удобные методы для разных уровней логирования
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sys import stderr
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, Final, Union
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
from aiogram.types import Message, User
|
||||||
|
|
||||||
|
try:
|
||||||
|
from config import LogConfig
|
||||||
|
except ImportError:
|
||||||
|
class LogConfig:
|
||||||
|
"""Запасные настройки логирования, если config недоступен."""
|
||||||
|
CONSOLE: Final[bool] = True
|
||||||
|
FILE: Final[bool] = True
|
||||||
|
DIR: Final[Path] = Path('Logs')
|
||||||
|
ROTATION: Final[str] = '100 MB'
|
||||||
|
RETENTION: Final[str] = '7 days'
|
||||||
|
|
||||||
|
# Настройка экспорта в модули
|
||||||
|
__all__ = ['Logs', 'logs']
|
||||||
|
|
||||||
|
|
||||||
|
class Logs:
|
||||||
|
"""
|
||||||
|
Класс для работы с логированием через loguru.
|
||||||
|
"""
|
||||||
|
_SYSTEM_NAME: Final[str] = 'PRIMO' # Исправлено: убран обратный слэш
|
||||||
|
_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>'
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _format_user(message: Optional[Message]) -> str:
|
||||||
|
"""
|
||||||
|
Форматирует информацию о пользователе из сообщения.
|
||||||
|
"""
|
||||||
|
if not message or not message.from_user:
|
||||||
|
return '@System'
|
||||||
|
user: User = message.from_user
|
||||||
|
return f"@{user.username}" if user.username else f"id{user.id}"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _log(cls,
|
||||||
|
level: Union[str, int],
|
||||||
|
text: str,
|
||||||
|
log_type: str,
|
||||||
|
message: Optional[Message] = None) -> None:
|
||||||
|
"""Внутренний метод логирования."""
|
||||||
|
user_ctx = cls._format_user(message)
|
||||||
|
logger.bind(
|
||||||
|
system=cls._SYSTEM_NAME,
|
||||||
|
user=user_ctx,
|
||||||
|
log_type=log_type,
|
||||||
|
).log(level, text)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setup(cls, start: bool = True) -> None:
|
||||||
|
"""Инициализация логирования: консоль и/или файл."""
|
||||||
|
logger.remove()
|
||||||
|
|
||||||
|
# Консольный вывод
|
||||||
|
if getattr(LogConfig, 'CONSOLE', False):
|
||||||
|
logger.add(
|
||||||
|
stderr,
|
||||||
|
format=cls._LOG_FORMAT,
|
||||||
|
colorize=True,
|
||||||
|
level='DEBUG',
|
||||||
|
filter=lambda rec: rec['extra'].get('log_type') != 'DEBUG'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Файловый вывод с ротацией
|
||||||
|
if getattr(LogConfig, 'FILE', False):
|
||||||
|
log_dir = getattr(LogConfig, 'DIR', Path('logs'))
|
||||||
|
log_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
logger.add(
|
||||||
|
log_dir / 'bot.log',
|
||||||
|
rotation=getattr(LogConfig, 'ROTATION', '100 MB'),
|
||||||
|
retention=getattr(LogConfig, 'RETENTION', '7 days'),
|
||||||
|
format=cls._LOG_FORMAT,
|
||||||
|
level='DEBUG',
|
||||||
|
enqueue=True,
|
||||||
|
backtrace=True,
|
||||||
|
diagnose=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Добавляем вызов start() если нужно
|
||||||
|
if start:
|
||||||
|
cls.start()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def start(cls, text: str = 'Запуск бота...', log_type: str = 'START') -> None:
|
||||||
|
"""Логирование старта приложения."""
|
||||||
|
cls._log(level='INFO', text=text, log_type=log_type)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def debug(cls,
|
||||||
|
text: str,
|
||||||
|
log_type: str = 'DEBUG',
|
||||||
|
message: Optional[Message] = None) -> None:
|
||||||
|
cls._log(level='DEBUG', text=text, log_type=log_type, message=message)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def info(cls,
|
||||||
|
text: str,
|
||||||
|
log_type: str = 'INFO',
|
||||||
|
message: Optional[Message] = None) -> None:
|
||||||
|
cls._log(level='INFO', text=text, log_type=log_type, message=message)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def warning(cls,
|
||||||
|
text: str,
|
||||||
|
log_type: str = 'WARNING',
|
||||||
|
message: Optional[Message] = None) -> None:
|
||||||
|
cls._log(level='WARNING', text=text, log_type=log_type, message=message)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def error(cls,
|
||||||
|
text: str,
|
||||||
|
log_type: str = 'ERROR',
|
||||||
|
message: Optional[Message] = None) -> None:
|
||||||
|
cls._log(level='ERROR', text=text, log_type=log_type, message=message)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def exception(cls,
|
||||||
|
text: str,
|
||||||
|
exception: Exception,
|
||||||
|
log_type: str = 'EXCEPTION',
|
||||||
|
message: Optional[Message] = None) -> None:
|
||||||
|
full_text = f"{text}\nException: {exception!r}"
|
||||||
|
cls._log(level='ERROR', text=full_text, log_type=log_type, message=message)
|
||||||
|
|
||||||
|
|
||||||
|
# Инициализация экземпляра логгера
|
||||||
|
logs = Logs()
|
||||||
|
logs.setup()
|
||||||
3
BotCode/utils/__init__.py
Normal file
3
BotCode/utils/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from .md2_escape import *
|
||||||
|
from .usernames import *
|
||||||
|
from .pagination import *
|
||||||
36
BotCode/utils/md2_escape.py
Normal file
36
BotCode/utils/md2_escape.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# BotCode/utils/md2_escape.py
|
||||||
|
from BotCode.config import PARSE_MODE
|
||||||
|
|
||||||
|
# Настройка экспорта в модули
|
||||||
|
__all__ = ("textmd2",)
|
||||||
|
|
||||||
|
def textmd2(msg: str,
|
||||||
|
parse_mode: str = PARSE_MODE,
|
||||||
|
special_chars: str = r"_*[]()~`>#+-=|{}.!") -> str:
|
||||||
|
"""
|
||||||
|
Экранирует специальные символы MarkdownV2 в переданном тексте.
|
||||||
|
|
||||||
|
:param msg: Входной текст в виде строки.
|
||||||
|
:param parse_mode: Формат форматирования ('MarkdownV2' или 'HTML').
|
||||||
|
:param special_chars: Символы, которые необходимо экранировать.
|
||||||
|
|
||||||
|
:return: Экранированный текст или исходный текст, если формат HTML.
|
||||||
|
:raises TypeError: Если передан не строковый тип данных.
|
||||||
|
:raises ValueError: Если parse_mode задан некорректно.
|
||||||
|
"""
|
||||||
|
from re import sub, escape
|
||||||
|
|
||||||
|
if not isinstance(msg, str):
|
||||||
|
raise TypeError(f"Ожидается строка, но получено {type(msg).__name__}")
|
||||||
|
|
||||||
|
if not isinstance(parse_mode, str):
|
||||||
|
raise TypeError(f"parse_mode должен быть строкой, но получено {type(parse_mode).__name__}")
|
||||||
|
|
||||||
|
if parse_mode.strip().lower() == "html":
|
||||||
|
return msg
|
||||||
|
|
||||||
|
elif parse_mode in {"markdownv2", "markdown"}:
|
||||||
|
return sub(rf"([{escape(special_chars)}])", r"\\\1", msg)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Недопустимое значение parse_mode: '{parse_mode}'. Ожидалось 'HTML' или 'MarkdownV2'")
|
||||||
22
BotCode/utils/pagination.py
Normal file
22
BotCode/utils/pagination.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# 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
|
||||||
22
BotCode/utils/usernames.py
Normal file
22
BotCode/utils/usernames.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# BotCode/utils/username.py
|
||||||
|
from aiogram.types import Message
|
||||||
|
|
||||||
|
# Настройка экспорта в модули
|
||||||
|
__all__ = ('username', )
|
||||||
|
|
||||||
|
# Функция получения юзера или ID пользователя
|
||||||
|
def username(message: Message) -> str:
|
||||||
|
"""
|
||||||
|
Возвращает юзернейм пользователя из сообщения, или ID, если юзернейм не указан.
|
||||||
|
|
||||||
|
:param message: Объект сообщения из aiogram.
|
||||||
|
:return: Строка с юзернеймом пользователя или его ID.
|
||||||
|
:raises ValueError: Если в сообщении отсутствует информация о пользователе.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if message.from_user:
|
||||||
|
return f"@{message.from_user.username}" if message.from_user.username else f"@{message.from_user.id}"
|
||||||
|
raise ValueError("Информация о пользователе отсутствует в сообщении.")
|
||||||
|
|
||||||
|
except ValueError as e:
|
||||||
|
raise e # Перебрасываем ошибку выше для дальнейшей обработки
|
||||||
10
Dockerfile
Normal file
10
Dockerfile
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
FROM mwalbeck/python-poetry:2.1-3.11
|
||||||
|
|
||||||
|
WORKDIR /PostBot
|
||||||
|
|
||||||
|
COPY pyproject.toml poetry.lock ./
|
||||||
|
RUN poetry install --no-interaction --no-root --only main
|
||||||
|
|
||||||
|
COPY ../../../../Desktop/PostBot .
|
||||||
|
|
||||||
|
CMD ["poetry", "run", "python", "-m", "main"]
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) [2025] [Лейн]
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||||
|
of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included
|
||||||
|
in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||||
|
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||||
|
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||||
|
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
|
OTHER DEALINGS IN THE SOFTWARE.
|
||||||
31
README.md
Normal file
31
README.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# Создание поста
|
||||||
|
new_post = {
|
||||||
|
"id": "cat_post",
|
||||||
|
"author_id": 123,
|
||||||
|
"mod": "HTML",
|
||||||
|
"type": "photo",
|
||||||
|
"text": "Мой котик!",
|
||||||
|
"media": "cat.jpg",
|
||||||
|
"private": True,
|
||||||
|
"allowed_users": [456, 789],
|
||||||
|
"buttons": [[{
|
||||||
|
"type": "share",
|
||||||
|
"name": "Поделиться",
|
||||||
|
"params": {"message": "Посмотрите этого котика!"}
|
||||||
|
}]]
|
||||||
|
}
|
||||||
|
|
||||||
|
post_id = storage.create_post(new_post)
|
||||||
|
|
||||||
|
# Получение поста
|
||||||
|
post = storage.get_post(post_id, user_id=456) # Доступ разрешен
|
||||||
|
post = storage.get_post(post_id, user_id=000) # Доступ запрещен
|
||||||
|
|
||||||
|
# Поиск постов
|
||||||
|
results = storage.search_posts("котик", user_id=456)
|
||||||
|
|
||||||
|
# Обновление поста
|
||||||
|
storage.update_post(post_id, updater_id=123, updates={"text": "Новый текст"})
|
||||||
|
|
||||||
|
# Удаление поста
|
||||||
|
storage.delete_post(post_id, deleter_id=123)
|
||||||
BIN
assets/start.jpg
Normal file
BIN
assets/start.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
30
main.py
Normal file
30
main.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# 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()
|
||||||
|
TOKEN: str = BOT_DEBUG_TOKEN if DEBUG_MODE else BOT_TOKEN
|
||||||
|
bot: Bot = Bot(
|
||||||
|
token=TOKEN,
|
||||||
|
default=DefaultBotProperties(
|
||||||
|
parse_mode=PARSE_MODE,
|
||||||
|
link_preview_show_above_text=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def main() -> None:
|
||||||
|
from aiogram.types import User
|
||||||
|
from BotCode.loggers import logs
|
||||||
|
from BotCode.handlers import router as main_router
|
||||||
|
|
||||||
|
bot_info: User = await bot.get_me()
|
||||||
|
logs.start(text=f"Бот @{bot_info.username} запущен!")
|
||||||
|
|
||||||
|
dp.include_router(main_router)
|
||||||
|
|
||||||
|
await dp.start_polling(bot)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
from asyncio import run
|
||||||
|
run(main())
|
||||||
291
posts/posts_6751720805.json
Normal file
291
posts/posts_6751720805.json
Normal file
File diff suppressed because one or more lines are too long
19
pyproject.toml
Normal file
19
pyproject.toml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
[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"
|
||||||
Reference in New Issue
Block a user