v1.2.1
This commit is contained in:
@@ -1,36 +1,39 @@
|
||||
# .dockerignore: Исключения для Docker сборки
|
||||
# Игнорировать всё, кроме необходимого для production
|
||||
# Исключить скрытые системные каталоги, но не всё подряд
|
||||
.git/
|
||||
.gitattributes
|
||||
.gitignore
|
||||
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.dockerignore
|
||||
**/Dockerfile
|
||||
**/README.md
|
||||
# Виртуальные окружения и Python-кэш
|
||||
.venv/
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.pyo
|
||||
|
||||
# Директории
|
||||
**/__pycache__
|
||||
**/.mypy_cache
|
||||
**/.pytest_cache
|
||||
**/.idea
|
||||
**/.vscode
|
||||
**/test
|
||||
**/tests
|
||||
**/docs
|
||||
**/examples
|
||||
# IDE-файлы
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
# Файлы
|
||||
**/*.pyc
|
||||
**/*.pyo
|
||||
**/*.pyd
|
||||
**/*.egg-info
|
||||
**/*.log
|
||||
**/*.logs
|
||||
**/*.sqlite
|
||||
**/*.db
|
||||
config/.env
|
||||
**/docker-compose*
|
||||
# Тесты и документация
|
||||
tests/
|
||||
test/
|
||||
docs/
|
||||
examples/
|
||||
|
||||
# Артефакты сборки
|
||||
**/build
|
||||
**/dist
|
||||
**/node_modules
|
||||
# Логи и артефакты сборки
|
||||
*.log
|
||||
*.logs
|
||||
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 ###
|
||||
# Виртуальные окружения и настройки
|
||||
config/.env
|
||||
../../../../Desktop/PostBot/.venv
|
||||
configs/.env
|
||||
.venv
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
@@ -65,3 +65,4 @@ htmlcov/
|
||||
.nox/
|
||||
.pytest_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/loggers" 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" />
|
||||
</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" />
|
||||
</component>
|
||||
<component name="TemplatesService">
|
||||
<option name="TEMPLATE_FOLDERS">
|
||||
<list>
|
||||
<option value="$MODULE_DIR$/locales" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
</module>
|
||||
111
.idea/workspace.xml
generated
111
.idea/workspace.xml
generated
@@ -5,40 +5,57 @@
|
||||
</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" />
|
||||
<change afterPath="$PROJECT_DIR$/bot/bots.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/bot/core/__init__.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/bot/handlers/commands/__init__.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/bot/handlers/commands/help.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/bot/handlers/commands/start.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/bot/handlers/post/__init__.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/bot/handlers/post/create_posts.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/bot/keyboards/__init__.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/bot/keyboards/inline/__init__.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/bot/keyboards/inline/decision.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/bot/keyboards/reply/__init__.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/bot/templates/__init__.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/bot/templates/message_callback.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/bot/utils/interesting_facts.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/bot/utils/pagination.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/configs/__init__.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/configs/cmd_list.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/configs/config.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/docker-compose.yml" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/env_example" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/middleware/loggers/__init__.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/middleware/loggers/logs.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.dockerignore" beforeDir="false" afterPath="$PROJECT_DIR$/.dockerignore" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.env" beforeDir="false" afterPath="$PROJECT_DIR$/.env" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.gitattributes" beforeDir="false" afterPath="$PROJECT_DIR$/.gitattributes" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.gitignore" beforeDir="false" afterPath="$PROJECT_DIR$/.gitignore" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/BotCode/__init__.py" beforeDir="false" afterPath="$PROJECT_DIR$/bot/__init__.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/BotCode/config.py" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/BotCode/core/__init__.py" beforeDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/BotCode/core/storage.py" beforeDir="false" afterPath="$PROJECT_DIR$/bot/core/storage.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/BotCode/handlers/__init__.py" beforeDir="false" afterPath="$PROJECT_DIR$/bot/handlers/__init__.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/BotCode/handlers/callback.py" beforeDir="false" afterPath="$PROJECT_DIR$/bot/handlers/callback.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/BotCode/handlers/commands/__init__.py" beforeDir="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>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
@@ -55,9 +72,21 @@
|
||||
<component name="Git.Settings">
|
||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||
</component>
|
||||
<component name="ProjectColorInfo"><![CDATA[{
|
||||
"associatedIndex": 2
|
||||
<component name="GitHubPullRequestSearchHistory"><![CDATA[{
|
||||
"lastFilter": {
|
||||
"state": "OPEN",
|
||||
"assignee": "Whyverum"
|
||||
}
|
||||
}]]></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="ProjectViewState">
|
||||
<option name="hideEmptyMiddlePackages" value="true" />
|
||||
@@ -68,6 +97,7 @@
|
||||
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
||||
"Python.main.executor": "Run",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"git-widget-placeholder": "master",
|
||||
"node.js.detected.package.eslint": "true",
|
||||
@@ -94,8 +124,9 @@
|
||||
<env name="PYTHONUNBUFFERED" value="1" />
|
||||
</envs>
|
||||
<option name="SDK_HOME" value="" />
|
||||
<option name="SDK_NAME" value="Python 3.13 (PrimoStoryBot)" />
|
||||
<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_SOURCE_ROOTS" value="true" />
|
||||
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
||||
@@ -117,8 +148,8 @@
|
||||
<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" />
|
||||
<option value="bundled-js-predefined-d6986cc7102b-09060db00ec0-JavaScript-PY-251.26927.90" />
|
||||
<option value="bundled-python-sdk-41e8cd69c857-64d779b69b7a-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-251.26927.90" />
|
||||
</set>
|
||||
</attachedChunks>
|
||||
</component>
|
||||
@@ -130,6 +161,7 @@
|
||||
<option name="presentableId" value="Default" />
|
||||
<updated>1747702717100</updated>
|
||||
<workItem from="1747702718164" duration="4191000" />
|
||||
<workItem from="1754837986649" duration="1081000" />
|
||||
</task>
|
||||
<servers />
|
||||
</component>
|
||||
@@ -138,5 +170,6 @@
|
||||
</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$" />
|
||||
<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>
|
||||
</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 ./
|
||||
RUN poetry install --no-interaction --no-root --only main
|
||||
# Обновляем pip для актуальной версии (необязательно, но рекомендуется)
|
||||
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
|
||||
|
||||
Copyright (c) [2025] [Лейн]
|
||||
Copyright (c) [2025] [Verum]
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
78
README.md
78
README.md
@@ -1,31 +1,47 @@
|
||||
# Создание поста
|
||||
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)
|
||||
PROJECT/
|
||||
├── config/
|
||||
│ ├── __init__.py
|
||||
│ ├── settings.py # Основные настройки
|
||||
│ └── roles_config.py # Конфиг ролей и прав
|
||||
├── data/
|
||||
│ ├── database.db # SQLite база (или папка для миграций если PostgreSQL)
|
||||
│ ├── lists/ # JSON/CSV файлы списков (игроков, персонажей и т.д.)
|
||||
│ └── templates/ # Шаблоны сообщений
|
||||
├── handlers/
|
||||
│ ├── __init__.py
|
||||
│ ├── private/ # Обработчики ЛС
|
||||
│ │ ├── commands.py
|
||||
│ │ ├── faq.py
|
||||
│ │ ├── reports.py
|
||||
│ │ └── notifications.py
|
||||
│ ├── groups/ # Обработчики групповых чатов
|
||||
│ │ ├── flood.py
|
||||
│ │ ├── roleplay.py
|
||||
│ │ └── moderation.py
|
||||
│ └── channels/ # Обработчики каналов
|
||||
│ ├── info_updater.py
|
||||
│ └── life_news.py
|
||||
├── middlewares/
|
||||
│ ├── __init__.py
|
||||
│ ├── throttling.py # Анти-спам
|
||||
│ ├── database.py # Интеграция БД
|
||||
│ └── mode_switcher.py # Переключение режимов
|
||||
├── services/
|
||||
│ ├── __init__.py
|
||||
│ ├── 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 .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
|
||||
from os import path, makedirs, listdir
|
||||
from typing import Any, Dict, List, Optional
|
||||
from BotCode.config import POSTS_DIR
|
||||
from BotCode.loggers import logs
|
||||
from configs.config import Project
|
||||
from bot.loggers import logs
|
||||
|
||||
# Настройки экспорта
|
||||
__all__ = ("storage", )
|
||||
|
||||
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.global_posts: 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 .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__)
|
||||
# Настройка экспорта и роутера
|
||||
__all__ = ("router",)
|
||||
router: Router = Router(name="handlers_router")
|
||||
|
||||
# Include routers with different priorities
|
||||
# Подключение роутеров
|
||||
router.include_routers(
|
||||
cmd_routers,
|
||||
callback_router,
|
||||
@@ -1,15 +1,16 @@
|
||||
# BotCode/handlers/callback.py
|
||||
from typing import Optional
|
||||
|
||||
from aiogram import Router, F
|
||||
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("show_alert_"))
|
||||
async def handle_button_alert(callback_query: CallbackQuery) -> None:
|
||||
key = callback_query.data
|
||||
user_id = callback_query.from_user.id
|
||||
key: Optional[str] = callback_query.data
|
||||
user_id: int = callback_query.from_user.id
|
||||
|
||||
# Получаем уведомление через хранилище
|
||||
notif = storage.get_notification(key)
|
||||
@@ -29,11 +30,8 @@ async def handle_button_alert(callback_query: CallbackQuery) -> None:
|
||||
|
||||
try:
|
||||
await callback_query.answer(text=text, show_alert=show_alert)
|
||||
except Exception as e:
|
||||
try:
|
||||
except Exception:
|
||||
await callback_query.answer(text="Произошла ошибка при отображении уведомления.", show_alert=True)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
@router.callback_query(F.data == "void")
|
||||
@@ -44,5 +42,5 @@ async def handle_void_callback(callback_query: CallbackQuery) -> None:
|
||||
"""
|
||||
try:
|
||||
await callback_query.answer()
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
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 BotCode.core.storage import storage
|
||||
from BotCode.utils import textmd2
|
||||
from BotCode.config import PARSE_MODE
|
||||
from BotCode.loggers import logs
|
||||
from bot.core import storage
|
||||
from bot.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
|
||||
|
||||
# Тело сообщения
|
||||
text = textmd2(post.get("text", ""))
|
||||
text = post.get("text", "")
|
||||
image = post.get("image", "")
|
||||
if image and image.startswith("http"):
|
||||
text = f"{hide_link(image)}{text}"
|
||||
@@ -154,10 +152,7 @@ async def inline_query_handler(inline_query: InlineQuery):
|
||||
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
|
||||
),
|
||||
input_message_content=InputTextMessageContent(message_text=text),
|
||||
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 typing import Final
|
||||
|
||||
from aiogram import Router, F
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import (
|
||||
Message, CallbackQuery,
|
||||
InlineKeyboardButton, InlineKeyboardMarkup,
|
||||
@@ -8,14 +11,12 @@ from aiogram.types import (
|
||||
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
|
||||
from bot.core import storage
|
||||
from bot.utils import pagination_btn
|
||||
|
||||
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(
|
||||
message: Message = None,
|
||||
@@ -54,7 +55,7 @@ async def send_posts_list(
|
||||
rows.append([btn])
|
||||
|
||||
# Пагинация
|
||||
nav_buttons = create_pagination_buttons(
|
||||
nav_buttons = pagination_btn(
|
||||
action="open_post_list",
|
||||
page=page,
|
||||
total_posts=total,
|
||||
@@ -67,7 +68,7 @@ async def send_posts_list(
|
||||
rows.append([InlineKeyboardButton(text="Закрыть❌", callback_data="cancel_list")])
|
||||
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=rows)
|
||||
header = "Список ваших постов:"
|
||||
header: str = "Список ваших постов:"
|
||||
|
||||
try:
|
||||
if callback_query:
|
||||
@@ -83,21 +84,25 @@ async def send_posts_list(
|
||||
|
||||
# --- Хендлеры списка ---
|
||||
@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)
|
||||
|
||||
@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 cq.answer()
|
||||
|
||||
@router.callback_query(lambda c: c.data and c.data.startswith("open_post_list_page_"))
|
||||
async def cb_paginate(cq: CallbackQuery):
|
||||
@router.callback_query(F.data.startswith("open_post_list_page_"))
|
||||
async def cb_paginate(cq: CallbackQuery, state: FSMContext):
|
||||
try:
|
||||
page = int(cq.data.rsplit("_", 1)[-1])
|
||||
except ValueError:
|
||||
await cq.answer("Некорректная страница", show_alert=True)
|
||||
return
|
||||
await state.clear()
|
||||
await send_posts_list(callback_query=cq, page=page)
|
||||
await cq.answer()
|
||||
|
||||
@@ -106,9 +111,9 @@ 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_"))
|
||||
@router.callback_query(F.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)
|
||||
@@ -117,7 +122,7 @@ async def view_post_callback(cq: CallbackQuery):
|
||||
return
|
||||
|
||||
post = posts[pid]
|
||||
text = textmd2(post.get("text", ""))
|
||||
text = post.get("text", "")
|
||||
img = post.get("image", "")
|
||||
if img.startswith("http"):
|
||||
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="open_post_list")
|
||||
])
|
||||
rows.append(
|
||||
[InlineKeyboardButton(text="Отправить↪️", switch_inline_query=f"{pid}")])
|
||||
|
||||
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.answer()
|
||||
|
||||
# --- Удаление поста ---
|
||||
|
||||
@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_", "")
|
||||
uid = cq.from_user.id
|
||||
if storage.delete_user_post(uid, pid):
|
||||
await cq.answer(f"Пост {pid} удалён")
|
||||
await state.clear()
|
||||
await send_posts_list(callback_query=cq)
|
||||
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
|
||||
|
||||
try:
|
||||
from config import LogConfig
|
||||
from configs.config import LogConfig
|
||||
except ImportError:
|
||||
class LogConfig:
|
||||
"""Запасные настройки логирования, если 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 .usernames 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 BotCode.config import PARSE_MODE
|
||||
from re import sub, escape
|
||||
from configs.config import BotSettings
|
||||
|
||||
# Настройка экспорта в модули
|
||||
__all__ = ("textmd2",)
|
||||
|
||||
def textmd2(msg: str,
|
||||
parse_mode: str = PARSE_MODE,
|
||||
parse_mode: str = BotSettings.PARSE_MODE,
|
||||
special_chars: str = r"_*[]()~`>#+-=|{}.!") -> str:
|
||||
"""
|
||||
Экранирует специальные символы MarkdownV2 в переданном тексте.
|
||||
@@ -18,7 +18,6 @@ def textmd2(msg: str,
|
||||
:raises TypeError: Если передан не строковый тип данных.
|
||||
:raises ValueError: Если parse_mode задан некорректно.
|
||||
"""
|
||||
from re import sub, escape
|
||||
|
||||
if not isinstance(msg, str):
|
||||
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
|
||||
|
||||
# Настройка экспорта в модули
|
||||
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
|
||||
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,
|
||||
)
|
||||
)
|
||||
from asyncio import run
|
||||
from middleware.loggers import setup_logging
|
||||
from bot import *
|
||||
|
||||
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)
|
||||
|
||||
|
||||
# Вечная загрузка бота
|
||||
if __name__ == "__main__":
|
||||
from asyncio import run
|
||||
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