Private
Public Access
forked from FOSS/AutoShop-Djimbo-Simple
Initial local state
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
.git
|
||||
.gitignore
|
||||
.DS_Store
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
.pytest_cache/
|
||||
.mypy_cache/
|
||||
.ruff_cache/
|
||||
.coverage
|
||||
.coverage.*
|
||||
htmlcov/
|
||||
|
||||
.idea/
|
||||
.vscode/
|
||||
.venv/
|
||||
venv/
|
||||
ENV/
|
||||
env/
|
||||
|
||||
*.db
|
||||
*.db-journal
|
||||
*.db-shm
|
||||
*.db-wal
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
*.log
|
||||
tgbot/data/*.db*
|
||||
tgbot/data/*.sqlite*
|
||||
tgbot/data/*.log
|
||||
@@ -0,0 +1,10 @@
|
||||
BOT_TOKEN=
|
||||
BOT_ADMIN_IDS=
|
||||
BOT_DATABASE_EXPORT=True
|
||||
BOT_STATUS_NOTIFICATION=True
|
||||
BOT_TIMEZONE=Europe/Moscow
|
||||
BOT_USER_CACHE_TTL=300
|
||||
BOT_THROTTLE_RATE=0.5
|
||||
PATH_DATABASE=tgbot/data/database.db
|
||||
PATH_LOGS=tgbot/data/logs.log
|
||||
YOOMONEY_CLIENT_ID=
|
||||
+217
@@ -0,0 +1,217 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
.DS_Store
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
env/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*,cover
|
||||
.hypothesis/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# IPython Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# celery beat schedule file
|
||||
celerybeat-schedule
|
||||
|
||||
# dotenv
|
||||
.env
|
||||
|
||||
# virtualenv
|
||||
venv/
|
||||
ENV/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
### VirtualEnv template
|
||||
# Virtualenv
|
||||
# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/
|
||||
[Bb]in
|
||||
[Ii]nclude
|
||||
[Ll]ib
|
||||
[Ll]ib64
|
||||
[Ll]ocal
|
||||
[Ss]cripts
|
||||
pyvenv.cfg
|
||||
.venv
|
||||
pip-selfcheck.json
|
||||
|
||||
### JetBrains template
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# AWS User-specific
|
||||
.idea/**/aws.xml
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
auto-import.
|
||||
.idea/artifacts
|
||||
.idea/compiler.xml
|
||||
.idea/jarRepositories.xml
|
||||
.idea/modules.xml
|
||||
.idea/*.iml
|
||||
.idea/modules
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# SonarLint plugin
|
||||
.idea/sonarlint/
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
# idea folder, uncomment if you don't need it
|
||||
.idea
|
||||
|
||||
# Users exceptions
|
||||
/other/
|
||||
/tgbot/data/database
|
||||
/tgbot/data/database.db
|
||||
/tgbot/data/*.db
|
||||
/tgbot/data/*.db-wal
|
||||
/tgbot/data/*.db-shm
|
||||
/tgbot/data/xdatabase.db
|
||||
/tgbot/data/logs.log
|
||||
/settings.ini
|
||||
settings.ini
|
||||
main.sh
|
||||
database.*
|
||||
database.db
|
||||
|
||||
# Local secrets and runtime data
|
||||
.env.*
|
||||
!.env.example
|
||||
.vscode/
|
||||
.pytest_cache/
|
||||
.mypy_cache/
|
||||
.ruff_cache/
|
||||
*.db
|
||||
*.db-journal
|
||||
*.db-shm
|
||||
*.db-wal
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
*.log
|
||||
tgbot/data/*.db*
|
||||
tgbot/data/*.sqlite*
|
||||
tgbot/data/*.log
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PIP_NO_CACHE_DIR=1
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN useradd --create-home --shell /usr/sbin/nologin bot
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
COPY --chown=bot:bot . .
|
||||
RUN mkdir -p /app/tgbot/data && chown -R bot:bot /app
|
||||
|
||||
USER bot
|
||||
|
||||
CMD ["sh", "-c", "python migrate.py up && python main.py"]
|
||||
@@ -0,0 +1,61 @@
|
||||
# Telegram Autoshop Bot by Djimbo
|
||||
|
||||
Telegram bot на `aiogram 3` для магазина с категориями, позициями, товарами, покупками, пополнениями и админкой.
|
||||
|
||||
## Быстрый запуск
|
||||
|
||||
1. Установи Python `3.11+`.
|
||||
2. Поставь зависимости:
|
||||
|
||||
```bash
|
||||
python3.11 -m pip install -r requirements.txt
|
||||
```
|
||||
|
||||
3. Создай `.env` по примеру:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
4. Заполни минимум:
|
||||
|
||||
```env
|
||||
BOT_TOKEN=
|
||||
BOT_ADMIN_IDS=
|
||||
YOOMONEY_CLIENT_ID=
|
||||
```
|
||||
|
||||
Конфигурация читается только из `.env` или переменных окружения.
|
||||
|
||||
5. Примени миграции:
|
||||
|
||||
```bash
|
||||
python3 migrate.py up
|
||||
```
|
||||
|
||||
6. Запусти бота:
|
||||
|
||||
```bash
|
||||
python3 main.py
|
||||
```
|
||||
|
||||
## Миграции
|
||||
|
||||
Основные команды:
|
||||
|
||||
```bash
|
||||
python3 migrate.py status
|
||||
python3 migrate.py up
|
||||
python3 migrate.py down
|
||||
python3 migrate.py auto "change description"
|
||||
```
|
||||
|
||||
Схема БД теперь живет в `migrations/`, модели — в `tgbot/database/db_*.py`.
|
||||
|
||||
## Docker
|
||||
|
||||
```bash
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
Контейнер сам применяет миграции перед запуском.
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
[alembic]
|
||||
script_location = migrations
|
||||
prepend_sys_path = .
|
||||
path_separator = os
|
||||
file_template = %%(year)d%%(month).2d%%(day).2d_%%(hour).2d%%(minute).2d_%%(rev)s_%%(slug)s
|
||||
timezone = Europe/Moscow
|
||||
|
||||
sqlalchemy.url = sqlite+aiosqlite:///tgbot/data/database.db
|
||||
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARNING
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARNING
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# supervisorctl
|
||||
[program:dj_shop]
|
||||
directory=/root/autoshopDjimbo/
|
||||
command=python3.11 -u main.py
|
||||
environment=PYTHONUNBUFFERED="1"
|
||||
|
||||
autostart=True
|
||||
autorestart=True
|
||||
|
||||
stderr_logfile=/root/autoshopDjimbo/tgbot/data/sv_log_err.log
|
||||
stderr_logfile_maxbytes=50MB
|
||||
stdout_logfile=/root/autoshopDjimbo/tgbot/data/sv_log_out.log
|
||||
stdout_logfile_maxbytes=50MB
|
||||
@@ -0,0 +1,11 @@
|
||||
services:
|
||||
bot:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env
|
||||
volumes:
|
||||
- ./tgbot/data:/app/tgbot/data
|
||||
stop_grace_period: 30s
|
||||
@@ -0,0 +1,196 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
import colorama
|
||||
from aiogram import Bot, Dispatcher
|
||||
from aiogram.client.default import DefaultBotProperties
|
||||
from aiogram.client.session.aiohttp import AiohttpSession
|
||||
from aiogram.enums import ParseMode
|
||||
|
||||
from tgbot.data.config import BOT_DATABASE_EXPORT, BOT_SCHEDULER, BOT_TOKEN, get_admins, validate_bot_config
|
||||
from tgbot.database.core import close_database
|
||||
from tgbot.database.repository import prepare_database
|
||||
from tgbot.middlewares import register_all_middlewares
|
||||
from tgbot.routers import register_all_routers
|
||||
from tgbot.services.api_session import AsyncRequestSession
|
||||
from tgbot.utils.misc.bot_commands import set_commands
|
||||
from tgbot.utils.misc.bot_logging import bot_logger
|
||||
from tgbot.utils.misc.bot_models import ARS
|
||||
from tgbot.utils.misc_functions import (
|
||||
autobackup_admin,
|
||||
autosettings_unix,
|
||||
check_bot_username,
|
||||
check_mail,
|
||||
check_update,
|
||||
startup_notify,
|
||||
update_profit_day,
|
||||
update_profit_month,
|
||||
update_profit_week,
|
||||
)
|
||||
|
||||
|
||||
# Включение мгновенного вывода консоли
|
||||
def configure_console_output() -> None:
|
||||
for stream in (sys.stdout, sys.stderr):
|
||||
if hasattr(stream, "reconfigure"):
|
||||
stream.reconfigure(line_buffering=True, write_through=True)
|
||||
|
||||
|
||||
configure_console_output()
|
||||
colorama.init()
|
||||
|
||||
|
||||
# Подключение шедулеров
|
||||
async def scheduler_start(bot: Bot, arSession: ARS):
|
||||
# Автоматический сброс ежемесячной статистики
|
||||
BOT_SCHEDULER.add_job(
|
||||
update_profit_month,
|
||||
trigger="cron",
|
||||
day=1,
|
||||
hour=0,
|
||||
minute=0,
|
||||
second=5,
|
||||
id="update_profit_month",
|
||||
replace_existing=True,
|
||||
coalesce=True,
|
||||
misfire_grace_time=60,
|
||||
)
|
||||
|
||||
# Автоматический сброс еженедельной статистики
|
||||
BOT_SCHEDULER.add_job(
|
||||
update_profit_week,
|
||||
trigger="cron",
|
||||
day_of_week="mon",
|
||||
hour=0,
|
||||
minute=0,
|
||||
second=10,
|
||||
id="update_profit_week",
|
||||
replace_existing=True,
|
||||
coalesce=True,
|
||||
misfire_grace_time=60,
|
||||
)
|
||||
|
||||
# Автоматический сброс ежедневной статистики
|
||||
BOT_SCHEDULER.add_job(
|
||||
update_profit_day,
|
||||
trigger="cron",
|
||||
hour=0,
|
||||
minute=0,
|
||||
second=15,
|
||||
args=(bot,),
|
||||
id="update_profit_day",
|
||||
replace_existing=True,
|
||||
coalesce=True,
|
||||
misfire_grace_time=60,
|
||||
)
|
||||
|
||||
# Автоматический бэкап БД (если включено)
|
||||
if BOT_DATABASE_EXPORT:
|
||||
BOT_SCHEDULER.add_job(
|
||||
autobackup_admin,
|
||||
trigger="cron",
|
||||
hour=0,
|
||||
args=(bot,),
|
||||
id="autobackup_admin",
|
||||
replace_existing=True,
|
||||
coalesce=True,
|
||||
misfire_grace_time=60,
|
||||
)
|
||||
|
||||
# Ежедневная проверка обновлений
|
||||
BOT_SCHEDULER.add_job(
|
||||
check_update,
|
||||
trigger="cron",
|
||||
hour=0,
|
||||
args=(bot, arSession),
|
||||
id="check_update",
|
||||
replace_existing=True,
|
||||
coalesce=True,
|
||||
misfire_grace_time=60,
|
||||
)
|
||||
|
||||
# Ежедневная проверка важных пушей
|
||||
BOT_SCHEDULER.add_job(
|
||||
check_mail,
|
||||
trigger="cron",
|
||||
hour=12,
|
||||
args=(bot, arSession),
|
||||
id="check_mail",
|
||||
replace_existing=True,
|
||||
coalesce=True,
|
||||
misfire_grace_time=60,
|
||||
)
|
||||
|
||||
if not BOT_SCHEDULER.running:
|
||||
BOT_SCHEDULER.start()
|
||||
|
||||
|
||||
# Запуск миграций, роутеров и polling
|
||||
async def main():
|
||||
validate_bot_config()
|
||||
await prepare_database()
|
||||
|
||||
dp = Dispatcher()
|
||||
arSession = AsyncRequestSession()
|
||||
|
||||
proxy = ""
|
||||
session = AiohttpSession(proxy=proxy) if len(proxy) > 1 else AiohttpSession()
|
||||
|
||||
bot = Bot(
|
||||
token=BOT_TOKEN,
|
||||
session=session,
|
||||
default=DefaultBotProperties(
|
||||
parse_mode=ParseMode.HTML
|
||||
),
|
||||
)
|
||||
|
||||
register_all_middlewares(dp)
|
||||
register_all_routers(dp)
|
||||
|
||||
try:
|
||||
await autosettings_unix()
|
||||
await set_commands(bot)
|
||||
await check_bot_username(bot)
|
||||
await check_update(bot, arSession)
|
||||
await check_mail(bot, arSession)
|
||||
await startup_notify(bot, arSession)
|
||||
await scheduler_start(bot, arSession)
|
||||
|
||||
bot_info = await bot.get_me()
|
||||
bot_logger.info("Бот запущен: @%s", bot_info.username)
|
||||
print(colorama.Fore.LIGHTYELLOW_EX + f"~~~~~ Бот запущен - @{bot_info.username} ~~~~~")
|
||||
print(colorama.Fore.LIGHTBLUE_EX + "~~~~~ TG developer - @djimbox ~~~~~")
|
||||
print(colorama.Fore.RESET)
|
||||
|
||||
if len(get_admins()) == 0:
|
||||
print("***** УКАЖИТЕ BOT_ADMIN_IDS В .env *****")
|
||||
|
||||
await bot.delete_webhook()
|
||||
await bot.get_updates(offset=-1)
|
||||
|
||||
await dp.start_polling(
|
||||
bot,
|
||||
arSession=arSession,
|
||||
allowed_updates=dp.resolve_used_update_types(),
|
||||
)
|
||||
finally:
|
||||
if BOT_SCHEDULER.running:
|
||||
BOT_SCHEDULER.shutdown(wait=False)
|
||||
|
||||
await arSession.close()
|
||||
await bot.session.close()
|
||||
await close_database()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
bot_logger.warning("Бот остановлен")
|
||||
finally:
|
||||
if sys.platform.startswith("win"):
|
||||
os.system("cls")
|
||||
else:
|
||||
os.system("clear")
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
import argparse
|
||||
from typing import Optional
|
||||
|
||||
from alembic import command
|
||||
from alembic.config import Config
|
||||
|
||||
HELP_TEXT = """
|
||||
Миграции базы данных
|
||||
|
||||
Безопасное правило:
|
||||
python migrate.py только показывает эту справку
|
||||
действия с БД выполняются только при явной команде
|
||||
|
||||
Основные команды:
|
||||
python migrate.py help показать эту справку
|
||||
python migrate.py up применить все миграции до head
|
||||
python migrate.py down откатить последнюю миграцию
|
||||
python migrate.py new "add users" создать пустую миграцию
|
||||
python migrate.py auto "add users" создать миграцию по SQLAlchemy-моделям
|
||||
python migrate.py status показать текущую версию и последние версии
|
||||
|
||||
Короткие алиасы:
|
||||
up -> upgrade
|
||||
down -> downgrade
|
||||
new -> revision
|
||||
auto -> revision --autogenerate
|
||||
autogen -> auto
|
||||
cur -> current
|
||||
hist -> history
|
||||
st -> status
|
||||
""".strip()
|
||||
|
||||
|
||||
# Сбор CLI-парсера миграций
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Удобный CLI для Alembic-миграций",
|
||||
formatter_class=argparse.RawTextHelpFormatter,
|
||||
)
|
||||
subparsers = parser.add_subparsers(dest="command")
|
||||
|
||||
help_parser = subparsers.add_parser("help", aliases=["h"], help="Показать понятную справку")
|
||||
help_parser.set_defaults(action=show_help)
|
||||
|
||||
upgrade_parser = subparsers.add_parser("upgrade", aliases=["up"], help="Применить миграции")
|
||||
upgrade_parser.add_argument("revision", nargs="?", default="head", help="Версия миграции, по умолчанию head")
|
||||
upgrade_parser.set_defaults(action=run_upgrade)
|
||||
|
||||
downgrade_parser = subparsers.add_parser("downgrade", aliases=["down"], help="Откатить миграции")
|
||||
downgrade_parser.add_argument("revision", nargs="?", default="-1", help="Версия отката, по умолчанию -1")
|
||||
downgrade_parser.set_defaults(action=run_downgrade)
|
||||
|
||||
revision_parser = subparsers.add_parser("revision", aliases=["new"], help="Создать пустую миграцию")
|
||||
revision_parser.add_argument("message", nargs="?", help="Название миграции")
|
||||
revision_parser.add_argument("-m", "--message-option", dest="message_option", help="Название миграции")
|
||||
revision_parser.add_argument("--autogenerate", "-a", action="store_true", help="Собрать изменения из моделей")
|
||||
revision_parser.set_defaults(action=run_revision)
|
||||
|
||||
auto_parser = subparsers.add_parser("auto", aliases=["autogen"], help="Создать миграцию по моделям")
|
||||
auto_parser.add_argument("message", nargs="?", help="Название миграции")
|
||||
auto_parser.add_argument("-m", "--message-option", dest="message_option", help="Название миграции")
|
||||
auto_parser.set_defaults(action=run_auto_revision)
|
||||
|
||||
current_parser = subparsers.add_parser("current", aliases=["cur"], help="Показать текущую версию БД")
|
||||
current_parser.set_defaults(action=run_current)
|
||||
|
||||
history_parser = subparsers.add_parser("history", aliases=["hist"], help="Показать историю миграций")
|
||||
history_parser.set_defaults(action=run_history)
|
||||
|
||||
heads_parser = subparsers.add_parser("heads", help="Показать последние версии веток")
|
||||
heads_parser.set_defaults(action=run_heads)
|
||||
|
||||
status_parser = subparsers.add_parser("status", aliases=["st"], help="Показать текущую версию и heads")
|
||||
status_parser.set_defaults(action=run_status)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
# Просмотр справки по миграциям
|
||||
def show_help(_config: Optional[Config], _args: argparse.Namespace) -> None:
|
||||
print(HELP_TEXT)
|
||||
|
||||
|
||||
# Применение миграций до версии
|
||||
def run_upgrade(config: Config, args: argparse.Namespace) -> None:
|
||||
print(f"Применяю миграции до версии: {args.revision}")
|
||||
command.upgrade(config, args.revision)
|
||||
print("Готово")
|
||||
|
||||
|
||||
# Откат миграции до версии
|
||||
def run_downgrade(config: Config, args: argparse.Namespace) -> None:
|
||||
print(f"Откатываю миграции до версии: {args.revision}")
|
||||
command.downgrade(config, args.revision)
|
||||
print("Готово")
|
||||
|
||||
|
||||
# Создание пустой миграции
|
||||
def run_revision(config: Config, args: argparse.Namespace) -> None:
|
||||
message = get_revision_message(args)
|
||||
print(f"Создаю миграцию: {message}")
|
||||
config.attributes["autogenerate_empty"] = False
|
||||
command.revision(config, message=message, autogenerate=args.autogenerate)
|
||||
if config.attributes.get("autogenerate_empty"):
|
||||
print("Изменений в моделях не найдено, файл миграции не создан")
|
||||
return
|
||||
|
||||
print("Готово")
|
||||
|
||||
|
||||
# Создание миграции по моделям
|
||||
def run_auto_revision(config: Config, args: argparse.Namespace) -> None:
|
||||
message = get_revision_message(args)
|
||||
print(f"Создаю миграцию по моделям: {message}")
|
||||
config.attributes["autogenerate_empty"] = False
|
||||
command.revision(config, message=message, autogenerate=True)
|
||||
if config.attributes.get("autogenerate_empty"):
|
||||
print("Изменений в моделях не найдено, файл миграции не создан")
|
||||
return
|
||||
|
||||
print("Готово")
|
||||
|
||||
|
||||
# Просмотр текущей версии БД
|
||||
def run_current(config: Config, _args: argparse.Namespace) -> None:
|
||||
print("Текущая версия БД:")
|
||||
command.current(config)
|
||||
|
||||
|
||||
# Просмотр истории миграций
|
||||
def run_history(config: Config, _args: argparse.Namespace) -> None:
|
||||
print("История миграций:")
|
||||
command.history(config)
|
||||
|
||||
|
||||
# Просмотр heads миграций
|
||||
def run_heads(config: Config, _args: argparse.Namespace) -> None:
|
||||
print("Последние версии миграций:")
|
||||
command.heads(config)
|
||||
|
||||
|
||||
# Просмотр текущей версии и heads
|
||||
def run_status(config: Config, args: argparse.Namespace) -> None:
|
||||
run_current(config, args)
|
||||
print()
|
||||
run_heads(config, args)
|
||||
|
||||
|
||||
# Получение названия миграции
|
||||
def get_revision_message(args: argparse.Namespace) -> str:
|
||||
message: Optional[str] = args.message_option or args.message
|
||||
|
||||
if not message:
|
||||
raise SystemExit("Укажи название миграции. Пример: python migrate.py auto \"add users\"")
|
||||
|
||||
return message
|
||||
|
||||
|
||||
# Получение Alembic-конфига
|
||||
def get_config() -> Config:
|
||||
from tgbot.database.migration_runner import get_alembic_config
|
||||
|
||||
return get_alembic_config()
|
||||
|
||||
|
||||
# Запуск CLI миграций
|
||||
def main() -> None:
|
||||
parser = build_parser()
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command is None:
|
||||
show_help(None, args)
|
||||
return
|
||||
|
||||
if args.action == show_help:
|
||||
show_help(None, args)
|
||||
return
|
||||
|
||||
if args.action in (run_revision, run_auto_revision):
|
||||
get_revision_message(args)
|
||||
|
||||
config = get_config()
|
||||
args.action(config, args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,184 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
import asyncio
|
||||
from logging.config import fileConfig
|
||||
from typing import Any, List, Optional
|
||||
|
||||
from alembic import context
|
||||
from alembic.operations import ops
|
||||
from sqlalchemy import pool, text
|
||||
from sqlalchemy.engine import Connection
|
||||
from sqlalchemy.schema import DefaultClause
|
||||
from sqlalchemy.ext.asyncio import async_engine_from_config
|
||||
|
||||
from tgbot.database.core import Base
|
||||
|
||||
import tgbot.database # noqa: F401
|
||||
|
||||
config = context.config
|
||||
|
||||
if config.config_file_name is not None:
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
target_metadata = Base.metadata
|
||||
INDEX_DIFF_NOISE = {"ix_storage_payments_id", "ix_storage_settings_id"}
|
||||
|
||||
|
||||
# Отбрасывание SQLite-шума из автогенерации
|
||||
def _filter_autogenerated_ops(container: Any) -> None:
|
||||
filtered_ops = []
|
||||
|
||||
for operation in container.ops:
|
||||
if isinstance(operation, ops.AlterColumnOp):
|
||||
continue
|
||||
|
||||
if _is_index_noise(operation):
|
||||
continue
|
||||
|
||||
if isinstance(operation, ops.ModifyTableOps):
|
||||
_filter_autogenerated_ops(operation)
|
||||
|
||||
if operation.ops:
|
||||
filtered_ops.append(operation)
|
||||
|
||||
continue
|
||||
|
||||
filtered_ops.append(operation)
|
||||
|
||||
container.ops = filtered_ops
|
||||
|
||||
|
||||
# Подготовка новых колонок к безопасному SQLite ADD COLUMN
|
||||
def _prepare_sqlite_add_columns(container: Any) -> None:
|
||||
for operation in container.ops:
|
||||
if isinstance(operation, ops.AddColumnOp):
|
||||
_apply_server_default(operation.column)
|
||||
continue
|
||||
|
||||
if isinstance(operation, ops.ModifyTableOps):
|
||||
_prepare_sqlite_add_columns(operation)
|
||||
|
||||
|
||||
# Перенос default модели в server_default миграции
|
||||
def _apply_server_default(column: Any) -> None:
|
||||
if column.nullable or column.server_default is not None or column.default is None:
|
||||
return
|
||||
|
||||
default_value = _get_simple_default(column.default.arg)
|
||||
|
||||
if default_value is None:
|
||||
return
|
||||
|
||||
column.server_default = DefaultClause(text(default_value))
|
||||
|
||||
|
||||
# Получение SQL-литерала для простых default-значений
|
||||
def _get_simple_default(value: Any) -> Optional[str]:
|
||||
if callable(value):
|
||||
return None
|
||||
|
||||
if isinstance(value, bool):
|
||||
return "1" if value else "0"
|
||||
|
||||
if isinstance(value, (int, float)):
|
||||
return str(value)
|
||||
|
||||
if isinstance(value, str):
|
||||
return repr(value)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# Игнорирование индексов, которые SQLite отражает иначе
|
||||
def _is_index_noise(operation: Any) -> bool:
|
||||
if not isinstance(operation, (ops.CreateIndexOp, ops.DropIndexOp)):
|
||||
return False
|
||||
|
||||
return getattr(operation, "index_name", "") in INDEX_DIFF_NOISE
|
||||
|
||||
|
||||
# Ограничение сравнения SQLite только реальными добавлениями/удалениями
|
||||
def _include_object(object_: Any, name: str, type_: str, reflected: bool, compare_to: Any) -> bool:
|
||||
if type_ == "column" and compare_to is not None:
|
||||
return False
|
||||
|
||||
if type_ == "index" and name in INDEX_DIFF_NOISE:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# Очистка автосозданной миграции перед записью
|
||||
def _process_revision_directives(_context: Any, _revision: Any, directives: List[Any]) -> None:
|
||||
if not directives:
|
||||
return
|
||||
|
||||
script = directives[0]
|
||||
_prepare_sqlite_add_columns(script.upgrade_ops)
|
||||
_filter_autogenerated_ops(script.upgrade_ops)
|
||||
_filter_autogenerated_ops(script.downgrade_ops)
|
||||
|
||||
if not script.upgrade_ops.ops and not script.downgrade_ops.ops:
|
||||
config.attributes["autogenerate_empty"] = True
|
||||
directives.clear()
|
||||
|
||||
|
||||
# Запуск миграций без подключения
|
||||
def run_migrations_offline() -> None:
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
|
||||
context.configure(
|
||||
url=url,
|
||||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
dialect_opts={"paramstyle": "named"},
|
||||
render_as_batch=True,
|
||||
compare_type=False,
|
||||
include_object=_include_object,
|
||||
process_revision_directives=_process_revision_directives,
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
# Запуск миграций на соединении
|
||||
def do_run_migrations(connection: Connection) -> None:
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=target_metadata,
|
||||
render_as_batch=True,
|
||||
compare_type=False,
|
||||
include_object=_include_object,
|
||||
process_revision_directives=_process_revision_directives,
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
# Запуск миграций через async-engine
|
||||
async def run_async_migrations() -> None:
|
||||
connectable = async_engine_from_config(
|
||||
config.get_section(config.config_ini_section, {}),
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
connection = connectable.connect()
|
||||
|
||||
try:
|
||||
await connection.start()
|
||||
await connection.run_sync(do_run_migrations)
|
||||
finally:
|
||||
await connection.close()
|
||||
|
||||
await connectable.dispose()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
external_connection = config.attributes.get("connection")
|
||||
|
||||
if external_connection is not None:
|
||||
do_run_migrations(external_connection)
|
||||
else:
|
||||
asyncio.run(run_async_migrations())
|
||||
@@ -0,0 +1,24 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
revision: str = ${repr(up_revision)}
|
||||
down_revision: Union[str, None] = ${repr(down_revision)}
|
||||
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
||||
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
${downgrades if downgrades else "pass"}
|
||||
@@ -0,0 +1,515 @@
|
||||
"""Начальная схема Телеграм-магазина
|
||||
|
||||
Revision ID: 0001_shop_schema
|
||||
Revises:
|
||||
Create Date: 2026-05-29 00:00:00
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision: str = "0001_shop_schema"
|
||||
down_revision: Union[str, None] = None
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
# Проверка наличия таблицы
|
||||
def _table_exists(table_name: str) -> bool:
|
||||
bind = op.get_bind()
|
||||
result = bind.execute(
|
||||
sa.text(
|
||||
"""
|
||||
SELECT name
|
||||
FROM sqlite_master
|
||||
WHERE type = 'table'
|
||||
AND name = :table_name
|
||||
"""
|
||||
),
|
||||
{"table_name": table_name},
|
||||
)
|
||||
|
||||
return result.scalar_one_or_none() is not None
|
||||
|
||||
|
||||
# Проверка наличия колонки
|
||||
def _column_exists(table_name: str, column_name: str) -> bool:
|
||||
bind = op.get_bind()
|
||||
result = bind.execute(sa.text(f"PRAGMA table_info({table_name})"))
|
||||
|
||||
return column_name in [row.name for row in result]
|
||||
|
||||
|
||||
# Добавление колонки, если ее нет
|
||||
def _add_column(table_name: str, column_name: str, column_sql: str) -> None:
|
||||
if not _column_exists(table_name, column_name):
|
||||
op.execute(sa.text(f"ALTER TABLE {table_name} ADD COLUMN {column_name} {column_sql}"))
|
||||
|
||||
|
||||
# Создание индекса, если его нет
|
||||
def _create_index(index_name: str, table_name: str, columns: str, *, unique: bool = False) -> None:
|
||||
unique_sql = "UNIQUE " if unique else ""
|
||||
op.execute(sa.text(f"CREATE {unique_sql}INDEX IF NOT EXISTS {index_name} ON {table_name} ({columns})"))
|
||||
|
||||
|
||||
# Нормализация текстовых bool-значений
|
||||
def _normalize_boolean_text(table_name: str, column_name: str, default_value: str) -> None:
|
||||
bind = op.get_bind()
|
||||
bind.execute(
|
||||
sa.text(
|
||||
f"""
|
||||
UPDATE {table_name}
|
||||
SET {column_name} = CASE LOWER(CAST(COALESCE({column_name}, :default_value) AS TEXT))
|
||||
WHEN '1' THEN 'True'
|
||||
WHEN 'true' THEN 'True'
|
||||
WHEN 'yes' THEN 'True'
|
||||
WHEN 'on' THEN 'True'
|
||||
WHEN 'да' THEN 'True'
|
||||
WHEN '0' THEN 'False'
|
||||
WHEN 'false' THEN 'False'
|
||||
WHEN 'no' THEN 'False'
|
||||
WHEN 'off' THEN 'False'
|
||||
WHEN 'нет' THEN 'False'
|
||||
ELSE :default_value
|
||||
END
|
||||
"""
|
||||
),
|
||||
{"default_value": default_value},
|
||||
)
|
||||
|
||||
|
||||
# Сбор SQL для переноса текстовой колонки
|
||||
def _copy_expr(table_name: str, column_name: str, default_value: str) -> str:
|
||||
if _column_exists(table_name, column_name):
|
||||
return f"COALESCE({column_name}, '{default_value}')"
|
||||
|
||||
return f"'{default_value}'"
|
||||
|
||||
|
||||
# Сбор SQL для переноса числовой колонки
|
||||
def _copy_int_expr(table_name: str, column_name: str, default_value: int = 0) -> str:
|
||||
if _column_exists(table_name, column_name):
|
||||
return f"COALESCE({column_name}, {default_value})"
|
||||
|
||||
return str(default_value)
|
||||
|
||||
|
||||
# Создание или правка таблицы пользователей
|
||||
def _ensure_users() -> None:
|
||||
bind = op.get_bind()
|
||||
|
||||
if not _table_exists("storage_users"):
|
||||
op.create_table(
|
||||
"storage_users",
|
||||
sa.Column("increment", sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column("user_id", sa.BigInteger(), nullable=False),
|
||||
sa.Column("user_login", sa.String(length=255), nullable=False, server_default=""),
|
||||
sa.Column("user_name", sa.String(length=255), nullable=False, server_default=""),
|
||||
sa.Column("user_surname", sa.String(length=255), nullable=False, server_default=""),
|
||||
sa.Column("user_fullname", sa.String(length=511), nullable=False, server_default=""),
|
||||
sa.Column("user_balance", sa.Float(), nullable=False, server_default="0"),
|
||||
sa.Column("user_refill", sa.Float(), nullable=False, server_default="0"),
|
||||
sa.Column("user_give", sa.Float(), nullable=False, server_default="0"),
|
||||
sa.Column("user_unix", sa.Integer(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("increment"),
|
||||
)
|
||||
else:
|
||||
_add_column("storage_users", "user_login", "TEXT NOT NULL DEFAULT ''")
|
||||
_add_column("storage_users", "user_name", "TEXT NOT NULL DEFAULT ''")
|
||||
_add_column("storage_users", "user_surname", "TEXT NOT NULL DEFAULT ''")
|
||||
_add_column("storage_users", "user_fullname", "TEXT NOT NULL DEFAULT ''")
|
||||
_add_column("storage_users", "user_balance", "REAL NOT NULL DEFAULT 0")
|
||||
_add_column("storage_users", "user_refill", "REAL NOT NULL DEFAULT 0")
|
||||
_add_column("storage_users", "user_give", "REAL NOT NULL DEFAULT 0")
|
||||
_add_column("storage_users", "user_unix", "INTEGER NOT NULL DEFAULT 0")
|
||||
bind.execute(sa.text("UPDATE storage_users SET user_login = COALESCE(user_login, '')"))
|
||||
bind.execute(sa.text("UPDATE storage_users SET user_name = COALESCE(user_name, '')"))
|
||||
bind.execute(sa.text("UPDATE storage_users SET user_surname = COALESCE(user_surname, '')"))
|
||||
bind.execute(
|
||||
sa.text(
|
||||
"""
|
||||
UPDATE storage_users
|
||||
SET user_fullname = CASE
|
||||
WHEN user_fullname IS NULL OR user_fullname = '' THEN TRIM(user_name || ' ' || user_surname)
|
||||
ELSE user_fullname
|
||||
END
|
||||
"""
|
||||
)
|
||||
)
|
||||
bind.execute(sa.text("UPDATE storage_users SET user_balance = COALESCE(user_balance, 0)"))
|
||||
bind.execute(sa.text("UPDATE storage_users SET user_refill = COALESCE(user_refill, 0)"))
|
||||
bind.execute(sa.text("UPDATE storage_users SET user_give = COALESCE(user_give, 0)"))
|
||||
bind.execute(sa.text("UPDATE storage_users SET user_unix = COALESCE(user_unix, 0)"))
|
||||
bind.execute(sa.text("DELETE FROM storage_users WHERE user_id IS NULL"))
|
||||
bind.execute(
|
||||
sa.text(
|
||||
"""
|
||||
DELETE
|
||||
FROM storage_users
|
||||
WHERE user_id IS NOT NULL
|
||||
AND rowid NOT IN (
|
||||
SELECT MAX(rowid)
|
||||
FROM storage_users
|
||||
WHERE user_id IS NOT NULL
|
||||
GROUP BY user_id
|
||||
)
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
_create_index("ix_storage_users_user_id", "storage_users", "user_id", unique=True)
|
||||
|
||||
|
||||
# Создание или правка таблицы настроек
|
||||
def _ensure_settings() -> None:
|
||||
bind = op.get_bind()
|
||||
|
||||
if not _table_exists("storage_settings"):
|
||||
op.create_table(
|
||||
"storage_settings",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("status_work", sa.String(length=16), nullable=False, server_default="True"),
|
||||
sa.Column("status_refill", sa.String(length=16), nullable=False, server_default="False"),
|
||||
sa.Column("status_buy", sa.String(length=16), nullable=False, server_default="False"),
|
||||
sa.Column("notification_refill", sa.String(length=16), nullable=False, server_default="True"),
|
||||
sa.Column("notification_buy", sa.String(length=16), nullable=False, server_default="False"),
|
||||
sa.Column("misc_faq", sa.String(), nullable=False, server_default="None"),
|
||||
sa.Column("misc_support", sa.String(), nullable=False, server_default="None"),
|
||||
sa.Column("misc_bot", sa.String(length=255), nullable=False, server_default="None"),
|
||||
sa.Column("misc_hosting_text", sa.String(length=64), nullable=False, server_default="telegraph"),
|
||||
sa.Column("misc_token_telegraph", sa.String(), nullable=False, server_default="None"),
|
||||
sa.Column("misc_discord_webhook_url", sa.String(), nullable=False, server_default="None"),
|
||||
sa.Column("misc_discord_webhook_name", sa.String(length=255), nullable=False, server_default="None"),
|
||||
sa.Column("misc_hide_category", sa.String(length=16), nullable=False, server_default="False"),
|
||||
sa.Column("misc_hide_position", sa.String(length=16), nullable=False, server_default="False"),
|
||||
sa.Column("misc_profit_day", sa.Integer(), nullable=False, server_default="0"),
|
||||
sa.Column("misc_profit_week", sa.Integer(), nullable=False, server_default="0"),
|
||||
sa.Column("misc_profit_month", sa.Integer(), nullable=False, server_default="0"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
else:
|
||||
bind.execute(sa.text("DROP TABLE IF EXISTS storage_settings_new"))
|
||||
op.create_table(
|
||||
"storage_settings_new",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("status_work", sa.String(length=16), nullable=False, server_default="True"),
|
||||
sa.Column("status_refill", sa.String(length=16), nullable=False, server_default="False"),
|
||||
sa.Column("status_buy", sa.String(length=16), nullable=False, server_default="False"),
|
||||
sa.Column("notification_refill", sa.String(length=16), nullable=False, server_default="True"),
|
||||
sa.Column("notification_buy", sa.String(length=16), nullable=False, server_default="False"),
|
||||
sa.Column("misc_faq", sa.String(), nullable=False, server_default="None"),
|
||||
sa.Column("misc_support", sa.String(), nullable=False, server_default="None"),
|
||||
sa.Column("misc_bot", sa.String(length=255), nullable=False, server_default="None"),
|
||||
sa.Column("misc_hosting_text", sa.String(length=64), nullable=False, server_default="telegraph"),
|
||||
sa.Column("misc_token_telegraph", sa.String(), nullable=False, server_default="None"),
|
||||
sa.Column("misc_discord_webhook_url", sa.String(), nullable=False, server_default="None"),
|
||||
sa.Column("misc_discord_webhook_name", sa.String(length=255), nullable=False, server_default="None"),
|
||||
sa.Column("misc_hide_category", sa.String(length=16), nullable=False, server_default="False"),
|
||||
sa.Column("misc_hide_position", sa.String(length=16), nullable=False, server_default="False"),
|
||||
sa.Column("misc_profit_day", sa.Integer(), nullable=False, server_default="0"),
|
||||
sa.Column("misc_profit_week", sa.Integer(), nullable=False, server_default="0"),
|
||||
sa.Column("misc_profit_month", sa.Integer(), nullable=False, server_default="0"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
bind.execute(
|
||||
sa.text(
|
||||
f"""
|
||||
INSERT INTO storage_settings_new (
|
||||
id,
|
||||
status_work,
|
||||
status_refill,
|
||||
status_buy,
|
||||
notification_refill,
|
||||
notification_buy,
|
||||
misc_faq,
|
||||
misc_support,
|
||||
misc_bot,
|
||||
misc_hosting_text,
|
||||
misc_token_telegraph,
|
||||
misc_discord_webhook_url,
|
||||
misc_discord_webhook_name,
|
||||
misc_hide_category,
|
||||
misc_hide_position,
|
||||
misc_profit_day,
|
||||
misc_profit_week,
|
||||
misc_profit_month
|
||||
)
|
||||
SELECT
|
||||
1,
|
||||
{_copy_expr("storage_settings", "status_work", "True")},
|
||||
{_copy_expr("storage_settings", "status_refill", "False")},
|
||||
{_copy_expr("storage_settings", "status_buy", "False")},
|
||||
{_copy_expr("storage_settings", "notification_refill", "True")},
|
||||
{_copy_expr("storage_settings", "notification_buy", "False")},
|
||||
{_copy_expr("storage_settings", "misc_faq", "None")},
|
||||
{_copy_expr("storage_settings", "misc_support", "None")},
|
||||
{_copy_expr("storage_settings", "misc_bot", "None")},
|
||||
{_copy_expr("storage_settings", "misc_hosting_text", "telegraph")},
|
||||
{_copy_expr("storage_settings", "misc_token_telegraph", "None")},
|
||||
{_copy_expr("storage_settings", "misc_discord_webhook_url", "None")},
|
||||
{_copy_expr("storage_settings", "misc_discord_webhook_name", "None")},
|
||||
{_copy_expr("storage_settings", "misc_hide_category", "False")},
|
||||
{_copy_expr("storage_settings", "misc_hide_position", "False")},
|
||||
{_copy_int_expr("storage_settings", "misc_profit_day")},
|
||||
{_copy_int_expr("storage_settings", "misc_profit_week")},
|
||||
{_copy_int_expr("storage_settings", "misc_profit_month")}
|
||||
FROM storage_settings
|
||||
LIMIT 1
|
||||
"""
|
||||
)
|
||||
)
|
||||
bind.execute(sa.text("DROP TABLE storage_settings"))
|
||||
bind.execute(sa.text("ALTER TABLE storage_settings_new RENAME TO storage_settings"))
|
||||
|
||||
bind.execute(
|
||||
sa.text(
|
||||
"""
|
||||
INSERT INTO storage_settings (id)
|
||||
SELECT 1
|
||||
WHERE NOT EXISTS (SELECT 1 FROM storage_settings WHERE id = 1)
|
||||
"""
|
||||
)
|
||||
)
|
||||
for column, default in (
|
||||
("status_work", "True"),
|
||||
("status_refill", "False"),
|
||||
("status_buy", "False"),
|
||||
("notification_refill", "True"),
|
||||
("notification_buy", "False"),
|
||||
("misc_hide_category", "False"),
|
||||
("misc_hide_position", "False"),
|
||||
):
|
||||
_normalize_boolean_text("storage_settings", column, default)
|
||||
|
||||
_create_index("ix_storage_settings_id", "storage_settings", "id", unique=True)
|
||||
|
||||
|
||||
# Создание или правка таблицы платежей
|
||||
def _ensure_payments() -> None:
|
||||
bind = op.get_bind()
|
||||
|
||||
if not _table_exists("storage_payments"):
|
||||
op.create_table(
|
||||
"storage_payments",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("cryptobot_token", sa.String(), nullable=False, server_default="None"),
|
||||
sa.Column("yoomoney_token", sa.String(), nullable=False, server_default="None"),
|
||||
sa.Column("status_cryptobot", sa.String(length=16), nullable=False, server_default="False"),
|
||||
sa.Column("status_yoomoney", sa.String(length=16), nullable=False, server_default="False"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
else:
|
||||
bind.execute(sa.text("DROP TABLE IF EXISTS storage_payments_new"))
|
||||
op.create_table(
|
||||
"storage_payments_new",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("cryptobot_token", sa.String(), nullable=False, server_default="None"),
|
||||
sa.Column("yoomoney_token", sa.String(), nullable=False, server_default="None"),
|
||||
sa.Column("status_cryptobot", sa.String(length=16), nullable=False, server_default="False"),
|
||||
sa.Column("status_yoomoney", sa.String(length=16), nullable=False, server_default="False"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
bind.execute(
|
||||
sa.text(
|
||||
f"""
|
||||
INSERT INTO storage_payments_new (
|
||||
id,
|
||||
cryptobot_token,
|
||||
yoomoney_token,
|
||||
status_cryptobot,
|
||||
status_yoomoney
|
||||
)
|
||||
SELECT
|
||||
1,
|
||||
{_copy_expr("storage_payments", "cryptobot_token", "None")},
|
||||
{_copy_expr("storage_payments", "yoomoney_token", "None")},
|
||||
{_copy_expr("storage_payments", "status_cryptobot", "False")},
|
||||
{_copy_expr("storage_payments", "status_yoomoney", "False")}
|
||||
FROM storage_payments
|
||||
LIMIT 1
|
||||
"""
|
||||
)
|
||||
)
|
||||
bind.execute(sa.text("DROP TABLE storage_payments"))
|
||||
bind.execute(sa.text("ALTER TABLE storage_payments_new RENAME TO storage_payments"))
|
||||
|
||||
bind.execute(
|
||||
sa.text(
|
||||
"""
|
||||
INSERT INTO storage_payments (id)
|
||||
SELECT 1
|
||||
WHERE NOT EXISTS (SELECT 1 FROM storage_payments WHERE id = 1)
|
||||
"""
|
||||
)
|
||||
)
|
||||
_normalize_boolean_text("storage_payments", "status_cryptobot", "False")
|
||||
_normalize_boolean_text("storage_payments", "status_yoomoney", "False")
|
||||
_create_index("ix_storage_payments_id", "storage_payments", "id", unique=True)
|
||||
|
||||
|
||||
# Создание или правка таблицы категорий
|
||||
def _ensure_category() -> None:
|
||||
if not _table_exists("storage_category"):
|
||||
op.create_table(
|
||||
"storage_category",
|
||||
sa.Column("increment", sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column("category_id", sa.BigInteger(), nullable=False),
|
||||
sa.Column("category_name", sa.String(length=255), nullable=False),
|
||||
sa.Column("category_unix", sa.Integer(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("increment"),
|
||||
)
|
||||
else:
|
||||
_add_column("storage_category", "category_id", "INTEGER NOT NULL DEFAULT 0")
|
||||
_add_column("storage_category", "category_name", "TEXT NOT NULL DEFAULT ''")
|
||||
_add_column("storage_category", "category_unix", "INTEGER NOT NULL DEFAULT 0")
|
||||
|
||||
_create_index("ix_storage_category_category_id", "storage_category", "category_id")
|
||||
|
||||
|
||||
# Создание или правка таблицы позиций
|
||||
def _ensure_position() -> None:
|
||||
if not _table_exists("storage_position"):
|
||||
op.create_table(
|
||||
"storage_position",
|
||||
sa.Column("increment", sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column("category_id", sa.BigInteger(), nullable=False),
|
||||
sa.Column("position_id", sa.BigInteger(), nullable=False),
|
||||
sa.Column("position_name", sa.String(length=255), nullable=False),
|
||||
sa.Column("position_price", sa.Float(), nullable=False, server_default="0"),
|
||||
sa.Column("position_desc", sa.Text(), nullable=False, server_default="None"),
|
||||
sa.Column("position_photo", sa.Text(), nullable=False, server_default="None"),
|
||||
sa.Column("position_unix", sa.Integer(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("increment"),
|
||||
)
|
||||
else:
|
||||
_add_column("storage_position", "category_id", "INTEGER NOT NULL DEFAULT 0")
|
||||
_add_column("storage_position", "position_id", "INTEGER NOT NULL DEFAULT 0")
|
||||
_add_column("storage_position", "position_name", "TEXT NOT NULL DEFAULT ''")
|
||||
_add_column("storage_position", "position_price", "REAL NOT NULL DEFAULT 0")
|
||||
_add_column("storage_position", "position_desc", "TEXT NOT NULL DEFAULT 'None'")
|
||||
_add_column("storage_position", "position_photo", "TEXT NOT NULL DEFAULT 'None'")
|
||||
_add_column("storage_position", "position_unix", "INTEGER NOT NULL DEFAULT 0")
|
||||
|
||||
_create_index("ix_storage_position_category_id", "storage_position", "category_id")
|
||||
_create_index("ix_storage_position_position_id", "storage_position", "position_id")
|
||||
|
||||
|
||||
# Создание или правка таблицы товаров
|
||||
def _ensure_item() -> None:
|
||||
if not _table_exists("storage_item"):
|
||||
op.create_table(
|
||||
"storage_item",
|
||||
sa.Column("increment", sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column("user_id", sa.BigInteger(), nullable=False),
|
||||
sa.Column("category_id", sa.BigInteger(), nullable=False),
|
||||
sa.Column("position_id", sa.BigInteger(), nullable=False),
|
||||
sa.Column("item_id", sa.BigInteger(), nullable=False),
|
||||
sa.Column("item_unix", sa.Integer(), nullable=False),
|
||||
sa.Column("item_data", sa.Text(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("increment"),
|
||||
)
|
||||
else:
|
||||
_add_column("storage_item", "user_id", "INTEGER NOT NULL DEFAULT 0")
|
||||
_add_column("storage_item", "category_id", "INTEGER NOT NULL DEFAULT 0")
|
||||
_add_column("storage_item", "position_id", "INTEGER NOT NULL DEFAULT 0")
|
||||
_add_column("storage_item", "item_id", "INTEGER NOT NULL DEFAULT 0")
|
||||
_add_column("storage_item", "item_unix", "INTEGER NOT NULL DEFAULT 0")
|
||||
_add_column("storage_item", "item_data", "TEXT NOT NULL DEFAULT ''")
|
||||
|
||||
_create_index("ix_storage_item_user_id", "storage_item", "user_id")
|
||||
_create_index("ix_storage_item_category_id", "storage_item", "category_id")
|
||||
_create_index("ix_storage_item_position_id", "storage_item", "position_id")
|
||||
_create_index("ix_storage_item_item_id", "storage_item", "item_id")
|
||||
|
||||
|
||||
# Создание или правка таблицы покупок
|
||||
def _ensure_purchases() -> None:
|
||||
if not _table_exists("storage_purchases"):
|
||||
op.create_table(
|
||||
"storage_purchases",
|
||||
sa.Column("increment", sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column("user_id", sa.BigInteger(), nullable=False),
|
||||
sa.Column("user_balance_before", sa.Float(), nullable=False, server_default="0"),
|
||||
sa.Column("user_balance_after", sa.Float(), nullable=False, server_default="0"),
|
||||
sa.Column("purchase_receipt", sa.BigInteger(), nullable=False),
|
||||
sa.Column("purchase_data", sa.Text(), nullable=False),
|
||||
sa.Column("purchase_count", sa.Integer(), nullable=False, server_default="1"),
|
||||
sa.Column("purchase_price", sa.Float(), nullable=False, server_default="0"),
|
||||
sa.Column("purchase_price_one", sa.Float(), nullable=False, server_default="0"),
|
||||
sa.Column("purchase_position_id", sa.BigInteger(), nullable=False),
|
||||
sa.Column("purchase_position_name", sa.String(length=255), nullable=False),
|
||||
sa.Column("purchase_category_id", sa.BigInteger(), nullable=False),
|
||||
sa.Column("purchase_category_name", sa.String(length=255), nullable=False),
|
||||
sa.Column("purchase_unix", sa.Integer(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("increment"),
|
||||
)
|
||||
else:
|
||||
_add_column("storage_purchases", "user_id", "INTEGER NOT NULL DEFAULT 0")
|
||||
_add_column("storage_purchases", "user_balance_before", "REAL NOT NULL DEFAULT 0")
|
||||
_add_column("storage_purchases", "user_balance_after", "REAL NOT NULL DEFAULT 0")
|
||||
_add_column("storage_purchases", "purchase_receipt", "INTEGER NOT NULL DEFAULT 0")
|
||||
_add_column("storage_purchases", "purchase_data", "TEXT NOT NULL DEFAULT ''")
|
||||
_add_column("storage_purchases", "purchase_count", "INTEGER NOT NULL DEFAULT 1")
|
||||
_add_column("storage_purchases", "purchase_price", "REAL NOT NULL DEFAULT 0")
|
||||
_add_column("storage_purchases", "purchase_price_one", "REAL NOT NULL DEFAULT 0")
|
||||
_add_column("storage_purchases", "purchase_position_id", "INTEGER NOT NULL DEFAULT 0")
|
||||
_add_column("storage_purchases", "purchase_position_name", "TEXT NOT NULL DEFAULT ''")
|
||||
_add_column("storage_purchases", "purchase_category_id", "INTEGER NOT NULL DEFAULT 0")
|
||||
_add_column("storage_purchases", "purchase_category_name", "TEXT NOT NULL DEFAULT ''")
|
||||
_add_column("storage_purchases", "purchase_unix", "INTEGER NOT NULL DEFAULT 0")
|
||||
|
||||
_create_index("ix_storage_purchases_user_id", "storage_purchases", "user_id")
|
||||
_create_index("ix_storage_purchases_purchase_receipt", "storage_purchases", "purchase_receipt")
|
||||
_create_index("ix_storage_purchases_purchase_position_id", "storage_purchases", "purchase_position_id")
|
||||
_create_index("ix_storage_purchases_purchase_category_id", "storage_purchases", "purchase_category_id")
|
||||
|
||||
|
||||
# Создание или правка таблицы пополнений
|
||||
def _ensure_refill() -> None:
|
||||
if not _table_exists("storage_refill"):
|
||||
op.create_table(
|
||||
"storage_refill",
|
||||
sa.Column("increment", sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column("user_id", sa.BigInteger(), nullable=False),
|
||||
sa.Column("refill_receipt", sa.BigInteger(), nullable=False),
|
||||
sa.Column("refill_comment", sa.String(length=255), nullable=False, server_default=""),
|
||||
sa.Column("refill_amount", sa.Float(), nullable=False, server_default="0"),
|
||||
sa.Column("refill_method", sa.String(length=64), nullable=False),
|
||||
sa.Column("refill_unix", sa.Integer(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("increment"),
|
||||
)
|
||||
else:
|
||||
_add_column("storage_refill", "user_id", "INTEGER NOT NULL DEFAULT 0")
|
||||
_add_column("storage_refill", "refill_receipt", "INTEGER NOT NULL DEFAULT 0")
|
||||
_add_column("storage_refill", "refill_comment", "TEXT NOT NULL DEFAULT ''")
|
||||
_add_column("storage_refill", "refill_amount", "REAL NOT NULL DEFAULT 0")
|
||||
_add_column("storage_refill", "refill_method", "TEXT NOT NULL DEFAULT ''")
|
||||
_add_column("storage_refill", "refill_unix", "INTEGER NOT NULL DEFAULT 0")
|
||||
|
||||
_create_index("ix_storage_refill_user_id", "storage_refill", "user_id")
|
||||
_create_index("ix_storage_refill_refill_receipt", "storage_refill", "refill_receipt")
|
||||
|
||||
|
||||
# Применение начальной схемы магазина
|
||||
def upgrade() -> None:
|
||||
_ensure_users()
|
||||
_ensure_settings()
|
||||
_ensure_payments()
|
||||
_ensure_category()
|
||||
_ensure_position()
|
||||
_ensure_item()
|
||||
_ensure_purchases()
|
||||
_ensure_refill()
|
||||
|
||||
|
||||
# Откат начальной схемы магазина
|
||||
def downgrade() -> None:
|
||||
op.drop_table("storage_refill")
|
||||
op.drop_table("storage_purchases")
|
||||
op.drop_table("storage_item")
|
||||
op.drop_table("storage_position")
|
||||
op.drop_table("storage_category")
|
||||
op.drop_table("storage_payments")
|
||||
op.drop_table("storage_settings")
|
||||
op.drop_table("storage_users")
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
"""add column 'status_stars' into storage_payments
|
||||
|
||||
Revision ID: 492769059249
|
||||
Revises: 0001_shop_schema
|
||||
Create Date: 2026-05-30 19:15:00.214301+03:00
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision: str = '492769059249'
|
||||
down_revision: Union[str, None] = '0001_shop_schema'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('storage_payments', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('status_stars', sa.String(length=16), server_default=sa.text("'False'"), nullable=False))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('storage_payments', schema=None) as batch_op:
|
||||
batch_op.drop_column('status_stars')
|
||||
|
||||
# ### end Alembic commands ###
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
"""add column 'stars_course' into storage_payment
|
||||
|
||||
Revision ID: 700275128e27
|
||||
Revises: 492769059249
|
||||
Create Date: 2026-05-30 19:28:42.490458+03:00
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision: str = '700275128e27'
|
||||
down_revision: Union[str, None] = '492769059249'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('storage_payments', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('stars_course', sa.Float(), server_default=sa.text('(1.5)'), nullable=False))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('storage_payments', schema=None) as batch_op:
|
||||
batch_op.drop_column('stars_course')
|
||||
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,32 @@
|
||||
"""add column 'misc_method_prod' into 'storage_settings'
|
||||
|
||||
Revision ID: 36380377b0e6
|
||||
Revises: 700275128e27
|
||||
Create Date: 2026-06-01 19:34:35.858117+03:00
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision: str = '36380377b0e6'
|
||||
down_revision: Union[str, None] = '700275128e27'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('storage_settings', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('misc_method_prod', sa.String(length=16), server_default=sa.text("'skip'"), nullable=False))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('storage_settings', schema=None) as batch_op:
|
||||
batch_op.drop_column('misc_method_prod')
|
||||
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,31 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=68"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "djimbo-autoshop"
|
||||
version = "0.1.0"
|
||||
description = "Telegram autoshop bot on aiogram 3 by Djimbo"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"APScheduler>=3.11.2,<4.0",
|
||||
"aiogram>=3.28,<4.0",
|
||||
"aiofiles>=25.1,<26.0",
|
||||
"aiohttp>=3.13.5,<4.0",
|
||||
"aiosqlite>=0.20,<1.0",
|
||||
"alembic>=1.13,<2.0",
|
||||
"cachetools>=7.1,<8.0",
|
||||
"certifi>=2025.8,<2027.0",
|
||||
"colorama>=0.4,<1.0",
|
||||
"colorlog>=6.10,<7.0",
|
||||
"pydantic>=2.13,<3.0",
|
||||
"pydantic-settings>=2.6,<3.0",
|
||||
"python-dotenv>=1.2,<2.0",
|
||||
"pytz>=2026.2,<2027.0",
|
||||
"SQLAlchemy>=2.0,<3.0",
|
||||
"ujson>=5.12,<6.0",
|
||||
]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
include = ["tgbot*"]
|
||||
exclude = ["migrations*"]
|
||||
@@ -0,0 +1,16 @@
|
||||
APScheduler==3.11.2
|
||||
aiogram==3.28.2
|
||||
aiofiles==25.1.0
|
||||
aiohttp==3.13.5
|
||||
aiosqlite==0.22.1
|
||||
alembic==1.18.4
|
||||
cachetools==7.1.4
|
||||
certifi==2026.5.20
|
||||
colorama==0.4.6
|
||||
colorlog==6.10.1
|
||||
pydantic==2.13.4
|
||||
pydantic-settings==2.14.1
|
||||
python-dotenv==1.2.2
|
||||
pytz==2026.2
|
||||
SQLAlchemy==2.0.50
|
||||
ujson==5.12.1
|
||||
@@ -0,0 +1,168 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
from pydantic import Field, field_validator
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
from pytz import UnknownTimeZoneError, timezone
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parents[2]
|
||||
ENV_PATH = BASE_DIR / ".env"
|
||||
|
||||
|
||||
# Все настройки из окружения и локального .env
|
||||
class Settings(BaseSettings):
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=ENV_PATH,
|
||||
env_file_encoding="utf-8",
|
||||
extra="ignore",
|
||||
populate_by_name=True,
|
||||
enable_decoding=False,
|
||||
)
|
||||
|
||||
bot_token: str = Field(default="", validation_alias="BOT_TOKEN")
|
||||
admin_ids: List[int] = Field(default_factory=list, validation_alias="BOT_ADMIN_IDS")
|
||||
database_export: bool = Field(
|
||||
default=True,
|
||||
validation_alias="BOT_DATABASE_EXPORT",
|
||||
)
|
||||
status_notification: bool = Field(default=True, validation_alias="BOT_STATUS_NOTIFICATION")
|
||||
timezone: str = Field(default="Europe/Moscow", validation_alias="BOT_TIMEZONE")
|
||||
database_path: str = Field(default="tgbot/data/database.db", validation_alias="PATH_DATABASE")
|
||||
logs_path: str = Field(default="tgbot/data/logs.log", validation_alias="PATH_LOGS")
|
||||
user_cache_ttl: int = Field(default=300, ge=0, validation_alias="BOT_USER_CACHE_TTL")
|
||||
throttle_rate: float = Field(default=0.5, ge=0, validation_alias="BOT_THROTTLE_RATE")
|
||||
yoomoney_client_id: str = Field(default="", validation_alias="YOOMONEY_CLIENT_ID")
|
||||
|
||||
# Очистка строковых значений из окружения
|
||||
@field_validator("bot_token", "timezone", "database_path", "logs_path", "yoomoney_client_id", mode="before")
|
||||
@classmethod
|
||||
def _strip_string(cls, value: object) -> str:
|
||||
return str(value or "").strip()
|
||||
|
||||
# Разбор списка телеграм ID админов
|
||||
@field_validator("admin_ids", mode="before")
|
||||
@classmethod
|
||||
def _parse_admin_ids(cls, value: object) -> List[int]:
|
||||
if value is None or value == "":
|
||||
return []
|
||||
|
||||
if isinstance(value, int):
|
||||
values = [value]
|
||||
elif isinstance(value, str):
|
||||
values = [admin_id for admin_id in value.replace(" ", "").split(",") if admin_id]
|
||||
elif isinstance(value, (list, tuple, set)):
|
||||
values = list(value)
|
||||
else:
|
||||
raise ValueError("BOT_ADMIN_IDS должен быть числом или списком чисел через запятую")
|
||||
|
||||
admin_ids = []
|
||||
|
||||
for admin_id in values:
|
||||
try:
|
||||
parsed_id = int(admin_id)
|
||||
except (TypeError, ValueError) as error:
|
||||
raise ValueError("BOT_ADMIN_IDS должен содержать только Телеграм ID через запятую") from error
|
||||
|
||||
if parsed_id <= 0:
|
||||
raise ValueError("BOT_ADMIN_IDS должен содержать Телеграм ID больше нуля")
|
||||
|
||||
admin_ids.append(parsed_id)
|
||||
|
||||
return admin_ids
|
||||
|
||||
# Проверка корректности timezone
|
||||
@field_validator("timezone")
|
||||
@classmethod
|
||||
def _validate_timezone(cls, value: str) -> str:
|
||||
try:
|
||||
timezone(value)
|
||||
except UnknownTimeZoneError as error:
|
||||
raise ValueError("BOT_TIMEZONE должен быть корректной временной зоной, например Europe/Moscow") from error
|
||||
|
||||
return value
|
||||
|
||||
# Проверка непустых путей файлов
|
||||
@field_validator("database_path", "logs_path")
|
||||
@classmethod
|
||||
def _validate_path(cls, value: str) -> str:
|
||||
if not value:
|
||||
raise ValueError("Путь к файлу не должен быть пустым")
|
||||
|
||||
return value
|
||||
|
||||
# Получение копии списка админов
|
||||
@property
|
||||
def admins(self) -> List[int]:
|
||||
return list(self.admin_ids)
|
||||
|
||||
# Преобразование относительного пути в абсолютный
|
||||
@staticmethod
|
||||
def resolve_path(path_value: str) -> Path:
|
||||
path = Path(path_value)
|
||||
|
||||
if path.is_absolute():
|
||||
return path
|
||||
|
||||
return BASE_DIR / path
|
||||
|
||||
|
||||
# Кэширование настроек
|
||||
@lru_cache(maxsize=1)
|
||||
def get_settings() -> Settings:
|
||||
return Settings()
|
||||
|
||||
|
||||
settings = get_settings()
|
||||
|
||||
# Константы для старых импортов
|
||||
BOT_TOKEN = settings.bot_token.replace(" ", "")
|
||||
PATH_DATABASE = str(Settings.resolve_path(settings.database_path))
|
||||
PATH_LOGS = str(Settings.resolve_path(settings.logs_path))
|
||||
BOT_STATUS_NOTIFICATION = settings.status_notification
|
||||
BOT_DATABASE_EXPORT = settings.database_export
|
||||
BOT_TIMEZONE = settings.timezone
|
||||
BOT_USER_CACHE_TTL = settings.user_cache_ttl
|
||||
BOT_THROTTLE_RATE = settings.throttle_rate
|
||||
YOOMONEY_CLIENT_ID = settings.yoomoney_client_id
|
||||
BOT_SCHEDULER = AsyncIOScheduler(timezone=BOT_TIMEZONE)
|
||||
BOT_VERSION = 4.2
|
||||
|
||||
|
||||
# Получение администраторов бота
|
||||
def get_admins() -> List[int]:
|
||||
return settings.admins
|
||||
|
||||
|
||||
# Проверка настроек, которые нужны именно для запуска бота
|
||||
def validate_bot_config() -> None:
|
||||
if not BOT_TOKEN:
|
||||
raise RuntimeError("В .env не заполнен BOT_TOKEN")
|
||||
|
||||
|
||||
# Получение описания
|
||||
def get_text_desc() -> str:
|
||||
from tgbot.utils.const_functions import ded
|
||||
|
||||
return ded(f"""
|
||||
<b>♻️ Версия бота: <code>{BOT_VERSION}</code>
|
||||
👑 Разработчик бота - @djimbox
|
||||
🍩 Донат разработчику: <a href='https://t.me/send?start=IV8bjPKhYkYJ'>Click me</a>
|
||||
🤖 Новости и обновления: <a href='https://t.me/DJIMBO_SHOP'>Click me</a>
|
||||
🔗 Тема с ботом [LOLZ]: <a href='https://lolz.guru/threads/1888814'>Click me</a></b>
|
||||
""").strip()
|
||||
|
||||
|
||||
# Получение варнинг-текста
|
||||
def get_text_warning() -> str:
|
||||
from tgbot.utils.const_functions import ded
|
||||
|
||||
return ded(f"""
|
||||
❗️ Если вы видите данное сообщение, значит <u>Администратор</u> бота не изменил текст установленный
|
||||
по умолчанию. Скорее всего данный бот занимается <u>скамом, обманом</u> или другими схожими действиями.
|
||||
|
||||
❗️ Если вас обманули, не надо писать <b><u>РАЗРАБОТЧИКУ</u></b> бота, он ничем не сможет помочь. Бот является
|
||||
открытым и распространяется публично. Абсолютно любой желающий мог его запустить.
|
||||
""")
|
||||
@@ -0,0 +1,20 @@
|
||||
from .db_category import CategoryModel, Categoryx
|
||||
from .db_item import ItemModel, Itemx
|
||||
from .db_payments import PaymentsModel, Paymentsx
|
||||
from .db_position import PositionModel, Positionx
|
||||
from .db_purchases import PurchaseModel, Purchasesx
|
||||
from .db_refill import RefillModel, Refillx
|
||||
from .db_settings import SettingsModel, Settingsx
|
||||
from .db_users import UserModel, UsersRepository, Userx
|
||||
from .entities import Category, Item, Payments, Position, Purchase, Refill, Settings, User
|
||||
|
||||
ModelCategory = Category
|
||||
ModelItem = Item
|
||||
ModelPayments = Payments
|
||||
ModelPosition = Position
|
||||
ModelPurchases = Purchase
|
||||
ModelRefill = Refill
|
||||
ModelSettings = Settings
|
||||
ModelUser = User
|
||||
ModelUsers = User
|
||||
SettingsRepository = Settingsx
|
||||
@@ -0,0 +1,47 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
from collections.abc import AsyncIterator
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
|
||||
from sqlalchemy import event
|
||||
from sqlalchemy.ext.asyncio import AsyncAttrs, AsyncSession, async_sessionmaker, create_async_engine
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
|
||||
from tgbot.data.config import PATH_DATABASE
|
||||
|
||||
database_path = Path(PATH_DATABASE)
|
||||
database_url = f"sqlite+aiosqlite:///{database_path.as_posix()}"
|
||||
|
||||
engine = create_async_engine(database_url, echo=False)
|
||||
session_factory = async_sessionmaker(engine, expire_on_commit=False)
|
||||
|
||||
|
||||
# Общая база для всех SQLAlchemy-моделей
|
||||
class Base(AsyncAttrs, DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
# Включение SQLite-настроек на каждом соединении
|
||||
@event.listens_for(engine.sync_engine, "connect")
|
||||
def _configure_sqlite(dbapi_connection, connection_record) -> None:
|
||||
cursor = dbapi_connection.cursor()
|
||||
cursor.execute("PRAGMA foreign_keys=ON")
|
||||
cursor.execute("PRAGMA busy_timeout=5000")
|
||||
cursor.close()
|
||||
|
||||
|
||||
# Открытие сессии с автокоммитом или откатом
|
||||
@asynccontextmanager
|
||||
async def session_scope() -> AsyncIterator[AsyncSession]:
|
||||
async with session_factory() as session:
|
||||
try:
|
||||
yield session
|
||||
await session.commit()
|
||||
except Exception:
|
||||
await session.rollback()
|
||||
raise
|
||||
|
||||
|
||||
# Закрытие пула соединений БД
|
||||
async def close_database() -> None:
|
||||
await engine.dispose()
|
||||
@@ -0,0 +1,47 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
from typing import Any, Dict, Optional, Union
|
||||
|
||||
from sqlalchemy import BigInteger, Integer, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from tgbot.database.core import Base
|
||||
from tgbot.database.entities import Category
|
||||
from tgbot.database.repository import BaseRepository
|
||||
from tgbot.utils.const_functions import get_unix
|
||||
|
||||
|
||||
class CategoryModel(Base):
|
||||
__tablename__ = "storage_category"
|
||||
|
||||
increment: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
category_id: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True)
|
||||
category_name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
category_unix: Mapped[int] = mapped_column(Integer, nullable=False, default=get_unix)
|
||||
|
||||
|
||||
ModelBase = Category
|
||||
BaseModel = Category
|
||||
|
||||
|
||||
class Categoryx(BaseRepository[CategoryModel, Category]):
|
||||
# Подключение модели категорий
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.table_model = CategoryModel
|
||||
self.entity_model = Category
|
||||
self.storage_name = CategoryModel.__tablename__
|
||||
|
||||
# Добавление категории товара
|
||||
async def add(self, category_id: int, category_name: str) -> Category:
|
||||
return await self._insert(
|
||||
category_id=category_id,
|
||||
category_name=category_name,
|
||||
category_unix=get_unix(),
|
||||
)
|
||||
|
||||
# Обновление категории по ID или фильтру
|
||||
async def update(self, where: Optional[Union[Dict[str, Any], int]] = None, **kwargs) -> int:
|
||||
if isinstance(where, int):
|
||||
where = {"category_id": where}
|
||||
|
||||
return await self._update(where=where, **kwargs)
|
||||
@@ -0,0 +1,73 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
from sqlalchemy import BigInteger, Integer, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from tgbot.database.core import Base, session_scope
|
||||
from tgbot.database.entities import Item
|
||||
from tgbot.database.repository import BaseRepository
|
||||
from tgbot.utils.const_functions import clear_list, gen_id, get_unix
|
||||
|
||||
|
||||
class ItemModel(Base):
|
||||
__tablename__ = "storage_item"
|
||||
|
||||
increment: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True)
|
||||
category_id: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True)
|
||||
position_id: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True)
|
||||
item_id: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True)
|
||||
item_unix: Mapped[int] = mapped_column(Integer, nullable=False, default=get_unix)
|
||||
item_data: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
|
||||
|
||||
ModelBase = Item
|
||||
BaseModel = Item
|
||||
|
||||
|
||||
class Itemx(BaseRepository[ItemModel, Item]):
|
||||
# Подключение модели товаров
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.table_model = ItemModel
|
||||
self.entity_model = Item
|
||||
self.storage_name = ItemModel.__tablename__
|
||||
|
||||
# Добавление товаров пачкой
|
||||
async def add(
|
||||
self,
|
||||
user_id: int,
|
||||
category_id: int,
|
||||
position_id: int,
|
||||
item_datas: List[str],
|
||||
) -> Optional[List[Item]]:
|
||||
item_unix = get_unix()
|
||||
item_datas = clear_list(item_datas)
|
||||
|
||||
if len(item_datas) == 0:
|
||||
return None
|
||||
|
||||
rows = [
|
||||
ItemModel(
|
||||
user_id=user_id,
|
||||
category_id=category_id,
|
||||
position_id=position_id,
|
||||
item_id=gen_id(17),
|
||||
item_unix=item_unix,
|
||||
item_data=item_data.strip(),
|
||||
)
|
||||
for item_data in item_datas
|
||||
]
|
||||
|
||||
async with session_scope() as session:
|
||||
session.add_all(rows)
|
||||
|
||||
return [self._to_entity(row) for row in rows]
|
||||
|
||||
# Обновление товара по ID или фильтру
|
||||
async def update(self, where: Optional[Union[Dict[str, Any], int]] = None, **kwargs) -> int:
|
||||
if isinstance(where, int):
|
||||
where = {"item_id": where}
|
||||
|
||||
return await self._update(where=where, **kwargs)
|
||||
@@ -0,0 +1,64 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from sqlalchemy import Integer, String, Float
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from tgbot.database.core import Base
|
||||
from tgbot.database.entities import Payments
|
||||
from tgbot.database.repository import BaseRepository
|
||||
|
||||
|
||||
class PaymentsModel(Base):
|
||||
__tablename__ = "storage_payments"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, default=1)
|
||||
cryptobot_token: Mapped[str] = mapped_column(String, nullable=False, default="None")
|
||||
yoomoney_token: Mapped[str] = mapped_column(String, nullable=False, default="None")
|
||||
stars_course: Mapped[float] = mapped_column(Float, nullable=False, default=1.5)
|
||||
status_cryptobot: Mapped[str] = mapped_column(String(16), nullable=False, default="False")
|
||||
status_yoomoney: Mapped[str] = mapped_column(String(16), nullable=False, default="False")
|
||||
status_stars: Mapped[str] = mapped_column(String(16), nullable=False, default="False")
|
||||
|
||||
|
||||
ModelBase = Payments
|
||||
BaseModel = Payments
|
||||
|
||||
|
||||
class Paymentsx(BaseRepository[PaymentsModel, Payments]):
|
||||
# Подключение модели платежных настроек
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.table_model = PaymentsModel
|
||||
self.entity_model = Payments
|
||||
self.storage_name = PaymentsModel.__tablename__
|
||||
|
||||
# Создание дефолтной строки платежей
|
||||
async def ensure_default(self) -> None:
|
||||
payments = await BaseRepository.get(self, id=1)
|
||||
|
||||
if payments is None:
|
||||
await self._insert(id=1)
|
||||
|
||||
# Получение платежных настроек
|
||||
async def get(self, **kwargs) -> Payments:
|
||||
if not kwargs:
|
||||
kwargs = {"id": 1}
|
||||
|
||||
payments = await BaseRepository.get(self, **kwargs)
|
||||
|
||||
if payments is None and kwargs == {"id": 1}:
|
||||
await self.ensure_default()
|
||||
payments = await BaseRepository.get(self, id=1)
|
||||
|
||||
if payments is None:
|
||||
raise RuntimeError("Настройки платежных систем по умолчанию не сохранились")
|
||||
|
||||
return payments
|
||||
|
||||
# Обновление платежных настроек
|
||||
async def update(self, where: Optional[Dict[str, Any]] = None, **kwargs) -> int:
|
||||
if where is None:
|
||||
where = {"id": 1}
|
||||
|
||||
return await self._update(where=where, **kwargs)
|
||||
@@ -0,0 +1,79 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
from typing import Any, Dict, Optional, Union
|
||||
|
||||
from sqlalchemy import BigInteger, Float, Integer, String, Text, func, select
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from tgbot.database.core import Base, session_scope
|
||||
from tgbot.database.entities import Position
|
||||
from tgbot.database.repository import BaseRepository
|
||||
from tgbot.utils.const_functions import get_unix
|
||||
|
||||
|
||||
class PositionModel(Base):
|
||||
__tablename__ = "storage_position"
|
||||
|
||||
increment: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
category_id: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True)
|
||||
position_id: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True)
|
||||
position_name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
position_price: Mapped[float] = mapped_column(Float, nullable=False, default=0)
|
||||
position_desc: Mapped[str] = mapped_column(Text, nullable=False, default="None")
|
||||
position_photo: Mapped[str] = mapped_column(Text, nullable=False, default="None")
|
||||
position_unix: Mapped[int] = mapped_column(Integer, nullable=False, default=get_unix)
|
||||
|
||||
|
||||
ModelBase = Position
|
||||
BaseModel = Position
|
||||
|
||||
|
||||
class Positionx(BaseRepository[PositionModel, Position]):
|
||||
# Подключение модели позиций
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.table_model = PositionModel
|
||||
self.entity_model = Position
|
||||
self.storage_name = PositionModel.__tablename__
|
||||
|
||||
# Добавление позиции товара
|
||||
async def add(
|
||||
self,
|
||||
category_id: int,
|
||||
position_id: int,
|
||||
position_name: str,
|
||||
position_price: float,
|
||||
position_desc: str,
|
||||
position_photo: str,
|
||||
) -> Position:
|
||||
return await self._insert(
|
||||
category_id=category_id,
|
||||
position_id=position_id,
|
||||
position_name=position_name,
|
||||
position_price=position_price,
|
||||
position_desc=position_desc,
|
||||
position_photo=position_photo,
|
||||
position_unix=get_unix(),
|
||||
)
|
||||
|
||||
# Обновление позиции по ID или фильтру
|
||||
async def update(self, where: Optional[Union[Dict[str, Any], int]] = None, **kwargs) -> int:
|
||||
if isinstance(where, int):
|
||||
where = {"position_id": where}
|
||||
|
||||
return await self._update(where=where, **kwargs)
|
||||
|
||||
# Получение остатков товаров по позициям
|
||||
@staticmethod
|
||||
async def item_counts() -> Dict[int, int]:
|
||||
from tgbot.database.db_item import ItemModel
|
||||
|
||||
item_table = ItemModel.__table__
|
||||
statement = (
|
||||
select(item_table.c.position_id, func.count(item_table.c.increment).label("item_count"))
|
||||
.group_by(item_table.c.position_id)
|
||||
)
|
||||
|
||||
async with session_scope() as session:
|
||||
rows = await session.execute(statement)
|
||||
|
||||
return {int(position_id): int(item_count) for position_id, item_count in rows.all()}
|
||||
@@ -0,0 +1,331 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
from sqlalchemy import BigInteger, Float, Integer, String, Text, text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from tgbot.database.core import Base, session_factory
|
||||
from tgbot.database.entities import Purchase
|
||||
from tgbot.database.repository import BaseRepository
|
||||
from tgbot.utils.const_functions import gen_id, get_unix
|
||||
|
||||
TELEGRAM_LIMIT = 4000
|
||||
|
||||
|
||||
@dataclass
|
||||
class PurchaseResult:
|
||||
receipt: int
|
||||
items: List[List[str]]
|
||||
purchase_count: int
|
||||
purchase_unix: int
|
||||
purchase_price: float
|
||||
new_balance: float
|
||||
position_name: str
|
||||
|
||||
|
||||
class PurchaseModel(Base):
|
||||
__tablename__ = "storage_purchases"
|
||||
|
||||
increment: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True)
|
||||
user_balance_before: Mapped[float] = mapped_column(Float, nullable=False, default=0)
|
||||
user_balance_after: Mapped[float] = mapped_column(Float, nullable=False, default=0)
|
||||
purchase_receipt: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True)
|
||||
purchase_data: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
purchase_count: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
|
||||
purchase_price: Mapped[float] = mapped_column(Float, nullable=False, default=0)
|
||||
purchase_price_one: Mapped[float] = mapped_column(Float, nullable=False, default=0)
|
||||
purchase_position_id: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True)
|
||||
purchase_position_name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
purchase_category_id: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True)
|
||||
purchase_category_name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
purchase_unix: Mapped[int] = mapped_column(Integer, nullable=False, default=get_unix)
|
||||
|
||||
|
||||
ModelBase = Purchase
|
||||
BaseModel = Purchase
|
||||
|
||||
|
||||
class Purchasesx(BaseRepository[PurchaseModel, Purchase]):
|
||||
# Подключение модели покупок
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.table_model = PurchaseModel
|
||||
self.entity_model = Purchase
|
||||
self.storage_name = PurchaseModel.__tablename__
|
||||
|
||||
# Добавление записи покупки
|
||||
async def add(
|
||||
self,
|
||||
user_id: int,
|
||||
user_balance_before: float,
|
||||
user_balance_after: float,
|
||||
purchase_receipt: int,
|
||||
purchase_data: str,
|
||||
purchase_count: int,
|
||||
purchase_price: float,
|
||||
purchase_price_one: float,
|
||||
purchase_position_id: int,
|
||||
purchase_position_name: str,
|
||||
purchase_category_id: int,
|
||||
purchase_category_name: str,
|
||||
) -> Purchase:
|
||||
return await self._insert(
|
||||
user_id=user_id,
|
||||
user_balance_before=user_balance_before,
|
||||
user_balance_after=user_balance_after,
|
||||
purchase_receipt=purchase_receipt,
|
||||
purchase_data=purchase_data,
|
||||
purchase_count=purchase_count,
|
||||
purchase_price=purchase_price,
|
||||
purchase_price_one=purchase_price_one,
|
||||
purchase_position_id=purchase_position_id,
|
||||
purchase_position_name=purchase_position_name,
|
||||
purchase_category_id=purchase_category_id,
|
||||
purchase_category_name=purchase_category_name,
|
||||
purchase_unix=get_unix(),
|
||||
)
|
||||
|
||||
# Обновление покупки по чеку или фильтру
|
||||
async def update(self, where: Optional[Union[Dict[str, Any], int]] = None, **kwargs) -> int:
|
||||
if isinstance(where, int):
|
||||
where = {"purchase_receipt": where}
|
||||
|
||||
return await self._update(where=where, **kwargs)
|
||||
|
||||
# Атомарная покупка товаров
|
||||
@staticmethod
|
||||
async def buy(*, user_id: int, position_id: int, requested_count: int) -> Union[PurchaseResult, str]:
|
||||
async with session_factory() as session:
|
||||
try:
|
||||
await session.execute(text("BEGIN IMMEDIATE"))
|
||||
|
||||
user_result = await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT user_balance
|
||||
FROM storage_users
|
||||
WHERE user_id = :user_id
|
||||
"""
|
||||
),
|
||||
{"user_id": user_id},
|
||||
)
|
||||
get_user = user_result.mappings().first()
|
||||
|
||||
if get_user is None:
|
||||
await session.rollback()
|
||||
return "USER_NOT_FOUND"
|
||||
|
||||
position_result = await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT position_price, position_name, category_id
|
||||
FROM storage_position
|
||||
WHERE position_id = :position_id
|
||||
"""
|
||||
),
|
||||
{"position_id": position_id},
|
||||
)
|
||||
get_position = position_result.mappings().first()
|
||||
|
||||
if get_position is None:
|
||||
await session.rollback()
|
||||
return "POSITION_NOT_FOUND"
|
||||
|
||||
items_result = await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT increment, item_id, item_data
|
||||
FROM storage_item
|
||||
WHERE position_id = :position_id
|
||||
ORDER BY increment
|
||||
LIMIT :requested_count
|
||||
"""
|
||||
),
|
||||
{"position_id": position_id, "requested_count": requested_count},
|
||||
)
|
||||
items = items_result.mappings().all()
|
||||
|
||||
if len(items) < requested_count:
|
||||
await session.rollback()
|
||||
return "NOT_ENOUGH_ITEMS"
|
||||
|
||||
purchase_price = round(float(get_position["position_price"]) * requested_count, 2)
|
||||
|
||||
if float(get_user["user_balance"]) < purchase_price:
|
||||
await session.rollback()
|
||||
return "NOT_ENOUGH_BALANCE"
|
||||
|
||||
item_ids = [row["item_id"] for row in items]
|
||||
delete_params = {f"item_id_{index}": item_id for index, item_id in enumerate(item_ids)}
|
||||
delete_marks = ", ".join(f":item_id_{index}" for index in range(len(item_ids)))
|
||||
await session.execute(
|
||||
text(f"DELETE FROM storage_item WHERE item_id IN ({delete_marks})"),
|
||||
delete_params,
|
||||
)
|
||||
|
||||
new_balance = round(float(get_user["user_balance"]) - purchase_price, 2)
|
||||
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
UPDATE storage_users
|
||||
SET user_balance = :new_balance
|
||||
WHERE user_id = :user_id
|
||||
"""
|
||||
),
|
||||
{"new_balance": new_balance, "user_id": user_id},
|
||||
)
|
||||
|
||||
receipt = gen_id()
|
||||
purchase_data = "\n".join([row["item_data"] for row in items])
|
||||
purchase_unix = get_unix()
|
||||
|
||||
category_result = await session.execute(
|
||||
text("SELECT category_name FROM storage_category WHERE category_id = :category_id"),
|
||||
{"category_id": get_position["category_id"]},
|
||||
)
|
||||
category = category_result.mappings().first()
|
||||
category_name = category["category_name"] if category else "Unknown"
|
||||
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO storage_purchases (user_id,
|
||||
user_balance_before,
|
||||
user_balance_after,
|
||||
purchase_receipt,
|
||||
purchase_data,
|
||||
purchase_count,
|
||||
purchase_price,
|
||||
purchase_price_one,
|
||||
purchase_position_id,
|
||||
purchase_position_name,
|
||||
purchase_category_id,
|
||||
purchase_category_name,
|
||||
purchase_unix)
|
||||
VALUES (:user_id,
|
||||
:balance_before,
|
||||
:balance_after,
|
||||
:receipt,
|
||||
:purchase_data,
|
||||
:purchase_count,
|
||||
:purchase_price,
|
||||
:purchase_price_one,
|
||||
:position_id,
|
||||
:position_name,
|
||||
:category_id,
|
||||
:category_name,
|
||||
:purchase_unix)
|
||||
"""
|
||||
),
|
||||
{
|
||||
"user_id": user_id,
|
||||
"balance_before": float(get_user["user_balance"]),
|
||||
"balance_after": new_balance,
|
||||
"receipt": receipt,
|
||||
"purchase_data": purchase_data,
|
||||
"purchase_count": requested_count,
|
||||
"purchase_price": purchase_price,
|
||||
"purchase_price_one": float(get_position["position_price"]),
|
||||
"position_id": position_id,
|
||||
"position_name": get_position["position_name"],
|
||||
"category_id": get_position["category_id"],
|
||||
"category_name": category_name,
|
||||
"purchase_unix": purchase_unix,
|
||||
},
|
||||
)
|
||||
await session.commit()
|
||||
except Exception:
|
||||
await session.rollback()
|
||||
raise
|
||||
|
||||
purchase_items = [row["item_data"] for row in items]
|
||||
purchase_items_parts = chunk_items_by_len(purchase_items)
|
||||
|
||||
return PurchaseResult(
|
||||
receipt=receipt,
|
||||
items=purchase_items_parts,
|
||||
purchase_count=requested_count,
|
||||
purchase_unix=purchase_unix,
|
||||
purchase_price=purchase_price,
|
||||
position_name=get_position["position_name"],
|
||||
new_balance=new_balance,
|
||||
)
|
||||
|
||||
|
||||
# Разделение товаров на сообщения под лимит телеграма
|
||||
def chunk_items_by_len(items: List[str], limit: int = TELEGRAM_LIMIT, separator: str = "\n\n") -> List[List[str]]:
|
||||
chunks: List[List[str]] = []
|
||||
current_chunk: List[str] = []
|
||||
current_len = 0
|
||||
sep_len = len(separator)
|
||||
|
||||
for raw_item in items:
|
||||
item = str(raw_item)
|
||||
|
||||
if len(item) > limit:
|
||||
if current_chunk:
|
||||
chunks.append(current_chunk)
|
||||
current_chunk = []
|
||||
current_len = 0
|
||||
|
||||
for part in split_long_item(item, limit):
|
||||
chunks.append([part])
|
||||
|
||||
continue
|
||||
|
||||
add_len = len(item) if not current_chunk else sep_len + len(item)
|
||||
|
||||
if current_len + add_len <= limit:
|
||||
current_chunk.append(item)
|
||||
current_len += add_len
|
||||
else:
|
||||
chunks.append(current_chunk)
|
||||
current_chunk = [item]
|
||||
current_len = len(item)
|
||||
|
||||
if current_chunk:
|
||||
chunks.append(current_chunk)
|
||||
|
||||
return chunks
|
||||
|
||||
|
||||
# Разделение длинного товара без разрыва строк
|
||||
def split_long_item(item: str, limit: int = TELEGRAM_LIMIT) -> List[str]:
|
||||
chunks: List[str] = []
|
||||
current_lines: List[str] = []
|
||||
current_len = 0
|
||||
|
||||
for line in item.splitlines():
|
||||
line_len = len(line)
|
||||
add_len = line_len if not current_lines else 1 + line_len
|
||||
|
||||
if current_lines and current_len + add_len > limit:
|
||||
chunks.append("\n".join(current_lines))
|
||||
current_lines = []
|
||||
current_len = 0
|
||||
add_len = line_len
|
||||
|
||||
if line_len > limit:
|
||||
if current_lines:
|
||||
chunks.append("\n".join(current_lines))
|
||||
current_lines = []
|
||||
current_len = 0
|
||||
|
||||
start = 0
|
||||
|
||||
while start < line_len:
|
||||
chunks.append(line[start:start + limit])
|
||||
start += limit
|
||||
|
||||
continue
|
||||
|
||||
current_lines.append(line)
|
||||
current_len += add_len
|
||||
|
||||
if current_lines:
|
||||
chunks.append("\n".join(current_lines))
|
||||
|
||||
return chunks
|
||||
@@ -0,0 +1,146 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
from typing import Any, Dict, Optional, Union
|
||||
|
||||
from sqlalchemy import BigInteger, Float, Integer, String, text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from tgbot.database.core import Base, session_factory
|
||||
from tgbot.database.entities import Refill
|
||||
from tgbot.database.repository import BaseRepository
|
||||
from tgbot.utils.const_functions import get_unix
|
||||
|
||||
|
||||
class RefillModel(Base):
|
||||
__tablename__ = "storage_refill"
|
||||
|
||||
increment: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True)
|
||||
refill_receipt: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True)
|
||||
refill_comment: Mapped[str] = mapped_column(String(255), nullable=False, default="")
|
||||
refill_amount: Mapped[float] = mapped_column(Float, nullable=False, default=0)
|
||||
refill_method: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
refill_unix: Mapped[int] = mapped_column(Integer, nullable=False, default=get_unix)
|
||||
|
||||
|
||||
ModelBase = Refill
|
||||
BaseModel = Refill
|
||||
|
||||
|
||||
class Refillx(BaseRepository[RefillModel, Refill]):
|
||||
# Подключение модели пополнений
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.table_model = RefillModel
|
||||
self.entity_model = Refill
|
||||
self.storage_name = RefillModel.__tablename__
|
||||
|
||||
# Добавление записи пополнения
|
||||
async def add(
|
||||
self,
|
||||
user_id: int,
|
||||
refill_receipt: int,
|
||||
refill_comment: str,
|
||||
refill_amount: float,
|
||||
refill_method: str,
|
||||
) -> Refill:
|
||||
return await self._insert(
|
||||
user_id=user_id,
|
||||
refill_comment=refill_comment,
|
||||
refill_amount=refill_amount,
|
||||
refill_receipt=refill_receipt,
|
||||
refill_method=refill_method,
|
||||
refill_unix=get_unix(),
|
||||
)
|
||||
|
||||
# Обновление пополнения по чеку или фильтру
|
||||
async def update(self, where: Optional[Union[Dict[str, Any], int]] = None, **kwargs) -> int:
|
||||
if isinstance(where, int):
|
||||
where = {"refill_receipt": where}
|
||||
|
||||
return await self._update(where=where, **kwargs)
|
||||
|
||||
# Успешное зачисление пополнения
|
||||
@staticmethod
|
||||
async def success(
|
||||
user_id: int,
|
||||
pay_receipt: int,
|
||||
pay_comment: str,
|
||||
pay_amount: float,
|
||||
pay_method: str,
|
||||
) -> str:
|
||||
async with session_factory() as session:
|
||||
try:
|
||||
await session.execute(text("BEGIN IMMEDIATE"))
|
||||
|
||||
if pay_comment != "":
|
||||
duplicate_refill = await session.execute(
|
||||
text("SELECT increment FROM storage_refill WHERE refill_comment = :comment LIMIT 1"),
|
||||
{"comment": pay_comment},
|
||||
)
|
||||
else:
|
||||
duplicate_refill = await session.execute(
|
||||
text("SELECT increment FROM storage_refill WHERE refill_receipt = :receipt LIMIT 1"),
|
||||
{"receipt": pay_receipt},
|
||||
)
|
||||
|
||||
if duplicate_refill.mappings().first() is not None:
|
||||
await session.rollback()
|
||||
return "ALREADY"
|
||||
|
||||
user_result = await session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT user_balance, user_refill
|
||||
FROM storage_users
|
||||
WHERE user_id = :user_id
|
||||
"""
|
||||
),
|
||||
{"user_id": user_id},
|
||||
)
|
||||
get_user = user_result.mappings().first()
|
||||
|
||||
if get_user is None:
|
||||
await session.rollback()
|
||||
return "USER_NOT_FOUND"
|
||||
|
||||
new_balance = round(float(get_user["user_balance"]) + float(pay_amount), 2)
|
||||
new_refill = round(float(get_user["user_refill"]) + float(pay_amount), 2)
|
||||
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO storage_refill (user_id,
|
||||
refill_receipt,
|
||||
refill_comment,
|
||||
refill_amount,
|
||||
refill_method,
|
||||
refill_unix)
|
||||
VALUES (:user_id, :receipt, :comment, :amount, :method, :unix)
|
||||
"""
|
||||
),
|
||||
{
|
||||
"user_id": user_id,
|
||||
"receipt": pay_receipt,
|
||||
"comment": pay_comment,
|
||||
"amount": pay_amount,
|
||||
"method": pay_method,
|
||||
"unix": get_unix(),
|
||||
},
|
||||
)
|
||||
await session.execute(
|
||||
text(
|
||||
"""
|
||||
UPDATE storage_users
|
||||
SET user_balance = :balance,
|
||||
user_refill = :refill
|
||||
WHERE user_id = :user_id
|
||||
"""
|
||||
),
|
||||
{"balance": new_balance, "refill": new_refill, "user_id": user_id},
|
||||
)
|
||||
await session.commit()
|
||||
except Exception:
|
||||
await session.rollback()
|
||||
raise
|
||||
|
||||
return "ok"
|
||||
@@ -0,0 +1,76 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from sqlalchemy import Integer, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from tgbot.database.core import Base
|
||||
from tgbot.database.entities import Settings
|
||||
from tgbot.database.repository import BaseRepository
|
||||
|
||||
|
||||
class SettingsModel(Base):
|
||||
__tablename__ = "storage_settings"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, default=1)
|
||||
status_work: Mapped[str] = mapped_column(String(16), nullable=False, default="True")
|
||||
status_refill: Mapped[str] = mapped_column(String(16), nullable=False, default="False")
|
||||
status_buy: Mapped[str] = mapped_column(String(16), nullable=False, default="False")
|
||||
notification_refill: Mapped[str] = mapped_column(String(16), nullable=False, default="True")
|
||||
notification_buy: Mapped[str] = mapped_column(String(16), nullable=False, default="False")
|
||||
misc_faq: Mapped[str] = mapped_column(String, nullable=False, default="None")
|
||||
misc_support: Mapped[str] = mapped_column(String, nullable=False, default="None")
|
||||
misc_bot: Mapped[str] = mapped_column(String(255), nullable=False, default="None")
|
||||
misc_hosting_text: Mapped[str] = mapped_column(String(64), nullable=False, default="telegraph")
|
||||
misc_token_telegraph: Mapped[str] = mapped_column(String, nullable=False, default="None")
|
||||
misc_discord_webhook_url: Mapped[str] = mapped_column(String, nullable=False, default="None")
|
||||
misc_discord_webhook_name: Mapped[str] = mapped_column(String(255), nullable=False, default="None")
|
||||
misc_hide_category: Mapped[str] = mapped_column(String(16), nullable=False, default="False")
|
||||
misc_hide_position: Mapped[str] = mapped_column(String(16), nullable=False, default="False")
|
||||
misc_method_prod: Mapped[str] = mapped_column(String(16), nullable=False, default="skip")
|
||||
misc_profit_day: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
misc_profit_week: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
misc_profit_month: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||||
|
||||
|
||||
ModelBase = Settings
|
||||
BaseModel = Settings
|
||||
|
||||
|
||||
class Settingsx(BaseRepository[SettingsModel, Settings]):
|
||||
# Подключение модели настроек
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.table_model = SettingsModel
|
||||
self.entity_model = Settings
|
||||
self.storage_name = SettingsModel.__tablename__
|
||||
|
||||
# Создание дефолтной строки настроек
|
||||
async def ensure_default(self) -> None:
|
||||
settings = await BaseRepository.get(self, id=1)
|
||||
|
||||
if settings is None:
|
||||
await self._insert(id=1)
|
||||
|
||||
# Получение настроек бота
|
||||
async def get(self, **kwargs) -> Settings:
|
||||
if not kwargs:
|
||||
kwargs = {"id": 1}
|
||||
|
||||
settings = await BaseRepository.get(self, **kwargs)
|
||||
|
||||
if settings is None and kwargs == {"id": 1}:
|
||||
await self.ensure_default()
|
||||
settings = await BaseRepository.get(self, id=1)
|
||||
|
||||
if settings is None:
|
||||
raise RuntimeError("Настройки бота по умолчанию не сохранились")
|
||||
|
||||
return settings
|
||||
|
||||
# Обновление настроек бота
|
||||
async def update(self, where: Optional[Dict[str, Any]] = None, **kwargs) -> int:
|
||||
if where is None:
|
||||
where = {"id": 1}
|
||||
|
||||
return await self._update(where=where, **kwargs)
|
||||
@@ -0,0 +1,128 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
from typing import Any, Dict, Optional, Union
|
||||
|
||||
from sqlalchemy import BigInteger, Float, Integer, String, or_
|
||||
from sqlalchemy import update as sqlalchemy_update
|
||||
from sqlalchemy.dialects.sqlite import insert
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from tgbot.database.core import Base, session_scope
|
||||
from tgbot.database.entities import User
|
||||
from tgbot.database.repository import BaseRepository
|
||||
from tgbot.utils.const_functions import get_unix
|
||||
|
||||
|
||||
class UserModel(Base):
|
||||
__tablename__ = "storage_users"
|
||||
|
||||
increment: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False, unique=True, index=True)
|
||||
user_login: Mapped[str] = mapped_column(String(255), nullable=False, default="")
|
||||
user_name: Mapped[str] = mapped_column(String(255), nullable=False, default="")
|
||||
user_surname: Mapped[str] = mapped_column(String(255), nullable=False, default="")
|
||||
user_fullname: Mapped[str] = mapped_column(String(511), nullable=False, default="")
|
||||
user_balance: Mapped[float] = mapped_column(Float, nullable=False, default=0)
|
||||
user_refill: Mapped[float] = mapped_column(Float, nullable=False, default=0)
|
||||
user_give: Mapped[float] = mapped_column(Float, nullable=False, default=0)
|
||||
user_unix: Mapped[int] = mapped_column(Integer, nullable=False, default=get_unix)
|
||||
|
||||
|
||||
ModelBase = User
|
||||
BaseModel = User
|
||||
|
||||
|
||||
class UsersRepository(BaseRepository[UserModel, User]):
|
||||
# Подключение модели пользователей
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.table_model = UserModel
|
||||
self.entity_model = User
|
||||
self.storage_name = UserModel.__tablename__
|
||||
|
||||
# Добавление или обновление пользователя
|
||||
async def add(
|
||||
self,
|
||||
user_id: int,
|
||||
user_login: str,
|
||||
user_name: str,
|
||||
user_surname: str = "",
|
||||
user_fullname: str = "",
|
||||
) -> User:
|
||||
return await self.upsert(
|
||||
user_id=user_id,
|
||||
user_login=user_login,
|
||||
user_name=user_name,
|
||||
user_surname=user_surname,
|
||||
user_fullname=user_fullname or " ".join(filter(None, [user_name, user_surname])),
|
||||
)
|
||||
|
||||
# Выполнение upsert пользователя по телеграм ID
|
||||
async def upsert(
|
||||
self,
|
||||
user_id: int,
|
||||
user_login: str,
|
||||
user_name: str,
|
||||
user_surname: str = "",
|
||||
user_fullname: str = "",
|
||||
) -> User:
|
||||
user_fullname = user_fullname or " ".join(filter(None, [user_name, user_surname]))
|
||||
user_table = UserModel.__table__
|
||||
statement = insert(UserModel).values(
|
||||
user_id=user_id,
|
||||
user_login=(user_login or "").lower(),
|
||||
user_name=user_name or "",
|
||||
user_surname=user_surname or "",
|
||||
user_fullname=user_fullname or "",
|
||||
user_balance=0,
|
||||
user_refill=0,
|
||||
user_give=0,
|
||||
user_unix=get_unix(),
|
||||
)
|
||||
statement = statement.on_conflict_do_update(
|
||||
index_elements=[user_table.c.user_id],
|
||||
set_={
|
||||
"user_login": statement.excluded.user_login,
|
||||
"user_name": statement.excluded.user_name,
|
||||
"user_surname": statement.excluded.user_surname,
|
||||
"user_fullname": statement.excluded.user_fullname,
|
||||
},
|
||||
where=or_(
|
||||
user_table.c.user_login != statement.excluded.user_login,
|
||||
user_table.c.user_name != statement.excluded.user_name,
|
||||
user_table.c.user_surname != statement.excluded.user_surname,
|
||||
user_table.c.user_fullname != statement.excluded.user_fullname,
|
||||
),
|
||||
)
|
||||
|
||||
async with session_scope() as session:
|
||||
await session.execute(statement)
|
||||
|
||||
user = await self.get(user_id=user_id)
|
||||
|
||||
if user is None:
|
||||
raise RuntimeError("Пользователь не сохранился")
|
||||
|
||||
return user
|
||||
|
||||
# Обновление пользователя по ID или фильтру
|
||||
async def update(self, where: Optional[Union[Dict[str, Any], int]] = None, **kwargs) -> int:
|
||||
if isinstance(where, int):
|
||||
where = {"user_id": where}
|
||||
|
||||
return await self._update(where=where, **kwargs)
|
||||
|
||||
# Обновление пользователя по телеграм ID
|
||||
@staticmethod
|
||||
async def update_by_user_id(user_id: int, **kwargs) -> None:
|
||||
if not kwargs:
|
||||
return
|
||||
|
||||
async with session_scope() as session:
|
||||
await session.execute(
|
||||
sqlalchemy_update(UserModel)
|
||||
.where(UserModel.__table__.c.user_id == user_id)
|
||||
.values(**kwargs)
|
||||
)
|
||||
|
||||
|
||||
Userx = UsersRepository
|
||||
@@ -0,0 +1,110 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Category:
|
||||
increment: int
|
||||
category_id: int
|
||||
category_name: str
|
||||
category_unix: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class Position:
|
||||
increment: int
|
||||
category_id: int
|
||||
position_id: int
|
||||
position_name: str
|
||||
position_price: float
|
||||
position_desc: str
|
||||
position_photo: str
|
||||
position_unix: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class Item:
|
||||
increment: int
|
||||
user_id: int
|
||||
category_id: int
|
||||
position_id: int
|
||||
item_id: int
|
||||
item_unix: int
|
||||
item_data: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Payments:
|
||||
id: int
|
||||
cryptobot_token: str
|
||||
yoomoney_token: str
|
||||
stars_course: float
|
||||
status_cryptobot: str
|
||||
status_yoomoney: str
|
||||
status_stars: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Purchase:
|
||||
increment: int
|
||||
user_id: int
|
||||
user_balance_before: float
|
||||
user_balance_after: float
|
||||
purchase_receipt: int
|
||||
purchase_data: str
|
||||
purchase_count: int
|
||||
purchase_price: float
|
||||
purchase_price_one: float
|
||||
purchase_position_id: int
|
||||
purchase_position_name: str
|
||||
purchase_category_id: int
|
||||
purchase_category_name: str
|
||||
purchase_unix: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class Refill:
|
||||
increment: int
|
||||
user_id: int
|
||||
refill_receipt: int
|
||||
refill_comment: str
|
||||
refill_amount: float
|
||||
refill_method: str
|
||||
refill_unix: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class Settings:
|
||||
id: int
|
||||
status_work: str
|
||||
status_refill: str
|
||||
status_buy: str
|
||||
notification_refill: str
|
||||
notification_buy: str
|
||||
misc_faq: str
|
||||
misc_support: str
|
||||
misc_bot: str
|
||||
misc_hosting_text: str
|
||||
misc_token_telegraph: str
|
||||
misc_discord_webhook_url: str
|
||||
misc_discord_webhook_name: str
|
||||
misc_hide_category: str
|
||||
misc_hide_position: str
|
||||
misc_method_prod: str
|
||||
misc_profit_day: int
|
||||
misc_profit_week: int
|
||||
misc_profit_month: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class User:
|
||||
increment: int
|
||||
user_id: int
|
||||
user_login: str
|
||||
user_name: str
|
||||
user_surname: str
|
||||
user_fullname: str
|
||||
user_balance: float
|
||||
user_refill: float
|
||||
user_give: float
|
||||
user_unix: int
|
||||
@@ -0,0 +1,55 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from alembic import command
|
||||
from alembic.config import Config
|
||||
from sqlalchemy.engine import Connection
|
||||
from sqlalchemy.ext.asyncio import AsyncEngine
|
||||
|
||||
from tgbot.database.core import database_url
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
ALEMBIC_INI = PROJECT_ROOT / "alembic.ini"
|
||||
MIGRATIONS_DIR = PROJECT_ROOT / "migrations"
|
||||
|
||||
|
||||
# Сбор Alembic-конфига от корня проекта
|
||||
def get_alembic_config(url: str = database_url) -> Config:
|
||||
config = Config(str(ALEMBIC_INI))
|
||||
config.set_main_option("script_location", str(MIGRATIONS_DIR))
|
||||
config.set_main_option("sqlalchemy.url", url)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
# Применение миграций до последней версии
|
||||
async def run_migrations(engine: Optional[AsyncEngine] = None) -> None:
|
||||
if engine is None:
|
||||
config = get_alembic_config()
|
||||
await asyncio.to_thread(command.upgrade, config, "head")
|
||||
return
|
||||
|
||||
config = get_alembic_config(str(engine.url))
|
||||
|
||||
connection = engine.connect()
|
||||
await connection.start()
|
||||
transaction = connection.begin()
|
||||
await transaction.start()
|
||||
|
||||
try:
|
||||
await connection.run_sync(_upgrade_with_connection, config)
|
||||
except Exception:
|
||||
await transaction.rollback()
|
||||
raise
|
||||
else:
|
||||
await transaction.commit()
|
||||
finally:
|
||||
await connection.close()
|
||||
|
||||
|
||||
# Запуск Alembic на готовом соединении
|
||||
def _upgrade_with_connection(connection: Connection, config: Config) -> None:
|
||||
config.attributes["connection"] = connection
|
||||
command.upgrade(config, "head")
|
||||
@@ -0,0 +1,162 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
from dataclasses import fields
|
||||
from typing import Any, Dict, Generic, List, Optional, Type, TypeVar
|
||||
|
||||
from sqlalchemy import delete as sqlalchemy_delete
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import update as sqlalchemy_update
|
||||
from tgbot.database.core import Base, database_path, session_scope
|
||||
from tgbot.database.migration_runner import run_migrations
|
||||
from tgbot.utils.misc.bot_logging import bot_logger
|
||||
|
||||
ModelTranslator = TypeVar("ModelTranslator", bound=Base)
|
||||
EntityTranslator = TypeVar("EntityTranslator")
|
||||
|
||||
|
||||
# Базовый репозиторий для SQLAlchemy-моделей
|
||||
class BaseRepository(Generic[ModelTranslator, EntityTranslator]):
|
||||
# Настройка модели репозитория
|
||||
def __init__(self):
|
||||
self.storage_name = "storage"
|
||||
self.table_model: Optional[Type[ModelTranslator]] = None
|
||||
self.entity_model: Optional[Type[EntityTranslator]] = None
|
||||
|
||||
# Получение SQLAlchemy-модели репозитория
|
||||
def _model(self) -> Type[ModelTranslator]:
|
||||
if self.table_model is None:
|
||||
raise RuntimeError("Модель базы данных не настроена")
|
||||
|
||||
return self.table_model
|
||||
|
||||
# Преобразование ORM-строки в безопасный DTO
|
||||
def _to_entity(self, instance: ModelTranslator) -> EntityTranslator:
|
||||
if self.entity_model is None:
|
||||
raise RuntimeError("DTO-модель базы данных не настроена")
|
||||
|
||||
values = {
|
||||
field.name: getattr(instance, field.name)
|
||||
for field in fields(self.entity_model)
|
||||
}
|
||||
|
||||
return self.entity_model(**values)
|
||||
|
||||
# Добавление записи через текущую модель
|
||||
async def _insert(self, **kwargs) -> EntityTranslator:
|
||||
model = self._model()
|
||||
instance = model(**kwargs)
|
||||
|
||||
async with session_scope() as session:
|
||||
session.add(instance)
|
||||
await session.flush()
|
||||
await session.refresh(instance)
|
||||
|
||||
return self._to_entity(instance)
|
||||
|
||||
# Добавление записи с возвратом DTO
|
||||
async def add(self, **kwargs) -> EntityTranslator:
|
||||
return await self._insert(**kwargs)
|
||||
|
||||
# Удаление записи по явному фильтру
|
||||
async def delete(self, **kwargs) -> None:
|
||||
if not kwargs:
|
||||
raise ValueError("Для удаления нужен хотя бы один фильтр")
|
||||
|
||||
model = self._model()
|
||||
|
||||
async with session_scope() as session:
|
||||
await session.execute(sqlalchemy_delete(model).filter_by(**kwargs))
|
||||
|
||||
# Очистка текущей таблицы
|
||||
async def clear(self) -> None:
|
||||
await self.delete_all_rows()
|
||||
|
||||
# Удаление всех строк текущей таблицы
|
||||
async def delete_all_rows(self) -> None:
|
||||
model = self._model()
|
||||
|
||||
async with session_scope() as session:
|
||||
await session.execute(sqlalchemy_delete(model))
|
||||
|
||||
# Получение первой записи по фильтру
|
||||
async def get(self, **kwargs) -> Optional[EntityTranslator]:
|
||||
model = self._model()
|
||||
statement = select(model)
|
||||
|
||||
if kwargs:
|
||||
statement = statement.filter_by(**kwargs)
|
||||
|
||||
if hasattr(model, "increment"):
|
||||
statement = statement.order_by(getattr(model, "increment"))
|
||||
|
||||
async with session_scope() as session:
|
||||
response = await session.execute(statement)
|
||||
instance = response.scalars().first()
|
||||
|
||||
if instance is None:
|
||||
return None
|
||||
|
||||
return self._to_entity(instance)
|
||||
|
||||
# Получение записи или явно падает
|
||||
async def get_required(self, **kwargs) -> EntityTranslator:
|
||||
entity = await self.get(**kwargs)
|
||||
|
||||
if entity is None:
|
||||
raise LookupError(f"Запись не найдена в {self.storage_name}: {kwargs}")
|
||||
|
||||
return entity
|
||||
|
||||
# Получение списка записей по фильтру
|
||||
async def gets(self, **kwargs) -> List[EntityTranslator]:
|
||||
model = self._model()
|
||||
statement = select(model)
|
||||
|
||||
if kwargs:
|
||||
statement = statement.filter_by(**kwargs)
|
||||
|
||||
if hasattr(model, "increment"):
|
||||
statement = statement.order_by(getattr(model, "increment"))
|
||||
|
||||
async with session_scope() as session:
|
||||
response = await session.execute(statement)
|
||||
|
||||
return [self._to_entity(instance) for instance in response.scalars().all()]
|
||||
|
||||
# Получение всех записей таблицы
|
||||
async def get_all(self) -> List[EntityTranslator]:
|
||||
return await self.gets()
|
||||
|
||||
# Обновление записи через текущую модель
|
||||
async def _update(self, where: Optional[Dict[str, Any]] = None, **kwargs) -> int:
|
||||
if not kwargs:
|
||||
return 0
|
||||
|
||||
model = self._model()
|
||||
statement = sqlalchemy_update(model).values(**kwargs)
|
||||
|
||||
if where:
|
||||
statement = statement.filter_by(**where)
|
||||
|
||||
async with session_scope() as session:
|
||||
result = await session.execute(statement)
|
||||
rowcount = getattr(result, "rowcount", 0)
|
||||
|
||||
return int(rowcount or 0)
|
||||
|
||||
# Обновление записи по фильтру
|
||||
async def update(self, where: Optional[Dict[str, Any]] = None, **kwargs) -> int:
|
||||
return await self._update(where=where, **kwargs)
|
||||
|
||||
|
||||
# Применение миграций и создание дефолтных строк
|
||||
async def prepare_database() -> None:
|
||||
database_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
await run_migrations()
|
||||
|
||||
from tgbot.database.db_payments import Paymentsx
|
||||
from tgbot.database.db_settings import Settingsx
|
||||
|
||||
await Settingsx().ensure_default()
|
||||
await Paymentsx().ensure_default()
|
||||
|
||||
bot_logger.info("База данных готова")
|
||||
@@ -0,0 +1,255 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
from aiogram.types import InlineKeyboardMarkup
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from tgbot.database import Settingsx, Paymentsx
|
||||
from tgbot.utils.const_functions import ikb
|
||||
|
||||
|
||||
################################################################################
|
||||
#################################### ПРОЧЕЕ ####################################
|
||||
# Удаление сообщения
|
||||
def close_finl() -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardBuilder()
|
||||
|
||||
keyboard.row(
|
||||
ikb("❌ Закрыть", data="close_this"),
|
||||
)
|
||||
|
||||
return keyboard.as_markup()
|
||||
|
||||
|
||||
# Рассылка
|
||||
def mail_confirm_finl() -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardBuilder()
|
||||
|
||||
keyboard.row(
|
||||
ikb("✅ Отправить", data="mail_confirm:Yes"),
|
||||
ikb("❌ Отменить", data="mail_confirm:Not"),
|
||||
)
|
||||
|
||||
return keyboard.as_markup()
|
||||
|
||||
|
||||
# Поиск профиля пользователя
|
||||
def profile_edit_finl(user_id: int) -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardBuilder()
|
||||
|
||||
keyboard.row(
|
||||
ikb("💰 Изменить баланс", data=f"admin_user_balance_set:{user_id}"),
|
||||
ikb("💰 Выдать баланс", data=f"admin_user_balance_add:{user_id}"),
|
||||
).row(
|
||||
ikb("🎁 Покупки", data=f"admin_user_purchases:{user_id}"),
|
||||
ikb("💌 Отправить СМС", data=f"admin_user_message:{user_id}"),
|
||||
).row(
|
||||
ikb("🔄 Обновить", data=f"admin_user_refresh:{user_id}"),
|
||||
)
|
||||
|
||||
return keyboard.as_markup()
|
||||
|
||||
|
||||
# Переход к профилю пользователя
|
||||
def profile_edit_return_finl(user_id: int) -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardBuilder()
|
||||
|
||||
keyboard.row(
|
||||
ikb("❌ Отменить", data=f"admin_user_refresh:{user_id}"),
|
||||
)
|
||||
|
||||
return keyboard.as_markup()
|
||||
|
||||
|
||||
################################################################################
|
||||
############################## ПЛАТЕЖНЫЕ СИСТЕМЫ ###############################
|
||||
# Управление - CryptoBot
|
||||
async def payment_cryptobot_finl() -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardBuilder()
|
||||
|
||||
get_payments = await Paymentsx().get()
|
||||
|
||||
if get_payments.cryptobot_token == "None":
|
||||
assets_symbol = "➖"
|
||||
else:
|
||||
assets_symbol = "➕"
|
||||
|
||||
if get_payments.status_cryptobot == "True":
|
||||
status_kb = ikb(f"{assets_symbol} | Статус: Включено ✅", data="payment_cryptobot_status:False")
|
||||
else:
|
||||
status_kb = ikb(f"{assets_symbol} | Статус: Выключено ❌", data="payment_cryptobot_status:True")
|
||||
|
||||
keyboard.row(
|
||||
ikb("Информация ♻️", data="payment_cryptobot_check"),
|
||||
).row(
|
||||
ikb("Баланс 💰", data="payment_cryptobot_balance"),
|
||||
).row(
|
||||
ikb("Изменить 🖍", data="payment_cryptobot_edit"),
|
||||
).row(
|
||||
ikb("", data="..."),
|
||||
).row(
|
||||
status_kb,
|
||||
)
|
||||
|
||||
return keyboard.as_markup()
|
||||
|
||||
|
||||
# Управление - ЮMoney
|
||||
async def payment_yoomoney_finl() -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardBuilder()
|
||||
|
||||
get_payments = await Paymentsx().get()
|
||||
|
||||
if get_payments.yoomoney_token == "None":
|
||||
assets_symbol = "➖"
|
||||
else:
|
||||
assets_symbol = "➕"
|
||||
|
||||
if get_payments.status_yoomoney == "True":
|
||||
status_kb = ikb(f"{assets_symbol} | Статус: Включено ✅", data="payment_yoomoney_status:False")
|
||||
else:
|
||||
status_kb = ikb(f"{assets_symbol} | Статус: Выключено ❌", data="payment_yoomoney_status:True")
|
||||
|
||||
keyboard.row(
|
||||
ikb("Информация ♻️", data="payment_yoomoney_check"),
|
||||
).row(
|
||||
ikb("Баланс 💰", data="payment_yoomoney_balance"),
|
||||
).row(
|
||||
ikb("Изменить 🖍", data="payment_yoomoney_edit"),
|
||||
).row(
|
||||
ikb("", data="..."),
|
||||
).row(
|
||||
status_kb,
|
||||
)
|
||||
|
||||
return keyboard.as_markup()
|
||||
|
||||
|
||||
# Управление - Звёзды
|
||||
async def payment_stars_finl() -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardBuilder()
|
||||
|
||||
get_payments = await Paymentsx().get()
|
||||
|
||||
assets_symbol = "➕"
|
||||
|
||||
if get_payments.status_stars == "True":
|
||||
status_kb = ikb(f"{assets_symbol} | Статус: Включено ✅", data="payment_stars_status:False")
|
||||
else:
|
||||
status_kb = ikb(f"{assets_symbol} | Статус: Выключено ❌", data="payment_stars_status:True")
|
||||
|
||||
keyboard.row(
|
||||
ikb("Информация ♻️", data="payment_stars_check"),
|
||||
).row(
|
||||
ikb("Баланс 💰", data="payment_stars_balance"),
|
||||
).row(
|
||||
ikb("Изменить курс 🖍", data="payment_stars_edit"),
|
||||
).row(
|
||||
ikb("", data="..."),
|
||||
).row(
|
||||
status_kb,
|
||||
)
|
||||
|
||||
return keyboard.as_markup()
|
||||
|
||||
|
||||
################################################################################
|
||||
################################## НАСТРОЙКИ ###################################
|
||||
# Основные настройки
|
||||
async def settings_finl() -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardBuilder()
|
||||
|
||||
get_settings = await Settingsx().get()
|
||||
|
||||
# Текст для FAQ
|
||||
if get_settings.misc_faq == "None":
|
||||
faq_kb = ikb("Не установлено ❌", data="settings_edit_faq")
|
||||
else:
|
||||
faq_kb = ikb(f"{get_settings.misc_faq[:15]}... ✅", data="settings_edit_faq")
|
||||
|
||||
# Контакты поддержки
|
||||
if get_settings.misc_support == "None":
|
||||
support_kb = ikb("Не установлена ❌", data="settings_edit_support")
|
||||
else:
|
||||
support_kb = ikb(f"@{get_settings.misc_support} ✅", data="settings_edit_support")
|
||||
|
||||
# Вебхук дискорда
|
||||
if get_settings.misc_discord_webhook_url == "None":
|
||||
discord_webhook_kb = ikb("Отсутствует ❌", data="settings_edit_discord_webhook")
|
||||
else:
|
||||
discord_webhook_kb = ikb(f"{get_settings.misc_discord_webhook_name} ✅", data="settings_edit_discord_webhook")
|
||||
|
||||
# Текстовый хостинг по умолчанию
|
||||
hosting_text_default_kb = ikb(get_settings.misc_hosting_text.title(), data="settings_edit_hosting_text")
|
||||
|
||||
# Скрытие категорий без товаров
|
||||
if get_settings.misc_hide_category == "True":
|
||||
hide_category_kb = ikb("Скрыты", data="settings_edit_hide_category:False")
|
||||
else:
|
||||
hide_category_kb = ikb("Отображены", data="settings_edit_hide_category:True")
|
||||
|
||||
# Скрытие позиций без товаров
|
||||
if get_settings.misc_hide_position == "True":
|
||||
hide_position_kb = ikb("Скрыты", data="settings_edit_hide_position:False")
|
||||
else:
|
||||
hide_position_kb = ikb("Отображены", data="settings_edit_hide_position:True")
|
||||
|
||||
# Метод добавления товаров
|
||||
if get_settings.misc_method_prod == "skip":
|
||||
method_prod_kb = ikb("Через строку", data="settings_edit_method_prod:every")
|
||||
else:
|
||||
method_prod_kb = ikb("На каждой строке", data="settings_edit_method_prod:skip")
|
||||
|
||||
keyboard.row(
|
||||
ikb("❔ FAQ", data="..."), faq_kb,
|
||||
).row(
|
||||
ikb("☎️ Поддержка", data="..."), support_kb,
|
||||
).row(
|
||||
ikb("🧿 Дискорд Webhook", url="https://teletype.in/@djimbox/djimboshop-discord"), discord_webhook_kb,
|
||||
).row(
|
||||
ikb("🗯 Текстовый хостинг", data="..."), hosting_text_default_kb,
|
||||
).row(
|
||||
ikb("🎁 Категории без товаров", data="..."), hide_category_kb,
|
||||
).row(
|
||||
ikb("🎁 Позиции без товаров", data="..."), hide_position_kb,
|
||||
).row(
|
||||
ikb("🎁 Метод добавления", data="..."), method_prod_kb,
|
||||
)
|
||||
|
||||
return keyboard.as_markup()
|
||||
|
||||
|
||||
# Выключатели
|
||||
async def settings_status_finl() -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardBuilder()
|
||||
|
||||
get_settings = await Settingsx().get()
|
||||
|
||||
status_work_kb = ikb("Включены ✅", data="settings_status_work:False")
|
||||
status_buy_kb = ikb("Включены ✅", data="settings_status_buy:False")
|
||||
status_refill_kb = ikb("Включены ✅", data="settings_status_refill:False")
|
||||
notification_buy_kb = ikb("Включены 🔔", data="settings_notification_buy:False")
|
||||
notification_refill_kb = ikb("Включены 🔔", data="settings_notification_refill:False")
|
||||
|
||||
if get_settings.status_buy == "False":
|
||||
status_buy_kb = ikb("Выключены ❌", data="settings_status_buy:True")
|
||||
if get_settings.status_work == "False":
|
||||
status_work_kb = ikb("Выключены ❌", data="settings_status_work:True")
|
||||
if get_settings.status_refill == "False":
|
||||
status_refill_kb = ikb("Выключены ❌", data="settings_status_refill:True")
|
||||
if get_settings.notification_buy == "False":
|
||||
notification_buy_kb = ikb("Выключены 🔕", data="settings_notification_buy:True")
|
||||
if get_settings.notification_refill == "False":
|
||||
notification_refill_kb = ikb("Выключены 🔕", data="settings_notification_refill:True")
|
||||
|
||||
keyboard.row(
|
||||
ikb("⛔ Тех. работы", data="..."), status_work_kb,
|
||||
).row(
|
||||
ikb("💰 Пополнения", data="..."), status_refill_kb,
|
||||
).row(
|
||||
ikb("🎁 Покупки", data="..."), status_buy_kb,
|
||||
).row(
|
||||
ikb("📢 Увед. о покупках", data="..."), notification_buy_kb,
|
||||
).row(
|
||||
ikb("📢 Увед. о пополнениях", data="..."), notification_refill_kb,
|
||||
)
|
||||
|
||||
return keyboard.as_markup()
|
||||
@@ -0,0 +1,188 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
from aiogram.types import InlineKeyboardMarkup
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from tgbot.database import Categoryx, Positionx, Itemx
|
||||
from tgbot.keyboards.inline_helper import build_pagination_finl
|
||||
from tgbot.utils.const_functions import ikb
|
||||
|
||||
|
||||
# fp - flip page
|
||||
|
||||
################################################################################
|
||||
############################## ИЗМЕНЕНИЕ КАТЕГОРИИ #############################
|
||||
# Cтраницы выбора категории для изменения
|
||||
async def category_edit_swipe_fp(remover: int) -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardBuilder()
|
||||
|
||||
get_categories = await Categoryx().get_all()
|
||||
|
||||
for count, select in enumerate(range(remover, len(get_categories))):
|
||||
if count < 10:
|
||||
category = get_categories[select]
|
||||
|
||||
keyboard.row(
|
||||
ikb(
|
||||
category.category_name,
|
||||
data=f"category_edit_open:{category.category_id}:{remover}",
|
||||
)
|
||||
)
|
||||
|
||||
buildp_kb = build_pagination_finl(get_categories, f"category_edit_swipe", remover)
|
||||
keyboard.row(*buildp_kb)
|
||||
|
||||
return keyboard.as_markup()
|
||||
|
||||
|
||||
################################################################################
|
||||
################################ СОЗДАНИЕ ПОЗИЦИИ ##############################
|
||||
# Страницы выбора категории для позиции
|
||||
async def position_add_swipe_fp(remover: int) -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardBuilder()
|
||||
|
||||
get_categories = await Categoryx().get_all()
|
||||
|
||||
for count, select in enumerate(range(remover, len(get_categories))):
|
||||
if count < 10:
|
||||
category = get_categories[select]
|
||||
|
||||
keyboard.row(
|
||||
ikb(
|
||||
category.category_name,
|
||||
data=f"position_add_open:{category.category_id}",
|
||||
)
|
||||
)
|
||||
|
||||
buildp_kb = build_pagination_finl(get_categories, f"position_add_swipe", remover)
|
||||
keyboard.row(*buildp_kb)
|
||||
|
||||
return keyboard.as_markup()
|
||||
|
||||
|
||||
################################################################################
|
||||
############################### ИЗМЕНЕНИЕ ПОЗИЦИИ ##############################
|
||||
# Cтраницы категорий для изменения позиции
|
||||
async def position_edit_category_swipe_fp(remover: int) -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardBuilder()
|
||||
|
||||
get_categories = await Categoryx().get_all()
|
||||
|
||||
for count, select in enumerate(range(remover, len(get_categories))):
|
||||
if count < 10:
|
||||
category = get_categories[select]
|
||||
|
||||
keyboard.row(
|
||||
ikb(
|
||||
category.category_name,
|
||||
data=f"position_edit_category_open:{category.category_id}"
|
||||
)
|
||||
)
|
||||
|
||||
buildp_kb = build_pagination_finl(get_categories, f"position_edit_category_swipe", remover)
|
||||
keyboard.row(*buildp_kb)
|
||||
|
||||
return keyboard.as_markup()
|
||||
|
||||
|
||||
# Cтраницы выбора позиции для изменения
|
||||
async def position_edit_swipe_fp(remover: int, category_id: int) -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardBuilder()
|
||||
|
||||
get_positions = await Positionx().gets(category_id=category_id)
|
||||
|
||||
for count, select in enumerate(range(remover, len(get_positions))):
|
||||
if count < 10:
|
||||
position = get_positions[select]
|
||||
get_items = await Itemx().gets(position_id=position.position_id)
|
||||
|
||||
keyboard.row(
|
||||
ikb(
|
||||
f"{position.position_name} | {position.position_price}₽ | {len(get_items)} шт",
|
||||
data=f"position_edit_open:{position.position_id}:{remover}",
|
||||
)
|
||||
)
|
||||
|
||||
buildp_kb = build_pagination_finl(get_positions, f"position_edit_swipe:{category_id}", remover)
|
||||
keyboard.row(*buildp_kb)
|
||||
|
||||
keyboard.row(ikb("🔙 Вернуться", data="position_edit_category_swipe:0"))
|
||||
|
||||
return keyboard.as_markup()
|
||||
|
||||
|
||||
################################################################################
|
||||
############################### ДОБАВЛЕНИЕ ТОВАРОВ #############################
|
||||
# Страницы категорий для добавления товаров
|
||||
async def item_add_category_swipe_fp(remover: int) -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardBuilder()
|
||||
|
||||
get_categories = await Categoryx().get_all()
|
||||
|
||||
for count, select in enumerate(range(remover, len(get_categories))):
|
||||
if count < 10:
|
||||
category = get_categories[select]
|
||||
|
||||
keyboard.row(
|
||||
ikb(
|
||||
category.category_name,
|
||||
data=f"item_add_category_open:{category.category_id}:{remover}",
|
||||
)
|
||||
)
|
||||
|
||||
buildp_kb = build_pagination_finl(get_categories, f"item_add_category_swipe", remover)
|
||||
keyboard.row(*buildp_kb)
|
||||
|
||||
return keyboard.as_markup()
|
||||
|
||||
|
||||
# Страницы позиций для добавления товаров
|
||||
async def item_add_position_swipe_fp(remover: int, category_id: int) -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardBuilder()
|
||||
|
||||
get_positions = await Positionx().gets(category_id=category_id)
|
||||
|
||||
for count, select in enumerate(range(remover, len(get_positions))):
|
||||
if count < 10:
|
||||
position = get_positions[select]
|
||||
get_items = await Itemx().gets(position_id=position.position_id)
|
||||
|
||||
keyboard.row(
|
||||
ikb(
|
||||
f"{position.position_name} | {position.position_price}₽ | {len(get_items)} шт",
|
||||
data=f"item_add_position_open:{position.position_id}",
|
||||
)
|
||||
)
|
||||
|
||||
buildp_kb = build_pagination_finl(get_positions, f"item_add_position_swipe:{category_id}", remover)
|
||||
keyboard.row(*buildp_kb)
|
||||
|
||||
keyboard.row(ikb("🔙 Вернуться", data="item_add_category_swipe:0"))
|
||||
|
||||
return keyboard.as_markup()
|
||||
|
||||
|
||||
################################################################################
|
||||
################################ УДАЛЕНИЕ ТОВАРОВ ##############################
|
||||
# Страницы товаров для удаления
|
||||
async def item_delete_swipe_fp(remover: int, position_id: int) -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardBuilder()
|
||||
|
||||
get_items = await Itemx().gets(position_id=position_id)
|
||||
|
||||
for count, select in enumerate(range(remover, len(get_items))):
|
||||
if count < 10:
|
||||
item = get_items[select]
|
||||
|
||||
keyboard.row(
|
||||
ikb(
|
||||
item.item_data,
|
||||
data=f"item_delete_open:{item.item_id}",
|
||||
)
|
||||
)
|
||||
|
||||
buildp_kb = build_pagination_finl(get_items, f"item_delete_swipe:{position_id}", remover)
|
||||
keyboard.row(*buildp_kb)
|
||||
|
||||
keyboard.row(ikb("🔙 Вернуться", data=f"position_edit_open:{position_id}:0"))
|
||||
|
||||
return keyboard.as_markup()
|
||||
@@ -0,0 +1,198 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
from aiogram import Bot
|
||||
from aiogram.types import InlineKeyboardMarkup
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from tgbot.database import Positionx
|
||||
from tgbot.utils.const_functions import ikb
|
||||
|
||||
|
||||
################################################################################
|
||||
################################### КАТЕГОРИИ ##################################
|
||||
# Изменение категории
|
||||
async def category_edit_open_finl(bot: Bot, category_id: int, remover: int) -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardBuilder()
|
||||
|
||||
get_bot = await bot.get_me()
|
||||
|
||||
keyboard.row(
|
||||
ikb("▪️ Изм. Название", data=f"category_edit_name:{category_id}:{remover}"),
|
||||
ikb("▪️ Добавить позицию", data=f"position_add_open:{category_id}"),
|
||||
).row(
|
||||
ikb("▪️ Скопировать ссылку", copy=f"t.me/{get_bot.username}?start=c_{category_id}"),
|
||||
ikb("▪️ Удалить", data=f"category_edit_delete:{category_id}:{remover}"),
|
||||
).row(
|
||||
ikb("🔙 Вернуться", data=f"category_edit_swipe:{remover}"),
|
||||
ikb("▪️ Обновить", data=f"category_edit_open:{category_id}:{remover}"),
|
||||
)
|
||||
|
||||
return keyboard.as_markup()
|
||||
|
||||
|
||||
# Подтверждение удаления категории
|
||||
def category_edit_delete_finl(category_id: int, remover: int) -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardBuilder()
|
||||
|
||||
keyboard.row(
|
||||
ikb("✅ Да, удалить", data=f"category_edit_delete_confirm:{category_id}:{remover}"),
|
||||
ikb("❌ Нет, отменить", data=f"category_edit_open:{category_id}:{remover}")
|
||||
)
|
||||
|
||||
return keyboard.as_markup()
|
||||
|
||||
|
||||
# Отмена изменения категории и возвращение
|
||||
def category_edit_cancel_finl(category_id: int, remover: int) -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardBuilder()
|
||||
|
||||
keyboard.row(
|
||||
ikb("❌ Отменить", data=f"category_edit_open:{category_id}:{remover}"),
|
||||
)
|
||||
|
||||
return keyboard.as_markup()
|
||||
|
||||
|
||||
################################################################################
|
||||
#################################### ПОЗИЦИИ ###################################
|
||||
# Кнопки при открытии позиции для изменения
|
||||
async def position_edit_open_finl(bot: Bot, position_id: int, remover: int) -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardBuilder()
|
||||
|
||||
get_position = await Positionx().get_required(position_id=position_id)
|
||||
get_bot = await bot.get_me()
|
||||
|
||||
keyboard.row(
|
||||
ikb("▪️ Изм. Название", data=f"position_edit_name:{position_id}:{remover}"),
|
||||
ikb("▪️ Изм. Цену", data=f"position_edit_price:{position_id}:{remover}"),
|
||||
).row(
|
||||
ikb("▪️ Изм. Описание", data=f"position_edit_desc:{position_id}:{remover}"),
|
||||
ikb("▪️ Изм. Фото", data=f"position_edit_photo:{position_id}:{remover}"),
|
||||
).row(
|
||||
ikb("▪️ Добавить Товары", data=f"item_add_position_open:{position_id}"),
|
||||
ikb("▪️ Выгрузить Товары", data=f"position_edit_items:{position_id}:{remover}"),
|
||||
).row(
|
||||
ikb("▪️ Очистить Товары", data=f"position_edit_clear:{position_id}:{remover}"),
|
||||
ikb("▪️ Удалить Товар", data=f"item_delete_swipe:{position_id}:0"),
|
||||
).row(
|
||||
ikb("▪️ Скопировать ссылку", copy=f"t.me/{get_bot.username}?start=p_{position_id}"),
|
||||
ikb("▪️ Удалить Позицию", data=f"position_edit_delete:{position_id}:{remover}"),
|
||||
).row(
|
||||
ikb("🔙 Вернуться", data=f"position_edit_swipe:{get_position.category_id}:{remover}"),
|
||||
ikb("▪️ Обновить", data=f"position_edit_open:{position_id}:{remover}"),
|
||||
)
|
||||
|
||||
return keyboard.as_markup()
|
||||
|
||||
|
||||
# Подтверждение удаления позиции
|
||||
def position_edit_delete_finl(position_id: int, remover: int) -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardBuilder()
|
||||
|
||||
keyboard.row(
|
||||
ikb("✅ Да, удалить", data=f"position_edit_delete_confirm:{position_id}:{remover}"),
|
||||
ikb("❌ Нет, отменить", data=f"position_edit_open:{position_id}:{remover}")
|
||||
)
|
||||
|
||||
return keyboard.as_markup()
|
||||
|
||||
|
||||
# Подтверждение очистки позиции
|
||||
def position_edit_clear_finl(position_id: int, remover: int) -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardBuilder()
|
||||
|
||||
keyboard.row(
|
||||
ikb("✅ Да, очистить", data=f"position_edit_clear_confirm:{position_id}:{remover}"),
|
||||
ikb("❌ Нет, отменить", data=f"position_edit_open:{position_id}:{remover}")
|
||||
)
|
||||
|
||||
return keyboard.as_markup()
|
||||
|
||||
|
||||
# Отмена изменения позиции и возвращение
|
||||
def position_edit_cancel_finl(position_id: int, remover: int) -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardBuilder()
|
||||
|
||||
keyboard.row(
|
||||
ikb("❌ Отменить", data=f"position_edit_open:{position_id}:{remover}"),
|
||||
)
|
||||
|
||||
return keyboard.as_markup()
|
||||
|
||||
|
||||
################################################################################
|
||||
##################################### ТОВАРЫ ###################################
|
||||
# Отмена изменения позиции и возвращение
|
||||
def item_add_finish_finl(position_id: int) -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardBuilder()
|
||||
|
||||
keyboard.row(
|
||||
ikb("✅ Завершить загрузку", data=f"item_add_position_finish:{position_id}"),
|
||||
)
|
||||
|
||||
return keyboard.as_markup()
|
||||
|
||||
|
||||
# Удаление товара
|
||||
def item_delete_finl(item_id: int, position_id: int) -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardBuilder()
|
||||
|
||||
keyboard.row(
|
||||
ikb("▪️ Удалить товар", data=f"item_delete_confirm:{item_id}"),
|
||||
).row(
|
||||
ikb("🔙 Вернуться", data=f"item_delete_swipe:{position_id}:0"),
|
||||
)
|
||||
|
||||
return keyboard.as_markup()
|
||||
|
||||
|
||||
################################################################################
|
||||
############################### УДАЛЕНИЕ РАЗДЕЛОВ ##############################
|
||||
# Выбор раздела для удаления
|
||||
def products_removes_finl() -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardBuilder()
|
||||
|
||||
keyboard.row(
|
||||
ikb("🗃 Удалить все категории", data=f"prod_removes_categories"),
|
||||
).row(
|
||||
ikb("📁 Удалить все позиции", data=f"prod_removes_positions"),
|
||||
).row(
|
||||
ikb("🎁 Удалить все товары", data=f"prod_removes_items"),
|
||||
)
|
||||
|
||||
return keyboard.as_markup()
|
||||
|
||||
|
||||
# Удаление всех категорий
|
||||
def products_removes_categories_finl() -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardBuilder()
|
||||
|
||||
keyboard.row(
|
||||
ikb("✅ Да, удалить все", data="prod_removes_categories_confirm"),
|
||||
ikb("❌ Нет, отменить", data="prod_removes_return")
|
||||
)
|
||||
|
||||
return keyboard.as_markup()
|
||||
|
||||
|
||||
# Удаление всех позиций
|
||||
def products_removes_positions_finl() -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardBuilder()
|
||||
|
||||
keyboard.row(
|
||||
ikb("✅ Да, удалить все", data="prod_removes_positions_confirm"),
|
||||
ikb("❌ Нет, отменить", data="prod_removes_return")
|
||||
)
|
||||
|
||||
return keyboard.as_markup()
|
||||
|
||||
|
||||
# Удаление всех товаров
|
||||
def products_removes_items_finl() -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardBuilder()
|
||||
|
||||
keyboard.row(
|
||||
ikb("✅ Да, удалить все", data="prod_removes_items_confirm"),
|
||||
ikb("❌ Нет, отменить", data="prod_removes_return")
|
||||
)
|
||||
|
||||
return keyboard.as_markup()
|
||||
@@ -0,0 +1,82 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
import math
|
||||
from typing import List
|
||||
|
||||
from aiogram.types import InlineKeyboardButton
|
||||
|
||||
from tgbot.utils.const_functions import ikb
|
||||
|
||||
|
||||
# Генерация пагинации страниц
|
||||
def build_pagination_finl(
|
||||
get_list: List[object],
|
||||
button_data: str,
|
||||
remover: int,
|
||||
) -> List[InlineKeyboardButton]:
|
||||
save_kb = []
|
||||
|
||||
if len(get_list) % 10 == 0:
|
||||
remover_page = len(get_list)
|
||||
else:
|
||||
remover_page = len(get_list) - len(get_list) % 10
|
||||
|
||||
if len(get_list) % 10 == 0 and remover_page == len(get_list):
|
||||
remover_page -= 10
|
||||
|
||||
if remover >= len(get_list): remover -= 10
|
||||
if remover < 0: remover = 0
|
||||
|
||||
if len(get_list) <= 10:
|
||||
...
|
||||
elif len(get_list) > 10 and remover < 10:
|
||||
if len(get_list) > 20:
|
||||
save_kb += [
|
||||
ikb(f"1/{math.ceil(len(get_list) / 10)}", data="..."),
|
||||
ikb("➡️", data=f"{button_data}:{remover + 10}"),
|
||||
ikb("⏩", data=f"{button_data}:{remover_page}"),
|
||||
]
|
||||
else:
|
||||
save_kb += [
|
||||
ikb(f"1/{math.ceil(len(get_list) / 10)}", data="..."),
|
||||
ikb("➡️", data=f"{button_data}:{remover + 10}"),
|
||||
]
|
||||
elif remover + 10 >= len(get_list):
|
||||
if len(get_list) > 20:
|
||||
save_kb += [
|
||||
ikb("⏪", data=f"{button_data}:0"),
|
||||
ikb("⬅️", data=f"{button_data}:{remover - 10}"),
|
||||
ikb(f"{str(remover + 10)[:-1]}/{math.ceil(len(get_list) / 10)}", data="..."),
|
||||
]
|
||||
else:
|
||||
save_kb += [
|
||||
ikb("⬅️", data=f"{button_data}:{remover - 10}"),
|
||||
ikb(f"{str(remover + 10)[:-1]}/{math.ceil(len(get_list) / 10)}", data="..."),
|
||||
]
|
||||
else:
|
||||
if len(get_list) > 20:
|
||||
if remover >= 20:
|
||||
save_kb += [
|
||||
ikb("⏪", data=f"{button_data}:0"),
|
||||
ikb("⬅️", data=f"{button_data}:{remover - 10}"),
|
||||
ikb(f"{str(remover + 10)[:-1]}/{math.ceil(len(get_list) / 10)}", data="..."),
|
||||
ikb("➡️", data=f"{button_data}:{remover + 10}"),
|
||||
]
|
||||
else:
|
||||
save_kb += [
|
||||
ikb("⬅️", data=f"{button_data}:{remover - 10}"),
|
||||
ikb(f"{str(remover + 10)[:-1]}/{math.ceil(len(get_list) / 10)}", data="..."),
|
||||
ikb("➡️", data=f"{button_data}:{remover + 10}"),
|
||||
]
|
||||
|
||||
if remover_page - 10 > remover:
|
||||
save_kb += [
|
||||
ikb("⏩", data=f"{button_data}:{remover_page}"),
|
||||
]
|
||||
else:
|
||||
save_kb += [
|
||||
ikb("⬅️", data=f"{button_data}:{remover - 10}"),
|
||||
ikb(f"{str(remover + 10)[:-1]}/{math.ceil(len(get_list) / 10)}", data="..."),
|
||||
ikb("➡️", data=f"{button_data}:{remover + 10}"),
|
||||
]
|
||||
|
||||
return save_kb
|
||||
@@ -0,0 +1,84 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
from typing import Union
|
||||
|
||||
from aiogram.types import InlineKeyboardMarkup
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from tgbot.database import Paymentsx
|
||||
from tgbot.utils.const_functions import ikb
|
||||
|
||||
|
||||
################################################################################
|
||||
#################################### ПРОЧЕЕ ####################################
|
||||
# Открытие своего профиля
|
||||
def user_profile_finl() -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardBuilder()
|
||||
|
||||
keyboard.row(
|
||||
ikb("💰 Пополнить", data="user_refill"),
|
||||
ikb("🎁 Мои покупки", data="user_purchases"),
|
||||
)
|
||||
|
||||
return keyboard.as_markup()
|
||||
|
||||
|
||||
# Ссылка на поддержку
|
||||
def user_support_finl(support_login: str) -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardBuilder()
|
||||
|
||||
keyboard.row(
|
||||
ikb("💌 Написать в поддержку", url=f"https://t.me/{support_login}"),
|
||||
)
|
||||
|
||||
return keyboard.as_markup()
|
||||
|
||||
|
||||
################################################################################
|
||||
################################### ПЛАТЕЖИ ####################################
|
||||
# Выбор способа пополнения
|
||||
async def refill_method_finl() -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardBuilder()
|
||||
|
||||
get_payments = await Paymentsx().get()
|
||||
|
||||
if get_payments.status_cryptobot == "True":
|
||||
keyboard.row(ikb("🔷 CryptoBot", data="user_refill_method:Cryptobot"))
|
||||
if get_payments.status_yoomoney == "True":
|
||||
keyboard.row(ikb("🔮 ЮMoney", data="user_refill_method:Yoomoney"))
|
||||
if get_payments.status_stars == "True":
|
||||
keyboard.row(ikb("⭐️ Звёзды", data="user_refill_method:Stars"))
|
||||
|
||||
keyboard.row(ikb("🔙 Вернуться", data="user_profile"))
|
||||
|
||||
return keyboard.as_markup()
|
||||
|
||||
|
||||
# Проверка платежа
|
||||
def refill_bill_finl(pay_link: str, pay_receipt: Union[str, int], pay_method: str) -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardBuilder()
|
||||
|
||||
keyboard.row(
|
||||
ikb("🌀 Перейти к оплате", url=pay_link),
|
||||
).row(
|
||||
ikb("🔄 Проверить оплату", data=f"Pay:{pay_method}:{pay_receipt}"),
|
||||
)
|
||||
|
||||
return keyboard.as_markup()
|
||||
|
||||
|
||||
# Выбор способа пополнения при нехватке баланса во время покупки товара
|
||||
async def refill_method_buy_finl() -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardBuilder()
|
||||
|
||||
get_payments = await Paymentsx().get()
|
||||
|
||||
if get_payments.status_cryptobot == "True":
|
||||
keyboard.row(ikb("🔷 CryptoBot", data="user_refill_method:Cryptobot"))
|
||||
if get_payments.status_yoomoney == "True":
|
||||
keyboard.row(ikb("🔮 ЮMoney", data="user_refill_method:Yoomoney"))
|
||||
if get_payments.status_stars == "True":
|
||||
keyboard.row(ikb("⭐️ Звёзды", data="user_refill_method:Stars"))
|
||||
|
||||
keyboard.row(ikb("❌ Закрыть", data="close_this"))
|
||||
|
||||
return keyboard.as_markup()
|
||||
@@ -0,0 +1,98 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
from aiogram.types import InlineKeyboardMarkup
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from tgbot.database import Itemx
|
||||
from tgbot.keyboards.inline_helper import build_pagination_finl
|
||||
from tgbot.utils.const_functions import ikb
|
||||
from tgbot.utils.products_functions import get_positions_items, get_categories_items
|
||||
|
||||
|
||||
# fp - flip page
|
||||
|
||||
|
||||
################################################################################
|
||||
################################ ПОКУПКИ ТОВАРОВ ###############################
|
||||
# Страницы категорий при покупке товара
|
||||
async def prod_item_category_swipe_fp(remover: int) -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardBuilder()
|
||||
|
||||
get_categories = await get_categories_items()
|
||||
|
||||
for count, select in enumerate(range(remover, len(get_categories))):
|
||||
if count < 10:
|
||||
category = get_categories[select]
|
||||
|
||||
keyboard.row(
|
||||
ikb(
|
||||
category.category_name,
|
||||
data=f"buy_category_open:{category.category_id}:0",
|
||||
)
|
||||
)
|
||||
|
||||
buildp_kb = build_pagination_finl(get_categories, f"buy_category_swipe", remover)
|
||||
keyboard.row(*buildp_kb)
|
||||
|
||||
return keyboard.as_markup()
|
||||
|
||||
|
||||
# Страницы позиций для покупки товаров
|
||||
async def prod_item_position_swipe_fp(remover: int, category_id: int) -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardBuilder()
|
||||
|
||||
get_positions = await get_positions_items(category_id)
|
||||
|
||||
for count, select in enumerate(range(remover, len(get_positions))):
|
||||
if count < 10:
|
||||
position = get_positions[select]
|
||||
get_items = await Itemx().gets(position_id=get_positions[select].position_id)
|
||||
|
||||
keyboard.row(
|
||||
ikb(
|
||||
f"{position.position_name} | {position.position_price}₽ | {len(get_items)} шт",
|
||||
data=f"buy_position_open:{position.position_id}:{remover}",
|
||||
)
|
||||
)
|
||||
|
||||
buildp_kb = build_pagination_finl(get_positions, f"buy_position_swipe:{category_id}", remover)
|
||||
keyboard.row(*buildp_kb)
|
||||
|
||||
keyboard.row(ikb("🔙 Вернуться", data=f"buy_category_swipe:0"))
|
||||
|
||||
return keyboard.as_markup()
|
||||
|
||||
|
||||
################################################################################
|
||||
################################ НАЛИЧИЕ ТОВАРОВ ###############################
|
||||
# Страницы наличия товаров
|
||||
def prod_available_swipe_fp(remover_now: int, remover_max: int) -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardBuilder()
|
||||
|
||||
if remover_max > 1:
|
||||
if remover_now > 1 and remover_max >= 3:
|
||||
keyboard.add(
|
||||
ikb("⏪", data=f"user_available_swipe:{0}"),
|
||||
)
|
||||
|
||||
if remover_now >= 1:
|
||||
keyboard.add(
|
||||
ikb("⬅️", data=f"user_available_swipe:{remover_now - 1}"),
|
||||
)
|
||||
|
||||
keyboard.add(
|
||||
ikb(f"{remover_now + 1}/{remover_max}", data="...")
|
||||
)
|
||||
|
||||
if remover_now + 1 < remover_max:
|
||||
keyboard.add(
|
||||
ikb("➡️", data=f"user_available_swipe:{remover_now + 1}"),
|
||||
)
|
||||
|
||||
if remover_now + 1 < remover_max - 1 and remover_max >= 3:
|
||||
keyboard.add(
|
||||
ikb("⏩", data=f"user_available_swipe:{remover_max - 1}"),
|
||||
)
|
||||
|
||||
keyboard.adjust(5)
|
||||
|
||||
return keyboard.as_markup()
|
||||
@@ -0,0 +1,41 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
from aiogram.types import InlineKeyboardMarkup
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from tgbot.utils.const_functions import ikb
|
||||
|
||||
|
||||
# Открытие позиции для просмотра
|
||||
def products_open_finl(position_id: int, category_id: int, remover: int) -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardBuilder()
|
||||
|
||||
keyboard.row(
|
||||
ikb("💰 Купить товар", data=f"buy_item_open:{position_id}:{remover}"),
|
||||
).row(
|
||||
ikb("🔙 Вернуться", data=f"buy_category_open:{category_id}:{remover}"),
|
||||
)
|
||||
|
||||
return keyboard.as_markup()
|
||||
|
||||
|
||||
# Подтверждение покупки товара
|
||||
def products_buy_confirm_finl(position_id: int, category_id: int, get_count: int) -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardBuilder()
|
||||
|
||||
keyboard.row(
|
||||
ikb("✅ Подтвердить", data=f"buy_item_confirm:{position_id}:{get_count}"),
|
||||
ikb("❌ Отменить", data=f"buy_position_open:{position_id}:0"),
|
||||
)
|
||||
|
||||
return keyboard.as_markup()
|
||||
|
||||
|
||||
# Возврат к позиции при отмене ввода
|
||||
def products_return_finl(position_id: int, category_id: int) -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardBuilder()
|
||||
|
||||
keyboard.row(
|
||||
ikb("🔙 Вернуться", data=f"buy_position_open:{position_id}:0"),
|
||||
)
|
||||
|
||||
return keyboard.as_markup()
|
||||
@@ -0,0 +1,80 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
from aiogram.types import ReplyKeyboardMarkup
|
||||
from aiogram.utils.keyboard import ReplyKeyboardBuilder
|
||||
|
||||
from tgbot.data.config import get_admins
|
||||
from tgbot.utils.const_functions import rkb
|
||||
|
||||
|
||||
# Главное меню
|
||||
def menu_frep(user_id: int) -> ReplyKeyboardMarkup:
|
||||
keyboard = ReplyKeyboardBuilder()
|
||||
|
||||
keyboard.row(
|
||||
rkb("🎁 Купить"), rkb("👤 Профиль"), rkb("🧮 Наличие товаров"),
|
||||
).row(
|
||||
rkb("☎️ Поддержка"), rkb("❔ FAQ"),
|
||||
)
|
||||
|
||||
if user_id in get_admins():
|
||||
keyboard.row(
|
||||
rkb("🎁 Управление товарами"), rkb("📊 Статистика"),
|
||||
).row(
|
||||
rkb("⚙️ Настройки"), rkb("🔆 Общие функции"), rkb("🔑 Платежные системы"),
|
||||
)
|
||||
|
||||
return keyboard.as_markup(resize_keyboard=True)
|
||||
|
||||
|
||||
# Платежные системы
|
||||
def payments_frep() -> ReplyKeyboardMarkup:
|
||||
keyboard = ReplyKeyboardBuilder()
|
||||
|
||||
keyboard.row(
|
||||
rkb("🔷 CryptoBot"), rkb("🔮 ЮMoney"), rkb("⭐️ Звёзды"),
|
||||
).row(
|
||||
rkb("🔙 Главное меню"),
|
||||
)
|
||||
|
||||
return keyboard.as_markup(resize_keyboard=True)
|
||||
|
||||
|
||||
# Общие функции
|
||||
def functions_frep() -> ReplyKeyboardMarkup:
|
||||
keyboard = ReplyKeyboardBuilder()
|
||||
|
||||
keyboard.row(
|
||||
rkb("🔍 Поиск"), rkb("📢 Рассылка"),
|
||||
).row(
|
||||
rkb("🔙 Главное меню"),
|
||||
)
|
||||
|
||||
return keyboard.as_markup(resize_keyboard=True)
|
||||
|
||||
|
||||
# Настройки бота
|
||||
def settings_frep() -> ReplyKeyboardMarkup:
|
||||
keyboard = ReplyKeyboardBuilder()
|
||||
|
||||
keyboard.row(
|
||||
rkb("🖍 Изменить данные"), rkb("🕹 Выключатели"),
|
||||
).row(
|
||||
rkb("🔙 Главное меню"),
|
||||
)
|
||||
|
||||
return keyboard.as_markup(resize_keyboard=True)
|
||||
|
||||
|
||||
# Управление товарами
|
||||
def items_frep() -> ReplyKeyboardMarkup:
|
||||
keyboard = ReplyKeyboardBuilder()
|
||||
|
||||
keyboard.row(
|
||||
rkb("📁 Создать позицию ➕"), rkb("🗃 Создать категорию ➕"),
|
||||
).row(
|
||||
rkb("📁 Изменить позицию 🖍"), rkb("🗃 Изменить категорию 🖍"),
|
||||
).row(
|
||||
rkb("🔙 Главное меню"), rkb("🎁 Добавить товары ➕"), rkb("❌ Удаление"),
|
||||
)
|
||||
|
||||
return keyboard.as_markup(resize_keyboard=True)
|
||||
@@ -0,0 +1,20 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
from aiogram import Dispatcher
|
||||
|
||||
from tgbot.middlewares.middleware_throttling import ThrottlingMiddleware
|
||||
from tgbot.middlewares.middleware_user import ExistsUserMiddleware
|
||||
|
||||
|
||||
# Регистрация пользовательских middleware
|
||||
def register_all_middlewares(dp: Dispatcher):
|
||||
dp.callback_query.outer_middleware(ExistsUserMiddleware())
|
||||
dp.message.outer_middleware(ExistsUserMiddleware())
|
||||
|
||||
throttling = ThrottlingMiddleware()
|
||||
dp.message.middleware(throttling)
|
||||
dp.callback_query.middleware(throttling)
|
||||
|
||||
|
||||
# Совместимое имя для старых импортов
|
||||
def register_all_middlwares(dp: Dispatcher):
|
||||
register_all_middlewares(dp)
|
||||
@@ -0,0 +1,77 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
import time
|
||||
from typing import Any, Awaitable, Callable, Dict, Optional, Union
|
||||
|
||||
from aiogram import BaseMiddleware
|
||||
from aiogram.dispatcher.flags import get_flag
|
||||
from aiogram.types import CallbackQuery, Message, TelegramObject, User
|
||||
from cachetools import TTLCache
|
||||
|
||||
from tgbot.data.config import BOT_THROTTLE_RATE
|
||||
|
||||
|
||||
class ThrottlingMiddleware(BaseMiddleware):
|
||||
# Создание хранилищ лимитов
|
||||
def __init__(self, default_rate: Union[int, float] = BOT_THROTTLE_RATE) -> None:
|
||||
self.default_rate = default_rate
|
||||
self.message_users = TTLCache(maxsize=10_000, ttl=600)
|
||||
self.callback_users = TTLCache(maxsize=10_000, ttl=600)
|
||||
|
||||
# Ограничение частых сообщений и колбэков
|
||||
async def __call__(self, handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]], event: TelegramObject, data):
|
||||
this_user: Optional[User] = data.get("event_from_user")
|
||||
|
||||
if this_user is None:
|
||||
return await handler(event, data)
|
||||
|
||||
flag_rate = get_flag(data, "rate")
|
||||
rate = float(self.default_rate if flag_rate is None else flag_rate)
|
||||
|
||||
if rate == 0:
|
||||
return await handler(event, data)
|
||||
|
||||
now_time = time.monotonic()
|
||||
bucket = self._get_bucket(event)
|
||||
user_key = this_user.id
|
||||
|
||||
if user_key not in bucket:
|
||||
bucket[user_key] = {
|
||||
"last_throttled": now_time,
|
||||
"count_throttled": 0,
|
||||
"now_rate": rate,
|
||||
}
|
||||
|
||||
return await handler(event, data)
|
||||
|
||||
if now_time - bucket[user_key]["last_throttled"] >= bucket[user_key]["now_rate"]:
|
||||
bucket.pop(user_key)
|
||||
|
||||
return await handler(event, data)
|
||||
|
||||
bucket[user_key]["last_throttled"] = now_time
|
||||
bucket[user_key]["count_throttled"] += 1
|
||||
|
||||
if bucket[user_key]["count_throttled"] == 1:
|
||||
bucket[user_key]["now_rate"] = rate + 2
|
||||
await self._warn_user(event)
|
||||
elif bucket[user_key]["count_throttled"] == 2:
|
||||
bucket[user_key]["now_rate"] = rate + 3
|
||||
else:
|
||||
bucket[user_key]["now_rate"] = rate + 5
|
||||
|
||||
return None
|
||||
|
||||
# Выбор bucket под тип апдейта
|
||||
def _get_bucket(self, event: TelegramObject) -> TTLCache:
|
||||
if isinstance(event, CallbackQuery):
|
||||
return self.callback_users
|
||||
|
||||
return self.message_users
|
||||
|
||||
# Предупреждение пользователя о спаме
|
||||
@staticmethod
|
||||
async def _warn_user(event: TelegramObject) -> None:
|
||||
if isinstance(event, Message):
|
||||
await event.reply("<b>❗ Пожалуйста, не спамьте</b>")
|
||||
elif isinstance(event, CallbackQuery) or hasattr(event, "answer"):
|
||||
await event.answer("❗ Пожалуйста, не спамьте", cache_time=1)
|
||||
@@ -0,0 +1,50 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
from aiogram import BaseMiddleware
|
||||
from cachetools import TTLCache
|
||||
|
||||
from tgbot.data.config import BOT_USER_CACHE_TTL
|
||||
from tgbot.database import Userx
|
||||
from tgbot.utils.const_functions import clear_html
|
||||
|
||||
|
||||
class ExistsUserMiddleware(BaseMiddleware):
|
||||
# Создание кеша пользователей
|
||||
def __init__(self, cache_ttl: int = BOT_USER_CACHE_TTL) -> None:
|
||||
self.users = Userx()
|
||||
self.cache = TTLCache(maxsize=10_000, ttl=cache_ttl)
|
||||
|
||||
# Добавление или обновление пользователя перед handler-ом
|
||||
async def __call__(self, handler, event, data):
|
||||
this_user = data.get("event_from_user")
|
||||
|
||||
if this_user is not None and not this_user.is_bot:
|
||||
user_id = this_user.id
|
||||
user_login = (this_user.username or "").lower()
|
||||
user_name = clear_html(this_user.first_name) or ""
|
||||
user_surname = clear_html(this_user.last_name) or ""
|
||||
user_fullname = clear_html(this_user.full_name) or " ".join(filter(None, [user_name, user_surname]))
|
||||
|
||||
user_data = (
|
||||
user_login,
|
||||
user_name,
|
||||
user_surname,
|
||||
user_fullname,
|
||||
)
|
||||
|
||||
cached_user = self.cache.get(user_id)
|
||||
|
||||
if cached_user is None or cached_user["data"] != user_data:
|
||||
user = await self.users.upsert(
|
||||
user_id=user_id,
|
||||
user_login=user_data[0],
|
||||
user_name=user_data[1],
|
||||
user_surname=user_data[2],
|
||||
user_fullname=user_data[3],
|
||||
)
|
||||
self.cache[user_id] = {"data": user_data, "user": user}
|
||||
else:
|
||||
user = cached_user["user"]
|
||||
|
||||
data["User"] = user
|
||||
|
||||
return await handler(event, data)
|
||||
@@ -0,0 +1,53 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
from aiogram import Dispatcher
|
||||
|
||||
from tgbot.routers import main_errors, main_start, main_missed
|
||||
from tgbot.routers.admin import admin_menu, admin_functions, admin_payments, admin_products, admin_settings
|
||||
from tgbot.routers.user import user_menu, user_transactions, user_products
|
||||
from tgbot.utils.misc.bot_filters import IsAdmin, IsPrivate
|
||||
|
||||
|
||||
# Регистрация всех роутеров
|
||||
def register_all_routers(dp: Dispatcher):
|
||||
# Подключение фильтров
|
||||
main_errors.router.message.filter(IsPrivate())
|
||||
main_errors.router.callback_query.filter(IsPrivate())
|
||||
main_start.router.message.filter(IsPrivate())
|
||||
main_start.router.callback_query.filter(IsPrivate())
|
||||
main_missed.router.message.filter(IsPrivate())
|
||||
main_missed.router.callback_query.filter(IsPrivate())
|
||||
|
||||
user_menu.router.message.filter(IsPrivate())
|
||||
user_menu.router.callback_query.filter(IsPrivate())
|
||||
user_products.router.message.filter(IsPrivate())
|
||||
user_products.router.callback_query.filter(IsPrivate())
|
||||
user_transactions.router.message.filter(IsPrivate())
|
||||
user_transactions.router.callback_query.filter(IsPrivate())
|
||||
|
||||
admin_menu.router.message.filter(IsPrivate(), IsAdmin()) # Работа message роутера только для админов
|
||||
admin_menu.router.callback_query.filter(IsPrivate(), IsAdmin()) # Работа callback роутера только для админов
|
||||
admin_functions.router.message.filter(IsPrivate(), IsAdmin()) # Работа message роутера только для админов
|
||||
admin_functions.router.callback_query.filter(IsPrivate(), IsAdmin()) # Работа callback роутера только для админов
|
||||
admin_payments.router.message.filter(IsPrivate(), IsAdmin()) # Работа message роутера только для админов
|
||||
admin_payments.router.callback_query.filter(IsPrivate(), IsAdmin()) # Работа callback роутера только для админов
|
||||
admin_settings.router.message.filter(IsPrivate(), IsAdmin()) # Работа message роутера только для админов
|
||||
admin_settings.router.callback_query.filter(IsPrivate(), IsAdmin()) # Работа callback роутера только для админов
|
||||
admin_products.router.message.filter(IsPrivate(), IsAdmin()) # Работа message роутера только для админов
|
||||
admin_products.router.callback_query.filter(IsPrivate(), IsAdmin()) # Работа callback роутера только для админов
|
||||
|
||||
# Подключение обязательных роутеров
|
||||
dp.include_router(main_errors.router) # Роутер ошибки
|
||||
dp.include_router(main_start.router) # Роутер основных команд
|
||||
|
||||
# Подключение пользовательских роутеров (юзеров и админов)
|
||||
dp.include_router(user_menu.router) # Юзер роутер
|
||||
dp.include_router(admin_menu.router) # Админ роутер
|
||||
dp.include_router(user_products.router) # Юзер роутер
|
||||
dp.include_router(user_transactions.router) # Юзер роутер
|
||||
dp.include_router(admin_functions.router) # Админ роутер
|
||||
dp.include_router(admin_payments.router) # Админ роутер
|
||||
dp.include_router(admin_settings.router) # Админ роутер
|
||||
dp.include_router(admin_products.router) # Админ роутер
|
||||
|
||||
# Подключение обязательных роутеров
|
||||
dp.include_router(main_missed.router) # Роутер пропущенных апдейтов
|
||||
@@ -0,0 +1,327 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
import asyncio
|
||||
|
||||
from aiogram import Router, Bot, F
|
||||
from aiogram.filters import StateFilter
|
||||
from aiogram.types import CallbackQuery, Message
|
||||
|
||||
from tgbot.database import Purchasesx, Refillx, Userx
|
||||
from tgbot.keyboards.inline_admin import profile_edit_return_finl, mail_confirm_finl
|
||||
from tgbot.services.api_hosting_text import HostingAPI
|
||||
from tgbot.utils.const_functions import is_number, to_number, del_message, ded, clear_html, convert_date
|
||||
from tgbot.utils.misc.bot_logging import bot_logger
|
||||
from tgbot.utils.misc.bot_models import FSM, ARS
|
||||
from tgbot.utils.misc_functions import functions_mail_make
|
||||
from tgbot.utils.text_functions import open_profile_admin, refill_open_admin, purchase_open_admin
|
||||
|
||||
router = Router(name=__name__)
|
||||
|
||||
|
||||
# Поиск чеков и профилей
|
||||
@router.message(F.text == "🔍 Поиск")
|
||||
async def functions_find(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
await state.clear()
|
||||
|
||||
await state.set_state("here_find")
|
||||
await message.answer("<b>🔍 Отправьте айди/логин пользователя или номер чека</b>")
|
||||
|
||||
|
||||
# Рассылка
|
||||
@router.message(F.text == "📢 Рассылка")
|
||||
async def functions_mail(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
await state.clear()
|
||||
|
||||
await state.set_state("here_mail_message")
|
||||
await message.answer(
|
||||
"<b>📢 Отправьте пост для рассылки пользователям</b>\n"
|
||||
"❕ Поддерживаются посты с любыми медиафайлами",
|
||||
)
|
||||
|
||||
|
||||
################################################################################
|
||||
################################### РАССЫЛКА ###################################
|
||||
# Принятие текста для рассылки
|
||||
@router.message(StateFilter("here_mail_message"))
|
||||
async def functions_mail_get(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
await state.update_data(here_mail_message=message)
|
||||
await state.set_state("here_mail_confirm")
|
||||
|
||||
get_users = await Userx().get_all()
|
||||
|
||||
await message.reply(
|
||||
f"<b>📢 Отправить <code>{len(get_users)}</code> юзерам данный пост?</b>",
|
||||
reply_markup=mail_confirm_finl(),
|
||||
)
|
||||
|
||||
|
||||
# Подтверждение отправки рассылки
|
||||
@router.callback_query(F.data.startswith("mail_confirm:"), StateFilter("here_mail_confirm"))
|
||||
async def functions_mail_confirm(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
get_status = call.data.split(":")[1]
|
||||
|
||||
send_message = (await state.get_data())['here_mail_message']
|
||||
await state.clear()
|
||||
|
||||
if get_status == "Yes":
|
||||
get_users = await Userx().get_all()
|
||||
|
||||
await call.message.edit_text(f"<b>📢 Рассылка началась... (0/{len(get_users)})</b>")
|
||||
|
||||
await asyncio.create_task(functions_mail_make(bot, send_message, call))
|
||||
else:
|
||||
await call.message.edit_text("<b>📢 Вы отменили отправку рассылки ✅</b>")
|
||||
|
||||
|
||||
################################################################################
|
||||
##################################### ПОИСК ####################################
|
||||
# Принятие айди/логина пользователя или чека для поиска
|
||||
@router.message(F.text, StateFilter("here_find"))
|
||||
@router.message(F.text.lower().startswith(('.find', 'find')))
|
||||
async def functions_find_get(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
find_data = message.text.lower()
|
||||
|
||||
if ".find" in find_data or "find" in find_data:
|
||||
if len(find_data.split(" ")) >= 2:
|
||||
if ".find" in find_data or "find" in find_data:
|
||||
find_data = message.text.split(" ")[1]
|
||||
else:
|
||||
return await message.answer(
|
||||
"<b>❌ Вы не указали поисковые данные</b>\n"
|
||||
"🔍 Отправьте айди/логин пользователя или номер чека",
|
||||
)
|
||||
|
||||
if find_data.startswith("@") or find_data.startswith("#"):
|
||||
find_data = find_data[1:]
|
||||
|
||||
if find_data.isdigit():
|
||||
get_user = await Userx().get(user_id=find_data)
|
||||
else:
|
||||
get_user = await Userx().get(user_login=find_data.lower())
|
||||
|
||||
get_refill = await Refillx().get(refill_receipt=find_data)
|
||||
get_purchase = await Purchasesx().get(purchase_receipt=find_data)
|
||||
|
||||
if get_user is None and get_refill is None and get_purchase is None:
|
||||
return await message.answer(
|
||||
"<b>❌ Данные не были найдены</b>\n"
|
||||
"🔍 Отправьте айди/логин пользователя или номер чека",
|
||||
)
|
||||
|
||||
await state.clear()
|
||||
|
||||
if get_user is not None:
|
||||
return await open_profile_admin(bot, message.from_user.id, get_user)
|
||||
|
||||
if get_refill is not None:
|
||||
return await refill_open_admin(bot, message.from_user.id, get_refill)
|
||||
|
||||
if get_purchase is not None:
|
||||
return await purchase_open_admin(bot, arSession, message.from_user.id, get_purchase)
|
||||
|
||||
|
||||
################################################################################
|
||||
############################## УПРАВЛЕНИЕ ПРОФИЛЕМ #############################
|
||||
# Обновление профиля пользователя
|
||||
@router.callback_query(F.data.startswith("admin_user_refresh:"))
|
||||
async def functions_user_refresh(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
user_id = int(call.data.split(":")[1])
|
||||
|
||||
get_user = await Userx().get_required(user_id=user_id)
|
||||
|
||||
await state.clear()
|
||||
|
||||
await del_message(call.message)
|
||||
await open_profile_admin(bot, call.from_user.id, get_user)
|
||||
|
||||
|
||||
# Покупки пользователя
|
||||
@router.callback_query(F.data.startswith("admin_user_purchases:"))
|
||||
async def functions_user_purchases(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
user_id = int(call.data.split(":")[1])
|
||||
|
||||
get_user = await Userx().get_required(user_id=user_id)
|
||||
get_purchases = await Purchasesx().gets(user_id=user_id)
|
||||
get_purchases = get_purchases[-10:]
|
||||
|
||||
if len(get_purchases) < 1:
|
||||
return await call.answer("❗ У пользователя отсутствуют покупки", True)
|
||||
|
||||
await call.answer("🎁 Последние 10 покупок")
|
||||
await del_message(call.message)
|
||||
|
||||
for purchase in get_purchases:
|
||||
link_items = await (
|
||||
await HostingAPI.connect(
|
||||
bot=bot,
|
||||
arSession=arSession,
|
||||
)
|
||||
).upload_text(purchase.purchase_data)
|
||||
|
||||
await call.message.answer(
|
||||
ded(f"""
|
||||
<b>🧾 Чек: <code>#{purchase.purchase_receipt}</code></b>
|
||||
🎁 Товар: <code>{purchase.purchase_position_name} | {purchase.purchase_count}шт | {purchase.purchase_price}₽</code>
|
||||
🕰 Дата покупки: <code>{convert_date(purchase.purchase_unix)}</code>
|
||||
🔗 Товары: <a href='{link_items}'>кликабельно</a>
|
||||
"""),
|
||||
disable_web_page_preview=True,
|
||||
)
|
||||
|
||||
await asyncio.sleep(0.2)
|
||||
|
||||
await open_profile_admin(bot, call.from_user.id, get_user)
|
||||
|
||||
|
||||
# Выдача баланса пользователю
|
||||
@router.callback_query(F.data.startswith("admin_user_balance_add:"))
|
||||
async def functions_user_balance_add(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
user_id = int(call.data.split(":")[1])
|
||||
|
||||
await state.update_data(here_user=user_id)
|
||||
await state.set_state("here_user_add")
|
||||
|
||||
await call.message.edit_text(
|
||||
"<b>💰 Введите сумму для выдачи баланса</b>",
|
||||
reply_markup=profile_edit_return_finl(user_id),
|
||||
)
|
||||
|
||||
|
||||
# Принятие суммы для выдачи баланса пользователю
|
||||
@router.message(F.text, StateFilter("here_user_add"))
|
||||
async def functions_user_balance_add_get(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
user_id = (await state.get_data())['here_user']
|
||||
|
||||
if not is_number(message.text):
|
||||
return await message.answer(
|
||||
"<b>❌ Данные были введены неверно</b>\n"
|
||||
"💰 Введите сумму для выдачи баланса",
|
||||
reply_markup=profile_edit_return_finl(user_id),
|
||||
)
|
||||
|
||||
get_amount = to_number(message.text)
|
||||
|
||||
if get_amount <= 0 or get_amount > 1_000_000_000:
|
||||
return await message.answer(
|
||||
"<b>❌ Сумма выдачи не может быть меньше 1 и больше 1 000 000 000</b>\n"
|
||||
"💰 Введите сумму для выдачи баланса",
|
||||
reply_markup=profile_edit_return_finl(user_id),
|
||||
)
|
||||
|
||||
await state.clear()
|
||||
|
||||
get_user = await Userx().get_required(user_id=user_id)
|
||||
await Userx().update(
|
||||
user_id,
|
||||
user_balance=round(get_user.user_balance + get_amount, 2),
|
||||
user_give=round(get_user.user_give + get_amount, 2),
|
||||
)
|
||||
|
||||
try:
|
||||
await bot.send_message(
|
||||
user_id,
|
||||
f"<b>💰 Вам было выдано <code>{message.text}₽</code></b>",
|
||||
)
|
||||
except Exception:
|
||||
bot_logger.debug("Не удалось уведомить пользователя %s о выдаче баланса", user_id, exc_info=True)
|
||||
|
||||
await message.answer(
|
||||
f"👤 Пользователь: <a href='tg://user?id={get_user.user_id}'>{get_user.user_name}</a>\n"
|
||||
f"💰 Выдача баланса: <code>{message.text}₽</code> | <code>{get_user.user_balance}₽</code> -> <code>{round(get_user.user_balance + get_amount, 2)}₽</code>"
|
||||
)
|
||||
|
||||
get_user = await Userx().get_required(user_id=user_id)
|
||||
await open_profile_admin(bot, message.from_user.id, get_user)
|
||||
|
||||
|
||||
# Изменение баланса пользователю
|
||||
@router.callback_query(F.data.startswith("admin_user_balance_set:"))
|
||||
async def functions_user_balance_set(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
user_id = int(call.data.split(":")[1])
|
||||
|
||||
await state.update_data(here_user=user_id)
|
||||
await state.set_state("here_user_set")
|
||||
|
||||
await call.message.edit_text(
|
||||
"<b>💰 Введите сумму для изменения баланса</b>",
|
||||
reply_markup=profile_edit_return_finl(user_id),
|
||||
)
|
||||
|
||||
|
||||
# Принятие суммы для изменения баланса пользователя
|
||||
@router.message(F.text, StateFilter("here_user_set"))
|
||||
async def functions_user_balance_set_get(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
user_id = (await state.get_data())['here_user']
|
||||
|
||||
if not is_number(message.text):
|
||||
return await message.answer(
|
||||
"<b>❌ Данные были введены неверно</b>\n"
|
||||
"💰 Введите сумму для изменения баланса",
|
||||
reply_markup=profile_edit_return_finl(user_id),
|
||||
)
|
||||
|
||||
get_amount = to_number(message.text)
|
||||
|
||||
if get_amount < -1_000_000_000 or get_amount > 1_000_000_000:
|
||||
return await message.answer(
|
||||
"<b>❌ Сумма изменения не может быть больше или меньше (-)1 000 000 000</b>\n"
|
||||
"💰 Введите сумму для изменения баланса",
|
||||
reply_markup=profile_edit_return_finl(user_id),
|
||||
)
|
||||
|
||||
await state.clear()
|
||||
|
||||
get_user = await Userx().get_required(user_id=user_id)
|
||||
|
||||
if get_amount > get_user.user_balance:
|
||||
user_give = get_amount - get_user.user_give
|
||||
else:
|
||||
user_give = 0
|
||||
|
||||
await Userx().update(
|
||||
user_id,
|
||||
user_balance=get_amount,
|
||||
user_give=round(get_user.user_give + user_give, 2),
|
||||
)
|
||||
|
||||
await message.answer(
|
||||
f"👤 Пользователь: <a href='tg://user?id={get_user.user_id}'>{get_user.user_name}</a>\n"
|
||||
f"💰 Установка баланса: <code>{message.text}₽</code> | <code>{get_user.user_balance}₽</code> -> <code>{get_amount}₽</code>"
|
||||
)
|
||||
|
||||
get_user = await Userx().get_required(user_id=user_id)
|
||||
await open_profile_admin(bot, message.from_user.id, get_user)
|
||||
|
||||
|
||||
# Отправка сообщения пользователю
|
||||
@router.callback_query(F.data.startswith("admin_user_message:"))
|
||||
async def functions_user_user_message(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
user_id = int(call.data.split(":")[1])
|
||||
|
||||
await state.update_data(here_user_id=user_id)
|
||||
await state.set_state("here_user_message")
|
||||
|
||||
await call.message.edit_text(
|
||||
"<b>💌 Введите сообщение для отправки</b>\n"
|
||||
"⚠️ Сообщение будет сразу отправлено пользователю.",
|
||||
reply_markup=profile_edit_return_finl(user_id),
|
||||
)
|
||||
|
||||
|
||||
# Принятие сообщения для отправки пользователю
|
||||
@router.message(F.text, StateFilter("here_user_message"))
|
||||
async def functions_user_user_message_get(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
user_id = (await state.get_data())['here_user_id']
|
||||
await state.clear()
|
||||
|
||||
get_message = "<b>💌 Сообщение от администратора:</b>\n" + f"<code>{clear_html(message.text)}</code>"
|
||||
get_user = await Userx().get_required(user_id=user_id)
|
||||
|
||||
try:
|
||||
await bot.send_message(user_id, get_message)
|
||||
except Exception:
|
||||
bot_logger.debug("Не удалось отправить сообщение пользователю %s", user_id, exc_info=True)
|
||||
await message.reply("<b>❌ Не удалось отправить сообщение</b>")
|
||||
else:
|
||||
await message.reply("<b>✅ Сообщение было успешно доставлено</b>")
|
||||
|
||||
await open_profile_admin(bot, message.from_user.id, get_user)
|
||||
@@ -0,0 +1,120 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
import os
|
||||
|
||||
import aiofiles
|
||||
from aiogram import Router, Bot, F
|
||||
from aiogram.filters import Command
|
||||
from aiogram.types import Message, FSInputFile
|
||||
from aiogram.utils.media_group import MediaGroupBuilder
|
||||
|
||||
from tgbot.data.config import PATH_LOGS, PATH_DATABASE
|
||||
from tgbot.keyboards.reply_main import payments_frep, settings_frep, functions_frep, items_frep
|
||||
from tgbot.utils.const_functions import get_date
|
||||
from tgbot.utils.misc.bot_models import FSM, ARS
|
||||
from tgbot.utils.misc_functions import get_statistics
|
||||
|
||||
router = Router(name=__name__)
|
||||
|
||||
|
||||
# Платежные системы
|
||||
@router.message(F.text == "🔑 Платежные системы")
|
||||
async def admin_payments(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
await state.clear()
|
||||
|
||||
await message.answer(
|
||||
"<b>🔑 Настройка платежных системы</b>",
|
||||
reply_markup=payments_frep(),
|
||||
)
|
||||
|
||||
|
||||
# Настройки бота
|
||||
@router.message(F.text == "⚙️ Настройки")
|
||||
async def admin_settings(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
await state.clear()
|
||||
|
||||
await message.answer(
|
||||
"<b>⚙️ Основные настройки бота</b>",
|
||||
reply_markup=settings_frep(),
|
||||
)
|
||||
|
||||
|
||||
# Общие функции
|
||||
@router.message(F.text == "🔆 Общие функции")
|
||||
async def admin_functions(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
await state.clear()
|
||||
|
||||
await message.answer(
|
||||
"<b>🔆 Общие функции бота</b>",
|
||||
reply_markup=functions_frep(),
|
||||
)
|
||||
|
||||
|
||||
# Управление товарами
|
||||
@router.message(F.text == "🎁 Управление товарами")
|
||||
async def admin_products(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
await state.clear()
|
||||
|
||||
await message.answer(
|
||||
"<b>🎁 Редактирование товаров</b>",
|
||||
reply_markup=items_frep(),
|
||||
)
|
||||
|
||||
|
||||
# Статистика бота
|
||||
@router.message(F.text == "📊 Статистика")
|
||||
async def admin_statistics(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
await state.clear()
|
||||
|
||||
await message.answer(await get_statistics())
|
||||
|
||||
|
||||
# Получение базы данных
|
||||
@router.message(Command(commands=['db', 'database']))
|
||||
async def admin_database(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
await state.clear()
|
||||
|
||||
await message.answer_document(
|
||||
FSInputFile(PATH_DATABASE),
|
||||
caption=f"<b>📦 #BACKUP | <code>{get_date()}</code></b>",
|
||||
)
|
||||
|
||||
|
||||
# Получение логов
|
||||
@router.message(Command(commands=['log', 'logs']))
|
||||
async def admin_log(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
await state.clear()
|
||||
|
||||
media_group = MediaGroupBuilder(
|
||||
caption=f"<b>🖨 #LOGS | <code>{get_date()}</code></b>",
|
||||
)
|
||||
|
||||
if os.path.isfile(PATH_LOGS):
|
||||
media_group.add_document(media=FSInputFile(PATH_LOGS))
|
||||
|
||||
if os.path.isfile("tgbot/data/sv_log_err.log"):
|
||||
media_group.add_document(media=FSInputFile("tgbot/data/sv_log_err.log"))
|
||||
|
||||
if os.path.isfile("tgbot/data/sv_log_out.log"):
|
||||
media_group.add_document(media=FSInputFile("tgbot/data/sv_log_out.log"))
|
||||
|
||||
await message.answer_media_group(media=media_group.build())
|
||||
|
||||
|
||||
# Очистка логов
|
||||
@router.message(Command(commands=['clear_log', 'clear_logs', 'log_clear', 'logs_clear']))
|
||||
async def admin_log_clear(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
await state.clear()
|
||||
|
||||
if os.path.isfile(PATH_LOGS):
|
||||
async with aiofiles.open(PATH_LOGS, "w") as file:
|
||||
await file.write(f"{get_date()} | LOGS WAS CLEAR")
|
||||
|
||||
if os.path.isfile("tgbot/data/sv_log_err.log"):
|
||||
async with aiofiles.open("tgbot/data/sv_log_err.log", "w") as file:
|
||||
await file.write(f"{get_date()} | LOGS ERR WAS CLEAR")
|
||||
|
||||
if os.path.isfile("tgbot/data/sv_log_out.log"):
|
||||
async with aiofiles.open("tgbot/data/sv_log_out.log", "w") as file:
|
||||
await file.write(f"{get_date()} | LOGS OUT WAS CLEAR")
|
||||
|
||||
await message.answer("<b>🖨 Логи были успешно очищены</b>")
|
||||
@@ -0,0 +1,355 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
from aiogram import Router, Bot, F
|
||||
from aiogram.filters import StateFilter
|
||||
from aiogram.types import CallbackQuery, Message
|
||||
|
||||
from tgbot.database import Paymentsx
|
||||
from tgbot.keyboards.inline_admin import payment_yoomoney_finl, close_finl, payment_cryptobot_finl, payment_stars_finl
|
||||
from tgbot.services.api_cryptobot import CryptobotAPI
|
||||
from tgbot.services.api_stars import StarsAPI
|
||||
from tgbot.services.api_yoomoney import YoomoneyAPI
|
||||
from tgbot.utils.const_functions import ded, is_number, to_number
|
||||
from tgbot.utils.misc.bot_logging import bot_logger
|
||||
from tgbot.utils.misc.bot_models import FSM, ARS
|
||||
|
||||
router = Router(name=__name__)
|
||||
|
||||
|
||||
# Управление - CryptoBot
|
||||
@router.message(F.text == "🔷 CryptoBot")
|
||||
async def payments_cryptobot_open(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
await state.clear()
|
||||
|
||||
await message.answer(
|
||||
"<b>🔷 Управление - CryptoBot</b>",
|
||||
reply_markup=await payment_cryptobot_finl(),
|
||||
)
|
||||
|
||||
|
||||
# Управление - ЮMoney
|
||||
@router.message(F.text == "🔮 ЮMoney")
|
||||
async def payments_yoomoney_open(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
await state.clear()
|
||||
|
||||
await message.answer(
|
||||
"<b>🔮 Управление - ЮMoney</b>",
|
||||
reply_markup=await payment_yoomoney_finl(),
|
||||
)
|
||||
|
||||
|
||||
# Управление - Звёзды
|
||||
@router.message(F.text == "⭐️ Звёзды")
|
||||
async def payments_stars_open(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
await state.clear()
|
||||
|
||||
await message.answer(
|
||||
"<b>⭐️ Управление - Звёзды</b>",
|
||||
reply_markup=await payment_stars_finl(),
|
||||
)
|
||||
|
||||
|
||||
################################################################################
|
||||
################################### CRYPTOBOT ##################################
|
||||
# Баланс - CryptoBot
|
||||
@router.callback_query(F.data == "payment_cryptobot_balance")
|
||||
async def payments_cryptobot_balance(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
response = await (
|
||||
await CryptobotAPI.connect(
|
||||
bot=bot,
|
||||
arSession=arSession,
|
||||
update=call,
|
||||
skipping_error=True,
|
||||
)
|
||||
).balance()
|
||||
|
||||
await call.message.answer(
|
||||
response,
|
||||
reply_markup=close_finl(),
|
||||
)
|
||||
|
||||
|
||||
# Информация - CryptoBot
|
||||
@router.callback_query(F.data == "payment_cryptobot_check")
|
||||
async def payments_cryptobot_check(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
status, response = await (
|
||||
await CryptobotAPI.connect(
|
||||
bot=bot,
|
||||
arSession=arSession,
|
||||
update=call,
|
||||
skipping_error=True,
|
||||
)
|
||||
).check()
|
||||
|
||||
await call.message.answer(
|
||||
response,
|
||||
reply_markup=close_finl(),
|
||||
)
|
||||
|
||||
|
||||
# Изменение - CryptoBot
|
||||
@router.callback_query(F.data == "payment_cryptobot_edit")
|
||||
async def payments_cryptobot_edit(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
await state.set_state("here_cryptobot_token")
|
||||
await call.message.edit_text(
|
||||
ded(f"""
|
||||
<b>🔷 Изменение @CryptoBot кошелька - <a href='https://teletype.in/@djimbox/djimboshop-cryptobot'>Инструкция</a></b>
|
||||
➖➖➖➖➖➖➖➖➖➖
|
||||
▪️ Создайте Приложение в "Crypto Pay" и отправьте токен
|
||||
"""),
|
||||
disable_web_page_preview=True,
|
||||
)
|
||||
|
||||
|
||||
# Выключатель - CryptoBot
|
||||
@router.callback_query(F.data.startswith("payment_cryptobot_status:"))
|
||||
async def payments_cryptobot_status(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
get_status = call.data.split(":")[1]
|
||||
|
||||
get_payments = await Paymentsx().get()
|
||||
|
||||
if get_status == "True" and get_payments.cryptobot_token == "None":
|
||||
return await call.answer("❌ Токен данной платежной системы не был добавлен", True)
|
||||
|
||||
await Paymentsx().update(status_cryptobot=get_status)
|
||||
|
||||
await call.message.edit_text(
|
||||
"<b>🔷 Управление - CryptoBot</b>",
|
||||
reply_markup=await payment_cryptobot_finl(),
|
||||
)
|
||||
|
||||
|
||||
############################## ПРИНЯТИЕ CRYPTOBOT ##############################
|
||||
# Принятие токена Cryptobot
|
||||
@router.message(StateFilter("here_cryptobot_token"))
|
||||
async def payments_cryptobot_get(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
get_token = message.text
|
||||
|
||||
await state.clear()
|
||||
|
||||
cache_message = await message.answer("<b>🔷 Проверка введённых CryptoBot данных... 🔄</b>")
|
||||
|
||||
status, response = await (
|
||||
await CryptobotAPI.connect(
|
||||
bot=bot,
|
||||
arSession=arSession,
|
||||
update=message,
|
||||
skipping_error=True,
|
||||
token=get_token,
|
||||
)
|
||||
).check()
|
||||
|
||||
if status:
|
||||
await Paymentsx().update(cryptobot_token=get_token)
|
||||
await cache_message.edit_text("<b>🔷 CryptoBot кошелёк был успешно изменён ✅</b>")
|
||||
else:
|
||||
await cache_message.edit_text("<b>🔷 Не удалось изменить CryptoBot кошелёк ❌</b>")
|
||||
|
||||
await message.answer(
|
||||
"<b>🔷 Управление - CryptoBot</b>",
|
||||
reply_markup=await payment_cryptobot_finl(),
|
||||
)
|
||||
|
||||
|
||||
################################################################################
|
||||
#################################### ЮMoney ####################################
|
||||
# Баланс - ЮMoney
|
||||
@router.callback_query(F.data == "payment_yoomoney_balance")
|
||||
async def payments_yoomoney_balance(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
response = await (
|
||||
await YoomoneyAPI.connect(
|
||||
bot=bot,
|
||||
arSession=arSession,
|
||||
update=call,
|
||||
skipping_error=True,
|
||||
)
|
||||
).balance()
|
||||
|
||||
await call.message.answer(
|
||||
response,
|
||||
reply_markup=close_finl(),
|
||||
)
|
||||
|
||||
|
||||
# Информация - ЮMoney
|
||||
@router.callback_query(F.data == "payment_yoomoney_check")
|
||||
async def payments_yoomoney_check(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
response = await (
|
||||
await YoomoneyAPI.connect(
|
||||
bot=bot,
|
||||
arSession=arSession,
|
||||
update=call,
|
||||
skipping_error=True,
|
||||
)
|
||||
).check()
|
||||
|
||||
await call.message.answer(
|
||||
response,
|
||||
reply_markup=close_finl(),
|
||||
)
|
||||
|
||||
|
||||
# Изменение - ЮMoney
|
||||
@router.callback_query(F.data == "payment_yoomoney_edit")
|
||||
async def payments_yoomoney_edit(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
response = await (
|
||||
await YoomoneyAPI.connect(
|
||||
bot=bot,
|
||||
arSession=arSession,
|
||||
)
|
||||
).authorization_get()
|
||||
|
||||
await state.set_state("here_yoomoney_token")
|
||||
await call.message.edit_text(
|
||||
ded(f"""
|
||||
<b>🔮 Изменение ЮMoney кошелька - <a href='https://teletype.in/@djimbox/djimboshop-yoomoney'>Инструкция</a></b>
|
||||
➖➖➖➖➖➖➖➖➖➖
|
||||
▪️ Отправьте ссылку/код из адресной строки
|
||||
▪️ {response}
|
||||
"""),
|
||||
disable_web_page_preview=True,
|
||||
)
|
||||
|
||||
|
||||
# Выключатель - ЮMoney
|
||||
@router.callback_query(F.data.startswith("payment_yoomoney_status:"))
|
||||
async def payments_yoomoney_status(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
get_status = call.data.split(":")[1]
|
||||
|
||||
get_payments = await Paymentsx().get()
|
||||
|
||||
if get_status == "True" and get_payments.yoomoney_token == "None":
|
||||
return await call.answer("❌ Токен данной платежной системы не был добавлен", True)
|
||||
|
||||
await Paymentsx().update(status_yoomoney=get_status)
|
||||
|
||||
await call.message.edit_text(
|
||||
"<b>🔮 Управление - ЮMoney</b>",
|
||||
reply_markup=await payment_yoomoney_finl(),
|
||||
)
|
||||
|
||||
|
||||
################################ ПРИНЯТИЕ ЮMONEY ###############################
|
||||
# Принятие токена ЮMoney
|
||||
@router.message(StateFilter("here_yoomoney_token"))
|
||||
async def payments_yoomoney_get(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
get_code = message.text
|
||||
|
||||
try:
|
||||
get_code = get_code[get_code.index("code=") + 5:].replace(" ", "")
|
||||
except Exception:
|
||||
bot_logger.debug("ЮMoney code= не найден в сообщении, пробую использовать текст как код", exc_info=True)
|
||||
|
||||
cache_message = await message.answer("<b>🔮 Проверка введённых ЮMoney данных... 🔄</b>")
|
||||
|
||||
status, token, response = await (
|
||||
await YoomoneyAPI.connect(
|
||||
bot=bot,
|
||||
arSession=arSession,
|
||||
)
|
||||
).authorization_enter(str(get_code))
|
||||
|
||||
if status:
|
||||
await Paymentsx().update(yoomoney_token=token)
|
||||
|
||||
await cache_message.edit_text(response)
|
||||
|
||||
await state.clear()
|
||||
await message.answer(
|
||||
"<b>🔮 Управление - ЮMoney</b>",
|
||||
reply_markup=await payment_yoomoney_finl(),
|
||||
)
|
||||
|
||||
|
||||
################################################################################
|
||||
#################################### ЗВЁЗДЫ ####################################
|
||||
# Баланс - Звёзды
|
||||
@router.callback_query(F.data == "payment_stars_balance")
|
||||
async def payments_stars_balance(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
response = await (
|
||||
await StarsAPI.connect(
|
||||
bot=bot,
|
||||
arSession=arSession,
|
||||
update=call,
|
||||
skipping_error=True,
|
||||
)
|
||||
).balance()
|
||||
|
||||
await call.message.answer(
|
||||
response,
|
||||
reply_markup=close_finl(),
|
||||
)
|
||||
|
||||
|
||||
# Информация - Звёзды
|
||||
@router.callback_query(F.data == "payment_stars_check")
|
||||
async def payments_stars_check(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
status, response = await (
|
||||
await StarsAPI.connect(
|
||||
bot=bot,
|
||||
arSession=arSession,
|
||||
update=call,
|
||||
skipping_error=True,
|
||||
)
|
||||
).check()
|
||||
|
||||
await call.message.answer(
|
||||
response,
|
||||
reply_markup=close_finl(),
|
||||
)
|
||||
|
||||
|
||||
# Изменение - Звёзды
|
||||
@router.callback_query(F.data == "payment_stars_edit")
|
||||
async def payments_stars_edit(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
get_payments = await Paymentsx().get()
|
||||
|
||||
await state.set_state("here_stars_course")
|
||||
await call.message.edit_text(
|
||||
ded(f"""
|
||||
<b>⭐️ Изменение курса Telegram Stars</b>
|
||||
➖➖➖➖➖➖➖➖➖➖
|
||||
▪️ Текущий курс: <code>1 ⭐️ = {get_payments.stars_course}₽</code>
|
||||
▪️ Отправьте новый курс в рублях за одну звезду
|
||||
"""),
|
||||
disable_web_page_preview=True,
|
||||
)
|
||||
|
||||
|
||||
# Выключатель - Звёзды
|
||||
@router.callback_query(F.data.startswith("payment_stars_status:"))
|
||||
async def payments_stars_status(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
get_status = call.data.split(":")[1]
|
||||
|
||||
await Paymentsx().update(status_stars=get_status)
|
||||
|
||||
await call.message.edit_text(
|
||||
"<b>⭐️ Управление - Звёзды</b>",
|
||||
reply_markup=await payment_stars_finl(),
|
||||
)
|
||||
|
||||
|
||||
############################# ПРИНЯТИЕ КУРСА ЗВЁЗД #############################
|
||||
# Принятие курса Telegram Stars
|
||||
@router.message(StateFilter("here_stars_course"))
|
||||
async def payments_stars_course_get(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
if not is_number(message.text):
|
||||
return await message.answer(
|
||||
"<b>❌ Введите число. Например: <code>1.5</code></b>"
|
||||
)
|
||||
|
||||
stars_course = float(to_number(message.text))
|
||||
|
||||
if stars_course <= 0:
|
||||
return await message.answer("<b>❌ Курс должен быть больше нуля</b>")
|
||||
|
||||
await Paymentsx().update(stars_course=stars_course)
|
||||
await state.clear()
|
||||
|
||||
await message.answer(
|
||||
f"<b>⭐️ Курс Telegram Stars изменён: <code>1 ⭐️ = {stars_course}₽</code></b>",
|
||||
)
|
||||
|
||||
await message.answer(
|
||||
"<b>⭐️ Управление - Звёзды</b>",
|
||||
reply_markup=await payment_stars_finl(),
|
||||
)
|
||||
@@ -0,0 +1,969 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
from aiogram import Router, Bot, F
|
||||
from aiogram.filters import StateFilter
|
||||
from aiogram.types import CallbackQuery, Message, ReactionTypeEmoji
|
||||
|
||||
from tgbot.database import Categoryx, Itemx, Positionx, Settingsx
|
||||
from tgbot.keyboards.inline_admin import close_finl
|
||||
from tgbot.keyboards.inline_admin_page import (
|
||||
category_edit_swipe_fp,
|
||||
position_add_swipe_fp,
|
||||
position_edit_category_swipe_fp,
|
||||
position_edit_swipe_fp,
|
||||
item_add_position_swipe_fp,
|
||||
item_add_category_swipe_fp,
|
||||
item_delete_swipe_fp,
|
||||
)
|
||||
from tgbot.keyboards.inline_admin_products import (
|
||||
category_edit_delete_finl,
|
||||
position_edit_clear_finl,
|
||||
position_edit_delete_finl,
|
||||
position_edit_cancel_finl,
|
||||
category_edit_cancel_finl,
|
||||
products_removes_finl,
|
||||
products_removes_categories_finl,
|
||||
products_removes_positions_finl,
|
||||
products_removes_items_finl,
|
||||
item_add_finish_finl,
|
||||
)
|
||||
from tgbot.services.api_discord import DiscordAPI
|
||||
from tgbot.services.api_hosting_text import HostingAPI
|
||||
from tgbot.utils.const_functions import clear_list, is_number, to_number, del_message, ded, clear_html, gen_id
|
||||
from tgbot.utils.misc.bot_logging import bot_logger
|
||||
from tgbot.utils.misc.bot_models import FSM, ARS
|
||||
from tgbot.utils.text_functions import category_open_admin, position_open_admin, item_open_admin
|
||||
|
||||
router = Router(name=__name__)
|
||||
|
||||
|
||||
# Создание новой категории
|
||||
@router.message(F.text == "🗃 Создать категорию ➕")
|
||||
async def prod_category_add(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
await state.clear()
|
||||
|
||||
await state.set_state("here_category_name")
|
||||
await message.answer("<b>🗃 Введите название для категории</b>")
|
||||
|
||||
|
||||
# Выбор категории для редактирования
|
||||
@router.message(F.text == "🗃 Изменить категорию 🖍")
|
||||
async def prod_category_edit(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
await state.clear()
|
||||
|
||||
get_categories = await Categoryx().get_all()
|
||||
|
||||
if len(get_categories) >= 1:
|
||||
await message.answer(
|
||||
"<b>🗃 Выберите категорию для изменения 🖍</b>",
|
||||
reply_markup=await category_edit_swipe_fp(0),
|
||||
)
|
||||
else:
|
||||
await message.answer("<b>❌ Отсутствуют категории для изменения категорий</b>")
|
||||
|
||||
|
||||
# Создание новой позиции
|
||||
@router.message(F.text == "📁 Создать позицию ➕")
|
||||
async def prod_position_add(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
await state.clear()
|
||||
|
||||
get_categories = await Categoryx().get_all()
|
||||
|
||||
if len(get_categories) >= 1:
|
||||
await message.answer(
|
||||
"<b>📁 Выберите категорию для позиции ➕</b>",
|
||||
reply_markup=await position_add_swipe_fp(0),
|
||||
)
|
||||
else:
|
||||
await message.answer("<b>❌ Отсутствуют категории для создания позиции</b>")
|
||||
|
||||
|
||||
# Выбор позиции для редактирования
|
||||
@router.message(F.text == "📁 Изменить позицию 🖍")
|
||||
async def prod_position_edit(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
await state.clear()
|
||||
|
||||
get_categories = await Categoryx().get_all()
|
||||
|
||||
if len(get_categories) >= 1:
|
||||
await message.answer(
|
||||
"<b>📁 Выберите позицию для изменения 🖍</b>",
|
||||
reply_markup=await position_edit_category_swipe_fp(0),
|
||||
)
|
||||
else:
|
||||
await message.answer("<b>❌ Отсутствуют категории для изменения позиций</b>")
|
||||
|
||||
|
||||
# Страницы товаров для добавления
|
||||
@router.message(F.text == "🎁 Добавить товары ➕")
|
||||
async def prod_item_add(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
await state.clear()
|
||||
|
||||
get_categories = await Categoryx().get_all()
|
||||
|
||||
if len(get_categories) >= 1:
|
||||
await message.answer(
|
||||
"<b>🎁 Выберите позицию для товаров ➕</b>",
|
||||
reply_markup=await item_add_category_swipe_fp(0),
|
||||
)
|
||||
else:
|
||||
await message.answer("<b>❌ Отсутствуют позиции для добавления товара</b>")
|
||||
|
||||
|
||||
# Удаление категорий, позиций или товаров
|
||||
@router.message(F.text == "❌ Удаление")
|
||||
async def prod_removes(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
await state.clear()
|
||||
|
||||
await message.answer(
|
||||
"<b>🎁 Выберите раздел который хотите удалить ❌</b>\n",
|
||||
reply_markup=products_removes_finl(),
|
||||
)
|
||||
|
||||
|
||||
################################################################################
|
||||
############################### СОЗДАНИЕ КАТЕГОРИИ #############################
|
||||
# Принятие названия категории для её создания
|
||||
@router.message(F.text, StateFilter('here_category_name'))
|
||||
async def prod_category_add_name_get(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
if len(message.text) > 50:
|
||||
return await message.answer(
|
||||
"<b>❌ Название не может превышать 50 символов</b>\n"
|
||||
"🗃 Введите название для категории",
|
||||
)
|
||||
|
||||
await state.clear()
|
||||
|
||||
category_id = gen_id(12)
|
||||
await Categoryx().add(category_id, clear_html(message.text))
|
||||
|
||||
await category_open_admin(bot, message.from_user.id, category_id, 0)
|
||||
|
||||
|
||||
################################################################################
|
||||
############################### ИЗМЕНЕНИЕ КАТЕГОРИИ ############################
|
||||
# Страница выбора категорий для редактирования
|
||||
@router.callback_query(F.data.startswith("category_edit_swipe:"))
|
||||
async def prod_category_edit_swipe(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
remover = int(call.data.split(":")[1])
|
||||
|
||||
await call.message.edit_text(
|
||||
"<b>🗃 Выберите категорию для изменения 🖍</b>",
|
||||
reply_markup=await category_edit_swipe_fp(remover),
|
||||
)
|
||||
|
||||
|
||||
# Выбор текущей категории для редактирования
|
||||
@router.callback_query(F.data.startswith("category_edit_open:"))
|
||||
async def prod_category_edit_open(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
category_id = int(call.data.split(":")[1])
|
||||
remover = int(call.data.split(":")[2])
|
||||
|
||||
await state.clear()
|
||||
|
||||
await del_message(call.message)
|
||||
await category_open_admin(bot, call.from_user.id, category_id, remover)
|
||||
|
||||
|
||||
############################ САМО ИЗМЕНЕНИЕ КАТЕГОРИИ ##########################
|
||||
# Изменение названия категории
|
||||
@router.callback_query(F.data.startswith("category_edit_name:"))
|
||||
async def prod_category_edit_name(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
category_id = int(call.data.split(":")[1])
|
||||
remover = int(call.data.split(":")[2])
|
||||
|
||||
await state.update_data(here_category_id=category_id)
|
||||
await state.update_data(here_remover=remover)
|
||||
await state.set_state("here_category_edit_name")
|
||||
|
||||
await del_message(call.message)
|
||||
|
||||
await call.message.answer(
|
||||
"<b>🗃 Введите новое название для категории</b>",
|
||||
reply_markup=category_edit_cancel_finl(category_id, remover),
|
||||
)
|
||||
|
||||
|
||||
# Принятие нового названия для категории
|
||||
@router.message(F.text, StateFilter('here_category_edit_name'))
|
||||
async def prod_category_edit_name_get(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
category_id = (await state.get_data())['here_category_id']
|
||||
remover = (await state.get_data())['here_remover']
|
||||
|
||||
if len(message.text) > 50:
|
||||
return await message.answer(
|
||||
"<b>❌ Название не может превышать 50 символов</b>\n"
|
||||
"🗃 Введите новое название для категории",
|
||||
reply_markup=category_edit_cancel_finl(category_id, remover),
|
||||
)
|
||||
|
||||
await state.clear()
|
||||
|
||||
await Categoryx().update(category_id, category_name=clear_html(message.text))
|
||||
await category_open_admin(bot, message.from_user.id, category_id, remover)
|
||||
|
||||
|
||||
# Удаление категории
|
||||
@router.callback_query(F.data.startswith("category_edit_delete:"))
|
||||
async def prod_category_edit_delete(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
category_id = int(call.data.split(":")[1])
|
||||
remover = int(call.data.split(":")[2])
|
||||
|
||||
await call.message.edit_text(
|
||||
"<b>❗ Вы действительно хотите удалить категорию и все её данные?</b>",
|
||||
reply_markup=category_edit_delete_finl(category_id, remover),
|
||||
)
|
||||
|
||||
|
||||
# Подтверждение удаления категории
|
||||
@router.callback_query(F.data.startswith("category_edit_delete_confirm:"))
|
||||
async def prod_category_edit_delete_confirm(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
category_id = int(call.data.split(":")[1])
|
||||
remover = int(call.data.split(":")[2])
|
||||
|
||||
await Categoryx().delete(category_id=category_id)
|
||||
await Positionx().delete(category_id=category_id)
|
||||
await Itemx().delete(category_id=category_id)
|
||||
|
||||
await call.answer("🗃 Категория и все её данные были успешно удалены ✅", True)
|
||||
|
||||
get_categories = await Categoryx().get_all()
|
||||
|
||||
if len(get_categories) >= 1:
|
||||
await call.message.edit_text(
|
||||
"<b>🗃 Выберите категорию для изменения 🖍</b>",
|
||||
reply_markup=await category_edit_swipe_fp(remover),
|
||||
)
|
||||
else:
|
||||
await del_message(call.message)
|
||||
|
||||
|
||||
################################################################################
|
||||
############################### ДОБАВЛЕНИЕ ПОЗИЦИИ #############################
|
||||
# Cтраницы выбора категорий для расположения позиции
|
||||
@router.callback_query(F.data.startswith("position_add_swipe:"))
|
||||
async def prod_position_add_swipe(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
remover = int(call.data.split(":")[1])
|
||||
|
||||
await call.message.edit_text(
|
||||
"<b>📁 Выберите категорию для позиции ➕</b>",
|
||||
reply_markup=await position_add_swipe_fp(remover),
|
||||
)
|
||||
|
||||
|
||||
# Выбор категории для создания позиции
|
||||
@router.callback_query(F.data.startswith("position_add_open:"))
|
||||
async def prod_position_add_open(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
category_id = int(call.data.split(":")[1])
|
||||
|
||||
await state.update_data(here_category_id=category_id)
|
||||
await state.set_state("here_position_name")
|
||||
|
||||
await call.message.edit_text("<b>📁 Введите название для позиции</b>")
|
||||
|
||||
|
||||
# Принятие названия для создания позиции
|
||||
@router.message(F.text, StateFilter('here_position_name'))
|
||||
async def prod_position_add_name_get(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
if len(message.text) > 50:
|
||||
return await message.answer(
|
||||
"<b>❌ Название не может превышать 50 символов</b>\n"
|
||||
"📁 Введите название для позиции",
|
||||
)
|
||||
|
||||
await state.update_data(here_position_name=clear_html(message.text))
|
||||
await state.set_state("here_position_price")
|
||||
|
||||
await message.answer("<b>📁 Введите цену для позиции</b>")
|
||||
|
||||
|
||||
# Принятие цены позиции для её создания
|
||||
@router.message(F.text, StateFilter('here_position_price'))
|
||||
async def prod_position_add_price_get(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
if not is_number(message.text):
|
||||
return await message.answer(
|
||||
"<b>❌ Данные были введены неверно</b>\n"
|
||||
"📁 Введите цену для позиции",
|
||||
)
|
||||
|
||||
if to_number(message.text) > 10_000_000 or to_number(message.text) < 0:
|
||||
return await message.answer(
|
||||
"<b>❌ Цена не может быть меньше 0₽ или больше 10 000 000₽</b>\n"
|
||||
"📁 Введите цену для позиции",
|
||||
)
|
||||
|
||||
category_id = (await state.get_data())['here_category_id']
|
||||
position_name = (await state.get_data())['here_position_name']
|
||||
position_price = to_number(message.text)
|
||||
position_desc = "None"
|
||||
position_photo = "None"
|
||||
position_id = gen_id(12)
|
||||
await state.clear()
|
||||
|
||||
await Positionx().add(
|
||||
category_id=category_id,
|
||||
position_id=position_id,
|
||||
position_name=position_name,
|
||||
position_price=position_price,
|
||||
position_desc=position_desc,
|
||||
position_photo=position_photo,
|
||||
)
|
||||
|
||||
await position_open_admin(bot, position_id, message.from_user.id)
|
||||
|
||||
|
||||
################################################################################
|
||||
############################ РЕДАКТИРОВАНИЕ ПОЗИЦИИ ############################
|
||||
# Страницы выбора категории для редактирования позиции
|
||||
@router.callback_query(F.data.startswith("position_edit_category_swipe:"))
|
||||
async def prod_position_edit_category_swipe(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
remover = int(call.data.split(":")[1])
|
||||
|
||||
await call.message.edit_text(
|
||||
"<b>📁 Выберите позицию для изменения 🖍</b>",
|
||||
reply_markup=await position_edit_category_swipe_fp(remover),
|
||||
)
|
||||
|
||||
|
||||
# Открытие категории для выбора позиции
|
||||
@router.callback_query(F.data.startswith("position_edit_category_open:"))
|
||||
async def prod_position_edit_category_open(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
category_id = int(call.data.split(":")[1])
|
||||
|
||||
get_category = await Categoryx().get_required(category_id=category_id)
|
||||
get_positions = await Positionx().gets(category_id=category_id)
|
||||
|
||||
if len(get_positions) >= 1:
|
||||
await call.message.edit_text(
|
||||
"<b>📁 Выберите позицию для изменения 🖍</b>",
|
||||
reply_markup=await position_edit_swipe_fp(0, category_id),
|
||||
)
|
||||
else:
|
||||
await call.answer(f"📁 Позиции в категории {get_category.category_name} отсутствуют")
|
||||
|
||||
|
||||
# Страницы выбора позиции для редактирования
|
||||
@router.callback_query(F.data.startswith("position_edit_swipe:"))
|
||||
async def prod_position_edit_swipe(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
category_id = int(call.data.split(":")[1])
|
||||
remover = int(call.data.split(":")[2])
|
||||
|
||||
await del_message(call.message)
|
||||
|
||||
await call.message.answer(
|
||||
"<b>📁 Выберите позицию для изменения 🖍</b>",
|
||||
reply_markup=await position_edit_swipe_fp(remover, category_id),
|
||||
)
|
||||
|
||||
|
||||
# Выбор позиции для редактирования
|
||||
@router.callback_query(F.data.startswith("position_edit_open:"))
|
||||
async def prod_position_edit_open(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
position_id = int(call.data.split(":")[1])
|
||||
remover = int(call.data.split(":")[2])
|
||||
|
||||
await state.clear()
|
||||
|
||||
await del_message(call.message)
|
||||
await position_open_admin(bot, position_id, call.from_user.id)
|
||||
|
||||
|
||||
############################ САМО ИЗМЕНЕНИЕ ПОЗИЦИИ ############################
|
||||
# Изменение названия позиции
|
||||
@router.callback_query(F.data.startswith("position_edit_name:"))
|
||||
async def prod_position_edit_name(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
position_id = int(call.data.split(":")[1])
|
||||
remover = int(call.data.split(":")[2])
|
||||
|
||||
await state.update_data(here_position_id=position_id)
|
||||
await state.update_data(here_remover=remover)
|
||||
await state.set_state("here_position_edit_name")
|
||||
|
||||
await del_message(call.message)
|
||||
await call.message.answer(
|
||||
"<b>📁 Введите новое название для позиции</b>",
|
||||
reply_markup=position_edit_cancel_finl(position_id, remover),
|
||||
)
|
||||
|
||||
|
||||
# Принятие названия позиции для её изменения
|
||||
@router.message(F.text, StateFilter('here_position_edit_name'))
|
||||
async def prod_position_edit_name_get(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
position_id = (await state.get_data())['here_position_id']
|
||||
remover = (await state.get_data())['here_remover']
|
||||
|
||||
if len(message.text) > 50:
|
||||
return await message.answer(
|
||||
"<b>❌ Название не может превышать 50 символов</b>\n"
|
||||
"📁 Введите новое название для позиции",
|
||||
reply_markup=position_edit_cancel_finl(position_id, remover),
|
||||
)
|
||||
|
||||
await state.clear()
|
||||
|
||||
await Positionx().update(position_id, position_name=clear_html(message.text))
|
||||
await position_open_admin(bot, position_id, message.from_user.id)
|
||||
|
||||
|
||||
# Изменение цены позиции
|
||||
@router.callback_query(F.data.startswith("position_edit_price:"))
|
||||
async def prod_position_edit_price(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
position_id = int(call.data.split(":")[1])
|
||||
remover = int(call.data.split(":")[2])
|
||||
|
||||
await state.update_data(here_position_id=position_id)
|
||||
await state.update_data(here_remover=remover)
|
||||
await state.set_state("here_position_edit_price")
|
||||
|
||||
await del_message(call.message)
|
||||
await call.message.answer(
|
||||
"<b>📁 Введите новую цену для позиции</b>",
|
||||
reply_markup=position_edit_cancel_finl(position_id, remover),
|
||||
)
|
||||
|
||||
|
||||
# Принятие цены позиции для её изменения
|
||||
@router.message(F.text, StateFilter('here_position_edit_price'))
|
||||
async def prod_position_edit_price_get(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
position_id = (await state.get_data())['here_position_id']
|
||||
remover = (await state.get_data())['here_remover']
|
||||
|
||||
if not is_number(message.text):
|
||||
return await message.answer(
|
||||
"<b>❌ Данные были введены неверно</b>\n"
|
||||
"📁 Введите новую цену для позиции",
|
||||
reply_markup=position_edit_cancel_finl(position_id, remover),
|
||||
)
|
||||
|
||||
if to_number(message.text) > 10_000_000 or to_number(message.text) < 0:
|
||||
return await message.answer(
|
||||
"<b>❌ Цена не может быть меньше 0₽ или больше 10 000 000₽</b>\n"
|
||||
"📁 Введите новую цену для позиции",
|
||||
reply_markup=position_edit_cancel_finl(position_id, remover),
|
||||
)
|
||||
|
||||
await state.clear()
|
||||
|
||||
await Positionx().update(position_id, position_price=to_number(message.text))
|
||||
await position_open_admin(bot, position_id, message.from_user.id)
|
||||
|
||||
|
||||
# Изменение описания позиции
|
||||
@router.callback_query(F.data.startswith("position_edit_desc:"))
|
||||
async def prod_position_edit_desc(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
position_id = int(call.data.split(":")[1])
|
||||
remover = int(call.data.split(":")[2])
|
||||
|
||||
await state.update_data(here_position_id=position_id)
|
||||
await state.update_data(here_remover=remover)
|
||||
await state.set_state("here_position_edit_desc")
|
||||
|
||||
await del_message(call.message)
|
||||
await call.message.answer(
|
||||
ded(f"""
|
||||
<b>📁 Введите новое описание для позиции</b>
|
||||
❕ Вы можете использовать HTML разметку
|
||||
❕ Отправьте <code>0</code> чтобы пропустить
|
||||
"""),
|
||||
reply_markup=position_edit_cancel_finl(position_id, remover),
|
||||
)
|
||||
|
||||
|
||||
# Принятие описания позиции для её изменения
|
||||
@router.message(F.text, StateFilter('here_position_edit_desc'))
|
||||
async def prod_position_edit_desc_get(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
position_id = (await state.get_data())['here_position_id']
|
||||
remover = (await state.get_data())['here_remover']
|
||||
|
||||
if len(message.text) > 1200:
|
||||
return await message.answer(
|
||||
ded(f"""
|
||||
<b>❌ Описание не может превышать 1200 символов</b>
|
||||
📁 Введите новое описание для позиции
|
||||
❕ Вы можете использовать HTML разметку
|
||||
❕ Отправьте <code>0</code> чтобы пропустить
|
||||
"""),
|
||||
reply_markup=position_edit_cancel_finl(position_id, remover),
|
||||
)
|
||||
|
||||
try:
|
||||
if message.text != "0":
|
||||
await (await message.answer(message.text)).delete()
|
||||
|
||||
position_desc = message.text
|
||||
else:
|
||||
position_desc = "None"
|
||||
except Exception:
|
||||
bot_logger.debug("Некорректная HTML-разметка описания позиции", exc_info=True)
|
||||
return await message.answer(
|
||||
ded(f"""
|
||||
<b>❌ Ошибка синтаксиса HTML</b>
|
||||
📁 Введите новое описание для позиции
|
||||
❕ Вы можете использовать HTML разметку
|
||||
❕ Отправьте <code>0</code> чтобы пропустить
|
||||
"""),
|
||||
reply_markup=position_edit_cancel_finl(position_id, remover),
|
||||
)
|
||||
|
||||
await state.clear()
|
||||
|
||||
await Positionx().update(position_id, position_desc=position_desc)
|
||||
await position_open_admin(bot, position_id, message.from_user.id)
|
||||
|
||||
|
||||
# Изменение изображения позиции
|
||||
@router.callback_query(F.data.startswith("position_edit_photo:"))
|
||||
async def prod_position_edit_photo(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
position_id = int(call.data.split(":")[1])
|
||||
remover = int(call.data.split(":")[2])
|
||||
|
||||
get_settings = await Settingsx().get()
|
||||
|
||||
if get_settings.misc_discord_webhook_url == "None":
|
||||
return await call.answer("🧿 Отсутствует Дискорд вебхук, добавьте его в настройках", True)
|
||||
|
||||
await state.update_data(here_position_id=position_id)
|
||||
await state.update_data(here_remover=remover)
|
||||
await state.set_state("here_position_edit_photo")
|
||||
|
||||
await del_message(call.message)
|
||||
await call.message.answer(
|
||||
"<b>📁 Отправьте новое изображение для позиции</b>\n"
|
||||
"❕ Отправьте <code>0</code> чтобы пропустить.",
|
||||
reply_markup=position_edit_cancel_finl(position_id, remover),
|
||||
)
|
||||
|
||||
|
||||
# Принятие нового фото для позиции
|
||||
@router.message((F.text == "0") | F.photo, StateFilter('here_position_edit_photo'))
|
||||
async def prod_position_edit_photo_get(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
position_id = (await state.get_data())['here_position_id']
|
||||
remover = (await state.get_data())['here_remover']
|
||||
|
||||
get_settings = await Settingsx().get()
|
||||
|
||||
if get_settings.misc_discord_webhook_url == "None":
|
||||
return await message.answer(
|
||||
"<b>🧿 Отсутствует Дискорд вебхук, добавьте его в настройках</b>"
|
||||
)
|
||||
|
||||
position_photo = "None"
|
||||
|
||||
if message.photo is not None:
|
||||
cache_message = await message.answer(
|
||||
"<b>♻️ Подождите, фотография загружается...</b>"
|
||||
)
|
||||
|
||||
file_path = (await bot.get_file(message.photo[-1].file_id)).file_path
|
||||
photo_path = await bot.download_file(file_path)
|
||||
|
||||
pay_image_status, pay_image_url = await (
|
||||
await DiscordAPI.connect(
|
||||
bot=bot,
|
||||
arSession=arSession,
|
||||
update=message,
|
||||
)
|
||||
).upload_photo(photo_path.read())
|
||||
|
||||
if pay_image_status:
|
||||
position_photo = pay_image_url
|
||||
|
||||
await del_message(cache_message)
|
||||
|
||||
await state.clear()
|
||||
|
||||
await Positionx().update(position_id, position_photo=position_photo)
|
||||
await position_open_admin(bot, position_id, message.from_user.id)
|
||||
|
||||
|
||||
# Выгрузка товаров
|
||||
@router.callback_query(F.data.startswith("position_edit_items:"))
|
||||
async def prod_position_edit_items(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
position_id = int(call.data.split(":")[1])
|
||||
remover = int(call.data.split(":")[2])
|
||||
|
||||
get_position = await Positionx().get_required(position_id=position_id)
|
||||
get_items = await Itemx().gets(position_id=position_id)
|
||||
|
||||
if len(get_items) >= 1:
|
||||
save_items = "\n\n".join([item.item_data for item in get_items])
|
||||
|
||||
link_items = await (
|
||||
await HostingAPI.connect(
|
||||
bot=bot,
|
||||
arSession=arSession,
|
||||
)
|
||||
).upload_text(save_items)
|
||||
|
||||
await call.message.answer(
|
||||
f"<b>🎁 Все товары позиции: <code>{get_position.position_name}</code>\n"
|
||||
f"🔗 Ссылка: <a href='{link_items}'>кликабельно</a></b>",
|
||||
reply_markup=close_finl(),
|
||||
disable_web_page_preview=True,
|
||||
)
|
||||
await call.answer(cache_time=5)
|
||||
else:
|
||||
await call.answer("❕ В данной позиции отсутствуют товары", True)
|
||||
|
||||
|
||||
# Удаление позиции
|
||||
@router.callback_query(F.data.startswith("position_edit_delete:"))
|
||||
async def prod_position_edit_delete(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
position_id = int(call.data.split(":")[1])
|
||||
remover = int(call.data.split(":")[2])
|
||||
|
||||
await del_message(call.message)
|
||||
|
||||
await call.message.answer(
|
||||
"<b>📁 Вы действительно хотите удалить позицию? ❌</b>",
|
||||
reply_markup=position_edit_delete_finl(position_id, remover),
|
||||
)
|
||||
|
||||
|
||||
# Подтверждение удаления позиции
|
||||
@router.callback_query(F.data.startswith("position_edit_delete_confirm:"))
|
||||
async def prod_position_edit_delete_confirm(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
position_id = int(call.data.split(":")[1])
|
||||
remover = int(call.data.split(":")[2])
|
||||
|
||||
get_position = await Positionx().get_required(position_id=position_id)
|
||||
get_positions = await Positionx().gets(category_id=get_position.category_id)
|
||||
|
||||
await Itemx().delete(position_id=position_id)
|
||||
await Positionx().delete(position_id=position_id)
|
||||
|
||||
await call.answer("📁 Вы успешно удалили позицию и её товары ✅")
|
||||
|
||||
if len(get_positions) >= 1:
|
||||
await call.message.edit_text(
|
||||
"<b>📁 Выберите позицию для изменения 🖍</b>",
|
||||
reply_markup=await position_edit_swipe_fp(remover, get_position.category_id),
|
||||
)
|
||||
else:
|
||||
await del_message(call.message)
|
||||
|
||||
|
||||
# Очистка позиции
|
||||
@router.callback_query(F.data.startswith("position_edit_clear:"))
|
||||
async def prod_position_edit_clear(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
position_id = int(call.data.split(":")[1])
|
||||
remover = int(call.data.split(":")[2])
|
||||
|
||||
await del_message(call.message)
|
||||
await call.message.answer(
|
||||
"<b>📁 Вы хотите удалить все товары в позиции?</b>",
|
||||
reply_markup=position_edit_clear_finl(position_id, remover),
|
||||
)
|
||||
|
||||
|
||||
# Согласие на очистку позиции
|
||||
@router.callback_query(F.data.startswith("position_edit_clear_confirm:"))
|
||||
async def prod_position_edit_clear_confirm(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
position_id = int(call.data.split(":")[1])
|
||||
remover = int(call.data.split(":")[2])
|
||||
|
||||
await Itemx().delete(position_id=position_id)
|
||||
await call.answer("📁 Вы успешно удалили все товары в позиции ✅")
|
||||
|
||||
await del_message(call.message)
|
||||
await position_open_admin(bot, position_id, call.from_user.id)
|
||||
|
||||
|
||||
################################################################################
|
||||
############################### ДОБАВЛЕНИЕ ТОВАРОВ #############################
|
||||
# Страницы выбора категории для добавления товара
|
||||
@router.callback_query(F.data.startswith("item_add_category_swipe:"))
|
||||
async def prod_item_add_category_swipe(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
remover = int(call.data.split(":")[1])
|
||||
|
||||
await call.message.edit_text(
|
||||
"<b>🎁 Выберите позицию для товаров ➕</b>",
|
||||
reply_markup=await item_add_category_swipe_fp(remover),
|
||||
)
|
||||
|
||||
|
||||
# Открытие категории для выбора позиции
|
||||
@router.callback_query(F.data.startswith("item_add_category_open:"))
|
||||
async def prod_item_add_category_open(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
category_id = int(call.data.split(":")[1])
|
||||
remover = int(call.data.split(":")[2])
|
||||
|
||||
get_category = await Categoryx().get_required(category_id=category_id)
|
||||
get_positions = await Positionx().gets(category_id=category_id)
|
||||
|
||||
await del_message(call.message)
|
||||
|
||||
if len(get_positions) >= 1:
|
||||
await call.message.answer(
|
||||
"<b>🎁 Выберите позицию для товаров ➕</b>",
|
||||
reply_markup=await item_add_position_swipe_fp(0, category_id),
|
||||
)
|
||||
else:
|
||||
await call.answer(f"🎁 Позиции в категории {get_category.category_name} отсутствуют")
|
||||
|
||||
|
||||
# Страницы выбора позиции для добавления товара
|
||||
@router.callback_query(F.data.startswith("item_add_position_swipe:"))
|
||||
async def prod_item_add_position_swipe(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
category_id = int(call.data.split(":")[1])
|
||||
remover = int(call.data.split(":")[2])
|
||||
|
||||
await call.message.edit_text(
|
||||
"<b>🎁 Выберите позицию для товаров ➕</b>",
|
||||
reply_markup=await item_add_position_swipe_fp(remover, category_id),
|
||||
)
|
||||
|
||||
|
||||
# Выбор позиции для добавления товаров
|
||||
@router.callback_query(F.data.startswith("item_add_position_open:"), flags={'rate': 0})
|
||||
async def prod_item_add_position_open(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
position_id = int(call.data.split(":")[1])
|
||||
|
||||
get_position = await Positionx().get_required(position_id=position_id)
|
||||
get_settings = await Settingsx().get()
|
||||
|
||||
await state.update_data(here_add_item_category_id=get_position.category_id)
|
||||
await state.update_data(here_add_item_position_id=get_position.position_id)
|
||||
await state.update_data(here_add_item_count=0)
|
||||
await state.set_state("here_add_items")
|
||||
|
||||
await del_message(call.message)
|
||||
|
||||
if get_settings.misc_method_prod == "skip":
|
||||
await call.message.answer(
|
||||
ded(f"""
|
||||
<b>🎁 Отправляйте данные товаров</b>
|
||||
❗ Товары разделяются одной пустой строчкой. Пример:
|
||||
<code>Данные товара...
|
||||
|
||||
Данные товара...
|
||||
|
||||
Данные товара...</code>
|
||||
"""),
|
||||
reply_markup=item_add_finish_finl(position_id),
|
||||
)
|
||||
else:
|
||||
await call.message.answer(
|
||||
ded(f"""
|
||||
<b>🎁 Отправляйте данные товаров</b>
|
||||
❗ Товары каждой новой строчкой. Пример:
|
||||
<code>Данные товара...
|
||||
Данные товара...
|
||||
Данные товара...</code>
|
||||
"""),
|
||||
reply_markup=item_add_finish_finl(position_id),
|
||||
)
|
||||
|
||||
|
||||
# Завершение загрузки товаров
|
||||
@router.callback_query(F.data.startswith('item_add_position_finish:'), flags={'rate': 0})
|
||||
async def prod_item_add_finish(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
position_id = int(call.data.split(":")[1])
|
||||
|
||||
try:
|
||||
count_items = (await state.get_data())['here_add_item_count']
|
||||
except Exception:
|
||||
bot_logger.debug("В state нет счетчика добавленных товаров", exc_info=True)
|
||||
count_items = 0
|
||||
|
||||
await state.clear()
|
||||
|
||||
await call.message.edit_reply_markup()
|
||||
await call.message.answer(
|
||||
"<b>🎁 Загрузка товаров была успешно завершена ✅\n"
|
||||
f"❕ Загружено товаров: <code>{count_items}шт</code></b>",
|
||||
)
|
||||
|
||||
await position_open_admin(bot, position_id, call.from_user.id)
|
||||
|
||||
|
||||
# Принятие данных товара
|
||||
@router.message(F.text, StateFilter('here_add_items'), flags={'rate': 0})
|
||||
async def prod_item_add_get(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
cache_message = await message.answer("<b>⌛ Ждите, товары добавляются..</b>")
|
||||
|
||||
get_settings = await Settingsx().get()
|
||||
|
||||
if get_settings.misc_method_prod == "skip":
|
||||
get_items = clear_list(message.text.split("\n\n"))
|
||||
else:
|
||||
get_items = clear_list(message.text.split("\n"))
|
||||
|
||||
item_count = (await state.get_data())['here_add_item_count']
|
||||
category_id = (await state.get_data())['here_add_item_category_id']
|
||||
position_id = (await state.get_data())['here_add_item_position_id']
|
||||
|
||||
await state.update_data(here_add_item_count=item_count + len(get_items))
|
||||
|
||||
await Itemx().add(
|
||||
user_id=message.from_user.id,
|
||||
category_id=category_id,
|
||||
position_id=position_id,
|
||||
item_datas=get_items,
|
||||
)
|
||||
|
||||
await cache_message.edit_text(
|
||||
f"<b>🎁 Товары в кол-ве <u>{len(get_items)}шт</u> были успешно добавлены ✅</b>",
|
||||
reply_markup=item_add_finish_finl(position_id),
|
||||
)
|
||||
|
||||
|
||||
################################################################################
|
||||
############################### УДАЛЕНИЕ ТОВАРОВ ###############################
|
||||
# Страницы удаления товаров
|
||||
@router.callback_query(F.data.startswith("item_delete_swipe:"))
|
||||
async def prod_item_delete_swipe(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
position_id = int(call.data.split(":")[1])
|
||||
remover = int(call.data.split(":")[2])
|
||||
|
||||
get_items = await Itemx().gets(position_id=position_id)
|
||||
get_position = await Positionx().get_required(position_id=position_id)
|
||||
|
||||
if len(get_items) >= 1:
|
||||
await del_message(call.message)
|
||||
|
||||
await call.message.answer(
|
||||
"<b>🎁 Выберите товар для удаления</b>",
|
||||
reply_markup=await item_delete_swipe_fp(remover, position_id),
|
||||
)
|
||||
else:
|
||||
await call.answer(f"🎁 Товары в позиции {get_position.position_name} отсутствуют", True)
|
||||
|
||||
|
||||
# Удаление товара
|
||||
@router.callback_query(F.data.startswith("item_delete_open:"))
|
||||
async def prod_item_delete_open(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
item_id = int(call.data.split(":")[1])
|
||||
|
||||
await del_message(call.message)
|
||||
await item_open_admin(bot, item_id, call.from_user.id)
|
||||
|
||||
|
||||
# Подтверждение удаления товара
|
||||
@router.callback_query(F.data.startswith("item_delete_confirm:"))
|
||||
async def prod_item_delete_confirm_open(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
item_id = int(call.data.split(":")[1])
|
||||
|
||||
get_item = await Itemx().get_required(item_id=item_id)
|
||||
get_items = await Itemx().gets(position_id=get_item.position_id)
|
||||
|
||||
await Itemx().delete(item_id=item_id)
|
||||
|
||||
await call.message.edit_reply_markup()
|
||||
await call.message.react([ReactionTypeEmoji(emoji="🔥")])
|
||||
|
||||
if len(get_items) >= 1:
|
||||
await call.message.answer(
|
||||
"<b>🎁 Выберите товар для удаления</b>",
|
||||
reply_markup=await item_delete_swipe_fp(0, get_item.position_id),
|
||||
)
|
||||
|
||||
|
||||
################################################################################
|
||||
############################### УДАЛЕНИЕ РАЗДЕЛОВ ##############################
|
||||
# Возвращение к меню удаления разделов
|
||||
@router.callback_query(F.data == "prod_removes_return")
|
||||
async def prod_removes_return(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
await state.clear()
|
||||
|
||||
await call.message.edit_text(
|
||||
"<b>🎁 Выберите раздел который хотите удалить ❌</b>\n",
|
||||
reply_markup=products_removes_finl(),
|
||||
)
|
||||
|
||||
|
||||
# Удаление всех категорий
|
||||
@router.callback_query(F.data == "prod_removes_categories")
|
||||
async def prod_removes_categories(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
get_categories = len(await Categoryx().get_all())
|
||||
get_positions = len(await Positionx().get_all())
|
||||
get_items = len(await Itemx().get_all())
|
||||
|
||||
await call.message.edit_text(
|
||||
ded(f"""
|
||||
<b>❌ Вы действительно хотите удалить все категории, позиции и товары?</b>
|
||||
🗃 Категорий: <code>{get_categories}шт</code>
|
||||
📁 Позиций: <code>{get_positions}шт</code>
|
||||
🎁 Товаров: <code>{get_items}шт</code>
|
||||
"""),
|
||||
reply_markup=products_removes_categories_finl(),
|
||||
)
|
||||
|
||||
|
||||
# Подтверждение удаления всех категорий (позиций и товаров включительно)
|
||||
@router.callback_query(F.data == "prod_removes_categories_confirm")
|
||||
async def prod_removes_categories_confirm(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
get_categories = len(await Categoryx().get_all())
|
||||
get_positions = len(await Positionx().get_all())
|
||||
get_items = len(await Itemx().get_all())
|
||||
|
||||
await Categoryx().clear()
|
||||
await Positionx().clear()
|
||||
await Itemx().clear()
|
||||
|
||||
await call.message.edit_text(
|
||||
ded(f"""
|
||||
<b>✅ Вы успешно удалили все категории</b>
|
||||
🗃 Категорий: <code>{get_categories}шт</code>
|
||||
📁 Позиций: <code>{get_positions}шт</code>
|
||||
🎁 Товаров: <code>{get_items}шт</code>
|
||||
""")
|
||||
)
|
||||
|
||||
|
||||
# Удаление всех позиций
|
||||
@router.callback_query(F.data == "prod_removes_positions")
|
||||
async def prod_removes_positions(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
get_positions = len(await Positionx().get_all())
|
||||
get_items = len(await Itemx().get_all())
|
||||
|
||||
await call.message.edit_text(
|
||||
ded(f"""
|
||||
<b>❌ Вы действительно хотите удалить все позиции и товары?</b>
|
||||
📁 Позиций: <code>{get_positions}шт</code>
|
||||
🎁 Товаров: <code>{get_items}шт</code>
|
||||
"""),
|
||||
reply_markup=products_removes_positions_finl(),
|
||||
)
|
||||
|
||||
|
||||
# Подтверждение удаления всех позиций (товаров включительно)
|
||||
@router.callback_query(F.data == "prod_removes_positions_confirm")
|
||||
async def prod_position_remove(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
get_positions = len(await Positionx().get_all())
|
||||
get_items = len(await Itemx().get_all())
|
||||
|
||||
await Positionx().clear()
|
||||
await Itemx().clear()
|
||||
|
||||
await call.message.edit_text(
|
||||
ded(f"""
|
||||
<b>✅ Вы успешно удалили все позиции</b>
|
||||
📁 Позиций: <code>{get_positions}шт</code>
|
||||
🎁 Товаров: <code>{get_items}шт</code>
|
||||
""")
|
||||
)
|
||||
|
||||
|
||||
# Удаление всех товаров
|
||||
@router.callback_query(F.data == "prod_removes_items")
|
||||
async def prod_removes_items(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
get_items = len(await Itemx().get_all())
|
||||
|
||||
await call.message.edit_text(
|
||||
f"<b>❌ Вы действительно хотите удалить все товары?</b>\n"
|
||||
f"🎁 Товаров: <code>{get_items}шт</code>",
|
||||
reply_markup=products_removes_items_finl(),
|
||||
)
|
||||
|
||||
|
||||
# Согласие на удаление всех товаров
|
||||
@router.callback_query(F.data == "prod_removes_items_confirm")
|
||||
async def prod_item_remove(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
get_items = len(await Itemx().get_all())
|
||||
|
||||
await Itemx().clear()
|
||||
|
||||
await call.message.edit_text(
|
||||
f"<b>✅ Вы успешно удалили все товары</b>\n"
|
||||
f"🎁 Товаров: <code>{get_items}шт</code>"
|
||||
)
|
||||
@@ -0,0 +1,376 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
from aiogram import Router, Bot, F
|
||||
from aiogram.filters import StateFilter
|
||||
from aiogram.types import CallbackQuery, Message
|
||||
|
||||
from tgbot.database import Settingsx, Userx
|
||||
from tgbot.keyboards.inline_admin import settings_status_finl, settings_finl
|
||||
from tgbot.services.api_discord import DiscordDJ, DiscordAPI
|
||||
from tgbot.utils.const_functions import ded
|
||||
from tgbot.utils.misc.bot_logging import bot_logger
|
||||
from tgbot.utils.misc.bot_models import FSM, ARS
|
||||
from tgbot.utils.misc_functions import send_admins, insert_tags
|
||||
|
||||
router = Router(name=__name__)
|
||||
|
||||
|
||||
# Изменение данных
|
||||
@router.message(F.text == "🖍 Изменить данные")
|
||||
async def settings_data_edit(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
await state.clear()
|
||||
|
||||
await message.answer(
|
||||
"<b>🖍 Изменение данных бота</b>",
|
||||
reply_markup=await settings_finl(),
|
||||
)
|
||||
|
||||
|
||||
# Выключатели бота
|
||||
@router.message(F.text == "🕹 Выключатели")
|
||||
async def settings_status_edit(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
await state.clear()
|
||||
|
||||
await message.answer(
|
||||
"<b>🕹 Включение и выключение основных функций</b>",
|
||||
reply_markup=await settings_status_finl(),
|
||||
)
|
||||
|
||||
|
||||
################################################################################
|
||||
################################## ВЫКЛЮЧАТЕЛИ #################################
|
||||
# Включение/выключение тех работ
|
||||
@router.callback_query(F.data.startswith("settings_status_work:"))
|
||||
async def settings_status_work(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
get_status = call.data.split(":")[1]
|
||||
|
||||
get_user = await Userx().get_required(user_id=call.from_user.id)
|
||||
await Settingsx().update(status_work=get_status)
|
||||
|
||||
if get_status == "True":
|
||||
send_text = "🔴 Отправил бота на технические работы"
|
||||
else:
|
||||
send_text = "🟢 Вывел бота из технических работ"
|
||||
|
||||
await send_admins(
|
||||
bot=bot,
|
||||
text=ded(f"""
|
||||
👤 Администратор <a href='tg://user?id={get_user.user_id}'>{get_user.user_name}</a>
|
||||
{send_text}
|
||||
"""),
|
||||
not_me=get_user.user_id,
|
||||
)
|
||||
|
||||
await call.message.edit_reply_markup(reply_markup=await settings_status_finl())
|
||||
|
||||
|
||||
# Включение/выключение покупок
|
||||
@router.callback_query(F.data.startswith("settings_status_buy:"))
|
||||
async def settings_status_buy(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
get_status = call.data.split(":")[1]
|
||||
|
||||
get_user = await Userx().get_required(user_id=call.from_user.id)
|
||||
await Settingsx().update(status_buy=get_status)
|
||||
|
||||
if get_status == "True":
|
||||
send_text = "🟢 Включил покупки в боте"
|
||||
else:
|
||||
send_text = "🔴 Выключил покупки в боте"
|
||||
|
||||
await send_admins(
|
||||
bot=bot,
|
||||
text=ded(f"""
|
||||
👤 Администратор <a href='tg://user?id={get_user.user_id}'>{get_user.user_name}</a>
|
||||
{send_text}
|
||||
"""),
|
||||
not_me=get_user.user_id,
|
||||
)
|
||||
|
||||
await call.message.edit_reply_markup(reply_markup=await settings_status_finl())
|
||||
|
||||
|
||||
# Включение/выключение пополнений
|
||||
@router.callback_query(F.data.startswith("settings_status_refill:"))
|
||||
async def settings_status_refill(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
get_status = call.data.split(":")[1]
|
||||
|
||||
get_user = await Userx().get_required(user_id=call.from_user.id)
|
||||
await Settingsx().update(status_refill=get_status)
|
||||
|
||||
if get_status == "True":
|
||||
send_text = "🟢 Включил пополнения в боте"
|
||||
else:
|
||||
send_text = "🔴 Выключил пополнения в боте"
|
||||
|
||||
await send_admins(
|
||||
bot,
|
||||
f"👤 Администратор <a href='tg://user?id={get_user.user_id}'>{get_user.user_name}</a>\n"
|
||||
f"{send_text}",
|
||||
not_me=get_user.user_id,
|
||||
)
|
||||
|
||||
await call.message.edit_reply_markup(reply_markup=await settings_status_finl())
|
||||
|
||||
|
||||
# Включение/выключение уведомлений о покупках
|
||||
@router.callback_query(F.data.startswith("settings_notification_buy:"))
|
||||
async def settings_notification_buy(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
get_status = call.data.split(":")[1]
|
||||
|
||||
get_user = await Userx().get_required(user_id=call.from_user.id)
|
||||
await Settingsx().update(notification_buy=get_status)
|
||||
|
||||
if get_status == "True":
|
||||
send_text = "🟢 Включил уведомления о покупках в боте"
|
||||
else:
|
||||
send_text = "🔴 Выключил уведомления о покупках в боте"
|
||||
|
||||
await send_admins(
|
||||
bot,
|
||||
f"👤 Администратор <a href='tg://user?id={get_user.user_id}'>{get_user.user_name}</a>\n"
|
||||
f"{send_text}",
|
||||
not_me=get_user.user_id,
|
||||
)
|
||||
|
||||
await call.message.edit_reply_markup(reply_markup=await settings_status_finl())
|
||||
|
||||
|
||||
# Включение/выключение уведомлений о пополнениях
|
||||
@router.callback_query(F.data.startswith("settings_notification_refill:"))
|
||||
async def settings_notification_refill(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
get_status = call.data.split(":")[1]
|
||||
|
||||
get_user = await Userx().get_required(user_id=call.from_user.id)
|
||||
await Settingsx().update(notification_refill=get_status)
|
||||
|
||||
if get_status == "True":
|
||||
send_text = "🟢 Включил уведомления о пополнениях в боте"
|
||||
else:
|
||||
send_text = "🔴 Выключил уведомления о пополнениях в боте"
|
||||
|
||||
await send_admins(
|
||||
bot,
|
||||
f"👤 Администратор <a href='tg://user?id={get_user.user_id}'>{get_user.user_name}</a>\n"
|
||||
f"{send_text}",
|
||||
not_me=get_user.user_id,
|
||||
)
|
||||
|
||||
await call.message.edit_reply_markup(reply_markup=await settings_status_finl())
|
||||
|
||||
|
||||
################################################################################
|
||||
############################### ИЗМЕНЕНИЕ ДАННЫХ ###############################
|
||||
# Изменение FAQ
|
||||
@router.callback_query(F.data == "settings_edit_faq")
|
||||
async def settings_faq_edit(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
await state.clear()
|
||||
|
||||
await state.set_state("here_settings_faq")
|
||||
await call.message.edit_text(
|
||||
ded("""
|
||||
<b>❔ Введите новый текст для FAQ</b>
|
||||
❕ Вы можете использовать заготовленный синтаксис и HTML разметку:
|
||||
▪️ <code>{username}</code> - логин пользоваля
|
||||
▪️ <code>{user_id}</code> - айди пользователя
|
||||
▪️ <code>{firstname}</code> - имя пользователя
|
||||
""")
|
||||
)
|
||||
|
||||
|
||||
# Изменение поддержки
|
||||
@router.callback_query(F.data == "settings_edit_support")
|
||||
async def settings_support_edit(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
await state.clear()
|
||||
|
||||
await state.set_state("here_settings_support")
|
||||
await call.message.edit_text(
|
||||
"<b>☎️ Отправьте юзернейм для поддержки</b>\n"
|
||||
"❕ Юзернейм пользователя/бота/канала/чата",
|
||||
)
|
||||
|
||||
|
||||
# Изменение отображения/скрытия категорий без товаров
|
||||
@router.callback_query(F.data.startswith("settings_edit_hide_category:"))
|
||||
async def settings_edit_hide_category(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
status = call.data.split(":")[1]
|
||||
|
||||
await Settingsx().update(misc_hide_category=status)
|
||||
|
||||
await call.message.edit_text(
|
||||
"<b>🖍 Изменение данных бота</b>",
|
||||
reply_markup=await settings_finl(),
|
||||
)
|
||||
|
||||
|
||||
# Изменение отображения/скрытия позиций без товаров
|
||||
@router.callback_query(F.data.startswith("settings_edit_hide_position:"))
|
||||
async def settings_edit_hide_position(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
status = call.data.split(":")[1]
|
||||
|
||||
await Settingsx().update(misc_hide_position=status)
|
||||
|
||||
await call.message.edit_text(
|
||||
"<b>🖍 Изменение данных бота</b>",
|
||||
reply_markup=await settings_finl(),
|
||||
)
|
||||
|
||||
|
||||
# Изменение метода добавления товаров
|
||||
@router.callback_query(F.data.startswith("settings_edit_method_prod:"))
|
||||
async def settings_edit_method_prod(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
method = call.data.split(":")[1]
|
||||
|
||||
await Settingsx().update(misc_method_prod=method)
|
||||
|
||||
await call.message.edit_text(
|
||||
"<b>🖍 Изменение данных бота</b>",
|
||||
reply_markup=await settings_finl(),
|
||||
)
|
||||
|
||||
|
||||
# Изменение дискорд вебхука
|
||||
@router.callback_query(F.data == "settings_edit_discord_webhook")
|
||||
async def settings_discord_edit(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
await state.clear()
|
||||
|
||||
get_discord_public_webhook = await (
|
||||
DiscordDJ(
|
||||
arSession=arSession,
|
||||
bot=bot,
|
||||
)
|
||||
).export_webhook()
|
||||
|
||||
await state.set_state("here_settings_discord_webhook")
|
||||
await call.message.edit_text(
|
||||
ded(f"""
|
||||
<b>🧿 Отправьте новый вебхук дискорда</b>
|
||||
❕ Для удаления вебхука введите <code>0</code>
|
||||
❕ Вы можете использовать публичный вебхук, но ответственность за его использование лежит только на вас
|
||||
▪️ Публичный вебхук: <code>{get_discord_public_webhook}</code>
|
||||
""")
|
||||
)
|
||||
|
||||
|
||||
# Изменение текстового хостинга по умолчанию
|
||||
@router.callback_query(F.data == "settings_edit_hosting_text")
|
||||
async def settings_edit_hosting_text(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
get_settings = await Settingsx().get()
|
||||
|
||||
if get_settings.misc_hosting_text == "telegraph":
|
||||
await Settingsx().update(misc_hosting_text="pastie")
|
||||
elif get_settings.misc_hosting_text == "pastie":
|
||||
await Settingsx().update(misc_hosting_text="friendpaste")
|
||||
elif get_settings.misc_hosting_text == "friendpaste":
|
||||
await Settingsx().update(misc_hosting_text="snippet")
|
||||
elif get_settings.misc_hosting_text == "snippet":
|
||||
await Settingsx().update(misc_hosting_text="telegraph")
|
||||
|
||||
await call.message.edit_text(
|
||||
"<b>🖍 Изменение данных бота</b>",
|
||||
reply_markup=await settings_finl(),
|
||||
)
|
||||
|
||||
|
||||
################################################################################
|
||||
################################ ПРИНЯТИЕ ДАННЫХ ###############################
|
||||
# Принятие FAQ
|
||||
@router.message(F.text, StateFilter("here_settings_faq"))
|
||||
async def settings_faq_get(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
get_message = await insert_tags(message.from_user.id, message.text)
|
||||
|
||||
try:
|
||||
await (await message.answer(get_message)).delete()
|
||||
except Exception:
|
||||
bot_logger.debug("Некорректная HTML-разметка FAQ", exc_info=True)
|
||||
return await message.answer(
|
||||
"<b>❌ Ошибка синтаксиса HTML</b>\n"
|
||||
"❔ Введите новый текст для FAQ",
|
||||
)
|
||||
|
||||
await state.clear()
|
||||
await Settingsx().update(misc_faq=message.text)
|
||||
|
||||
await message.answer(
|
||||
"<b>🖍 Изменение данных бота</b>",
|
||||
reply_markup=await settings_finl(),
|
||||
)
|
||||
|
||||
|
||||
# Принятие поддержки
|
||||
@router.message(F.text, StateFilter("here_settings_support"))
|
||||
async def settings_support_get(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
get_support = message.text
|
||||
|
||||
if get_support.startswith("@"):
|
||||
get_support = get_support[1:]
|
||||
|
||||
await Settingsx().update(misc_support=get_support)
|
||||
await state.clear()
|
||||
|
||||
await message.answer(
|
||||
"<b>🖍 Изменение данных бота</b>",
|
||||
reply_markup=await settings_finl(),
|
||||
)
|
||||
|
||||
|
||||
# Принятие дискорд вебхука
|
||||
@router.message(F.text, StateFilter("here_settings_discord_webhook"))
|
||||
async def settings_discord_get(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
get_discord_webhook = message.text
|
||||
|
||||
# Удаление вебхука
|
||||
if get_discord_webhook == "0":
|
||||
await Settingsx().update(
|
||||
misc_discord_webhook_url="None",
|
||||
misc_discord_webhook_name="None",
|
||||
)
|
||||
|
||||
return await message.answer(
|
||||
"<b>⚙️ Настройки бота</b>",
|
||||
reply_markup=await settings_finl(),
|
||||
)
|
||||
|
||||
# Добавление нового вебхука
|
||||
cache_message = await message.answer("<b>♻️ Проверка дискорд вебхука..</b>")
|
||||
|
||||
if "api" in get_discord_webhook and "webhooks" in get_discord_webhook:
|
||||
discord_webhook_status, discord_webhook_name = await (
|
||||
await DiscordAPI.connect(
|
||||
bot=bot,
|
||||
arSession=arSession,
|
||||
update=message,
|
||||
webhook_url=get_discord_webhook,
|
||||
skipping_error=True,
|
||||
)
|
||||
).check()
|
||||
|
||||
if discord_webhook_status:
|
||||
await state.clear()
|
||||
|
||||
await Settingsx().update(
|
||||
misc_discord_webhook_url=message.text,
|
||||
misc_discord_webhook_name=discord_webhook_name,
|
||||
)
|
||||
|
||||
return await cache_message.edit_text(
|
||||
"<b>⚙️ Настройки бота</b>",
|
||||
reply_markup=await settings_finl(),
|
||||
)
|
||||
|
||||
# Обработка ошибки добавления вебхука
|
||||
get_discord_public_webhook = await (
|
||||
DiscordDJ(
|
||||
arSession=arSession,
|
||||
bot=bot,
|
||||
)
|
||||
).export_webhook()
|
||||
|
||||
await cache_message.edit_text(
|
||||
ded(f"""
|
||||
<b>❌ Указан некорректный вебхук</b>
|
||||
➖➖➖➖➖➖➖➖➖➖
|
||||
🧿 Отправьте новый вебхук дискорда
|
||||
❕ Для удаления вебхука введите <code>0</code>
|
||||
❕ Вы можете использовать публичный вебхук, но ответственность за его использование лежит только на вас
|
||||
▪️ Публичный вебхук: <code>{get_discord_public_webhook}</code>
|
||||
""")
|
||||
)
|
||||
@@ -0,0 +1,35 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
from aiogram import Router
|
||||
from aiogram.filters import ExceptionMessageFilter
|
||||
from aiogram.handlers import ErrorHandler
|
||||
|
||||
from tgbot.utils.misc.bot_logging import bot_logger
|
||||
|
||||
router = Router(name=__name__)
|
||||
|
||||
|
||||
# Игнорирование повторного редактирования без падения handler-а
|
||||
@router.errors(ExceptionMessageFilter(
|
||||
"Bad Request: message is not modified: specified new message content and reply markup are exactly the same as a current content and reply markup of the message")
|
||||
)
|
||||
class MessageNotModifiedHandler(ErrorHandler):
|
||||
# Debug-запись о повторном edit
|
||||
async def handle(self):
|
||||
bot_logger.debug(
|
||||
"Телеграм отказал в повторном редактировании сообщения: %s",
|
||||
self.exception_message,
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
|
||||
# Логирование всех ошибок, которые дошли до aiogram error router
|
||||
@router.errors()
|
||||
class UnknownErrorHandler(ErrorHandler):
|
||||
# Запись traceback неизвестной ошибки
|
||||
async def handle(self):
|
||||
bot_logger.error(
|
||||
"Ошибка handler-а: %s | %s",
|
||||
self.exception_name,
|
||||
self.exception_message,
|
||||
exc_info=True,
|
||||
)
|
||||
@@ -0,0 +1,37 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
from aiogram import Router, Bot, F
|
||||
from aiogram.types import CallbackQuery, Message
|
||||
|
||||
from tgbot.utils.const_functions import del_message, ded
|
||||
from tgbot.utils.misc.bot_models import FSM, ARS
|
||||
|
||||
router = Router(name=__name__)
|
||||
|
||||
|
||||
# Колбэк с удалением сообщения
|
||||
@router.callback_query(F.data == "close_this")
|
||||
async def main_missed_callback_close(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
await del_message(call.message)
|
||||
|
||||
|
||||
# Колбэк с обработкой кнопки
|
||||
@router.callback_query(F.data == "...")
|
||||
async def main_missed_callback_answer(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
await call.answer(cache_time=30)
|
||||
|
||||
|
||||
# Обработка всех колбэков которые потеряли стейты после перезапуска скрипта
|
||||
@router.callback_query()
|
||||
async def main_missed_callback(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
await call.answer("❗️ Кнопка недействительна. Повторите действия заново", True)
|
||||
|
||||
|
||||
# Обработка всех неизвестных команд
|
||||
@router.message()
|
||||
async def main_missed_message(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
await message.answer(
|
||||
ded(f"""
|
||||
♦️ Неизвестная команда
|
||||
♦️ Введите /start
|
||||
"""),
|
||||
)
|
||||
@@ -0,0 +1,146 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
from aiogram import Router, Bot, F
|
||||
from aiogram.filters import StateFilter
|
||||
from aiogram.types import Message, CallbackQuery
|
||||
|
||||
from tgbot.database import Settingsx, Positionx, Categoryx
|
||||
from tgbot.keyboards.inline_user import user_support_finl
|
||||
from tgbot.keyboards.inline_user_page import prod_item_position_swipe_fp
|
||||
from tgbot.keyboards.reply_main import menu_frep
|
||||
from tgbot.utils.const_functions import ded
|
||||
from tgbot.utils.misc.bot_filters import IsBuy, IsRefill, IsWork
|
||||
from tgbot.utils.misc.bot_models import FSM, ARS
|
||||
from tgbot.utils.text_functions import position_open_user
|
||||
|
||||
# Игнор-колбэки покупок
|
||||
prohibit_buy = (
|
||||
'buy_category_swipe',
|
||||
'buy_category_open',
|
||||
'buy_position_swipe',
|
||||
'buy_position_open',
|
||||
'buy_item_open',
|
||||
'buy_item_confirm',
|
||||
)
|
||||
|
||||
# Игнор-колбэки пополнений
|
||||
prohibit_refill = (
|
||||
'user_refill',
|
||||
'user_refill_method',
|
||||
'Pay:Cryptobot',
|
||||
'Pay:Yoomoney',
|
||||
'Pay:',
|
||||
)
|
||||
|
||||
router = Router(name=__name__)
|
||||
|
||||
|
||||
################################################################################
|
||||
########################### СТАТУС ТЕХНИЧЕСКИХ РАБОТ ###########################
|
||||
# Фильтр на технические работы - сообщение
|
||||
@router.message(IsWork())
|
||||
async def filter_work_message(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
await state.clear()
|
||||
|
||||
get_settings = await Settingsx().get()
|
||||
|
||||
if get_settings.misc_support != "None":
|
||||
return await message.answer(
|
||||
"<b>⛔ Бот находится на технических работах</b>",
|
||||
reply_markup=user_support_finl(get_settings.misc_support),
|
||||
)
|
||||
|
||||
await message.answer("<b>⛔ Бот находится на технических работах</b>")
|
||||
|
||||
|
||||
# Фильтр на технические работы - колбэк
|
||||
@router.callback_query(IsWork())
|
||||
async def filter_work_callback(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
await state.clear()
|
||||
|
||||
await call.answer("⛔ Бот находится на технических работах.", True)
|
||||
|
||||
|
||||
################################################################################
|
||||
################################# СТАТУС ПОКУПОК ###############################
|
||||
# Фильтр на доступность покупок - сообщение
|
||||
@router.message(IsBuy(), F.text == "🎁 Купить")
|
||||
@router.message(IsBuy(), StateFilter('here_item_count'))
|
||||
async def filter_buy_message(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
await state.clear()
|
||||
|
||||
await message.answer("<b>⛔ Покупки временно отключены</b>")
|
||||
|
||||
|
||||
# Фильтр на доступность покупок - колбэк
|
||||
@router.callback_query(IsBuy(), F.data.startswith(prohibit_buy))
|
||||
async def filter_buy_callback(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
await state.clear()
|
||||
|
||||
await call.answer("⛔ Покупки временно отключены.", True)
|
||||
|
||||
|
||||
################################################################################
|
||||
############################### СТАТУС ПОПОЛНЕНИЙ ##############################
|
||||
# Фильтр на доступность пополнения - сообщение
|
||||
@router.message(IsRefill(), StateFilter('here_refill_amount'))
|
||||
async def filter_refill_message(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
await state.clear()
|
||||
|
||||
await message.answer("<b>⛔ Пополнение временно отключено</b>")
|
||||
|
||||
|
||||
# Фильтр на доступность пополнения - колбэк
|
||||
@router.callback_query(IsRefill(), F.data.startswith(prohibit_refill))
|
||||
async def filter_refill_callback(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
await state.clear()
|
||||
|
||||
await call.answer("⛔ Пополнение временно отключено.", True)
|
||||
|
||||
|
||||
################################################################################
|
||||
#################################### ПРОЧЕЕ ####################################
|
||||
# Открытие главного меню
|
||||
@router.message(F.text.in_(('🔙 Главное меню', '/start')))
|
||||
async def main_start(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
await state.clear()
|
||||
|
||||
await message.answer(
|
||||
ded("""
|
||||
🔸 Бот готов к использованию.
|
||||
🔸 Если не появились вспомогательные кнопки
|
||||
🔸 Введите /start
|
||||
"""),
|
||||
reply_markup=menu_frep(message.from_user.id),
|
||||
)
|
||||
|
||||
|
||||
# Открытие диплинков
|
||||
@router.message(F.text.startswith('/start '))
|
||||
async def main_start_deeplink(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
deepling_args = message.text[7:]
|
||||
|
||||
if deepling_args.startswith("p_"):
|
||||
position_id_raw = deepling_args[2:]
|
||||
|
||||
if not position_id_raw.isdigit():
|
||||
return
|
||||
|
||||
position_id = int(position_id_raw)
|
||||
get_position = await Positionx().get(position_id=position_id)
|
||||
|
||||
if get_position is not None:
|
||||
await position_open_user(bot, message.from_user.id, position_id, 0)
|
||||
elif deepling_args.startswith("c_"):
|
||||
category_id_raw = deepling_args[2:]
|
||||
|
||||
if not category_id_raw.isdigit():
|
||||
return
|
||||
|
||||
category_id = int(category_id_raw)
|
||||
get_category = await Categoryx().get(category_id=category_id)
|
||||
|
||||
if get_category is not None:
|
||||
await message.answer(
|
||||
f"<b>🎁 Текущая категория: <code>{get_category.category_name}</code></b>",
|
||||
reply_markup=await prod_item_position_swipe_fp(0, category_id),
|
||||
)
|
||||
@@ -0,0 +1,187 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
import asyncio
|
||||
|
||||
from aiogram import Router, Bot, F
|
||||
from aiogram.filters import Command
|
||||
from aiogram.types import CallbackQuery, Message
|
||||
|
||||
from tgbot.data.config import BOT_VERSION, get_text_desc, get_text_warning
|
||||
from tgbot.database import Purchasesx, Settingsx
|
||||
from tgbot.keyboards.inline_user import user_support_finl
|
||||
from tgbot.keyboards.inline_user_page import *
|
||||
from tgbot.services.api_hosting_text import HostingAPI
|
||||
from tgbot.utils.const_functions import ded, del_message, convert_date
|
||||
from tgbot.utils.misc.bot_models import FSM, ARS
|
||||
from tgbot.utils.misc_functions import insert_tags
|
||||
from tgbot.utils.products_functions import get_items_available
|
||||
from tgbot.utils.text_functions import open_profile_user
|
||||
|
||||
router = Router(name=__name__)
|
||||
|
||||
|
||||
# Открытие товаров
|
||||
@router.message(F.text == "🎁 Купить")
|
||||
async def user_shop(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
await state.clear()
|
||||
|
||||
get_categories = await get_categories_items()
|
||||
|
||||
if len(get_categories) >= 1:
|
||||
await message.answer(
|
||||
"<b>🎁 Выберите нужный вам товар</b>",
|
||||
reply_markup=await prod_item_category_swipe_fp(0),
|
||||
)
|
||||
else:
|
||||
await message.answer("<b>🎁 Увы, товары в данное время отсутствуют</b>")
|
||||
|
||||
|
||||
# Открытие профиля
|
||||
@router.message(F.text == "👤 Профиль")
|
||||
async def user_profile(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
await state.clear()
|
||||
|
||||
await open_profile_user(bot, message.from_user.id)
|
||||
|
||||
|
||||
# Проверка товаров в наличии
|
||||
@router.message(F.text == "🧮 Наличие товаров")
|
||||
async def user_available(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
await state.clear()
|
||||
|
||||
items_available, remover_max, remover_now = await get_items_available(0)
|
||||
|
||||
if len(items_available) >= 1:
|
||||
await message.answer(
|
||||
items_available,
|
||||
reply_markup=prod_available_swipe_fp(remover_now, remover_max),
|
||||
)
|
||||
else:
|
||||
await message.answer("<b>🎁 Увы, товары в данное время отсутствуют</b>")
|
||||
|
||||
|
||||
# Открытие FAQ
|
||||
@router.message(F.text.in_(('❔ FAQ', '/faq')))
|
||||
async def user_faq(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
await state.clear()
|
||||
|
||||
get_settings = await Settingsx().get()
|
||||
|
||||
if get_settings.misc_faq == "None":
|
||||
return await message.answer(
|
||||
ded(f"""
|
||||
❔ Текст FAQ не указан. Измените его в настройках бота.
|
||||
|
||||
➖➖➖➖➖➖➖➖➖➖
|
||||
{get_text_desc()}
|
||||
|
||||
➖➖➖➖➖➖➖➖➖➖
|
||||
{get_text_warning()}
|
||||
"""),
|
||||
disable_web_page_preview=True,
|
||||
)
|
||||
|
||||
await message.answer(
|
||||
await insert_tags(message.from_user.id, get_settings.misc_faq),
|
||||
disable_web_page_preview=True,
|
||||
)
|
||||
|
||||
|
||||
# Открытие сообщения с ссылкой на поддержку
|
||||
@router.message(F.text.in_(('☎️ Поддержка', '/support')))
|
||||
async def user_support(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
await state.clear()
|
||||
|
||||
get_settings = await Settingsx().get()
|
||||
|
||||
if get_settings.misc_support == "None":
|
||||
return await message.answer(
|
||||
ded(f"""
|
||||
☎️ Контакты поддержки не указаны. Измените их в настройках бота.
|
||||
|
||||
➖➖➖➖➖➖➖➖➖➖
|
||||
{get_text_desc()}
|
||||
|
||||
➖➖➖➖➖➖➖➖➖➖
|
||||
{get_text_warning()}
|
||||
"""),
|
||||
disable_web_page_preview=True,
|
||||
)
|
||||
|
||||
await message.answer(
|
||||
"<b>☎️ Нажмите кнопку ниже для связи с Администратором</b>",
|
||||
reply_markup=user_support_finl(get_settings.misc_support),
|
||||
)
|
||||
|
||||
|
||||
# Получение версии бота
|
||||
@router.message(Command(commands=['version']))
|
||||
async def admin_version(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
await state.clear()
|
||||
|
||||
await message.answer(f"<b>❇️ Текущая версия бота: <code>{BOT_VERSION}</code></b>")
|
||||
|
||||
|
||||
# Получение информации о боте
|
||||
@router.message(Command(commands=['dj_desc']))
|
||||
async def admin_desc(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
await state.clear()
|
||||
|
||||
await message.answer(get_text_desc(), disable_web_page_preview=True)
|
||||
|
||||
|
||||
################################################################################
|
||||
# Переход к профилю
|
||||
@router.callback_query(F.data == "user_profile")
|
||||
async def user_profile_return(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
await state.clear()
|
||||
|
||||
await del_message(call.message)
|
||||
await open_profile_user(bot, call.from_user.id)
|
||||
|
||||
|
||||
# Просмотр истории покупок
|
||||
@router.callback_query(F.data == "user_purchases")
|
||||
async def user_purchases(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
get_purchases = await Purchasesx().gets(user_id=call.from_user.id)
|
||||
get_purchases = get_purchases[-5:]
|
||||
|
||||
if len(get_purchases) >= 1:
|
||||
await call.answer("🎁 Последние 5 покупок")
|
||||
await del_message(call.message)
|
||||
|
||||
for purchase in get_purchases:
|
||||
link_items = await (
|
||||
await HostingAPI.connect(
|
||||
bot=bot,
|
||||
arSession=arSession,
|
||||
)
|
||||
).upload_text(purchase.purchase_data)
|
||||
|
||||
await call.message.answer(
|
||||
ded(f"""
|
||||
<b>🧾 Чек: <code>#{purchase.purchase_receipt}</code></b>
|
||||
▪️ Товар: <code>{purchase.purchase_position_name} | {purchase.purchase_count}шт | {purchase.purchase_price}₽</code>
|
||||
▪️ Дата покупки: <code>{convert_date(purchase.purchase_unix)}</code>
|
||||
▪️ Товары: <a href='{link_items}'>кликабельно</a>
|
||||
"""),
|
||||
disable_web_page_preview=True,
|
||||
)
|
||||
|
||||
await asyncio.sleep(0.2)
|
||||
|
||||
await open_profile_user(bot, call.from_user.id)
|
||||
else:
|
||||
await call.answer("❗ У вас отсутствуют покупки", True)
|
||||
|
||||
|
||||
# Страницы наличия товаров
|
||||
@router.callback_query(F.data.startswith("user_available_swipe:"))
|
||||
async def user_available_swipe(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
remover = int(call.data.split(":")[1])
|
||||
|
||||
items_available, remover_max, remover_now = await get_items_available(remover)
|
||||
|
||||
await call.message.edit_text(
|
||||
items_available,
|
||||
reply_markup=prod_available_swipe_fp(remover_now, remover_max),
|
||||
)
|
||||
@@ -0,0 +1,276 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
import asyncio
|
||||
|
||||
from aiogram import Router, Bot, F
|
||||
from aiogram.filters import StateFilter
|
||||
from aiogram.types import CallbackQuery, Message
|
||||
|
||||
from tgbot.database import Positionx, Userx, Categoryx, Itemx, Paymentsx, Purchasesx, Settingsx
|
||||
from tgbot.keyboards.inline_user import refill_method_buy_finl
|
||||
from tgbot.keyboards.inline_user_page import *
|
||||
from tgbot.keyboards.inline_user_products import products_buy_confirm_finl, products_return_finl
|
||||
from tgbot.keyboards.reply_main import menu_frep
|
||||
from tgbot.utils.const_functions import ded, del_message, convert_date, send_admins
|
||||
from tgbot.utils.misc.bot_models import FSM, ARS
|
||||
from tgbot.utils.products_functions import get_positions_items
|
||||
from tgbot.utils.text_functions import position_open_user
|
||||
|
||||
router = Router(name=__name__)
|
||||
|
||||
|
||||
# Страницы выбора категории для покупки товара
|
||||
@router.callback_query(F.data.startswith("buy_category_swipe:"))
|
||||
async def user_buy_category_swipe(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
remover = int(call.data.split(":")[1])
|
||||
|
||||
await call.message.edit_text(
|
||||
"<b>🎁 Выберите нужный вам товар</b>",
|
||||
reply_markup=await prod_item_category_swipe_fp(remover),
|
||||
)
|
||||
|
||||
|
||||
# Открытие категории с выбором позиции для покупки товара
|
||||
@router.callback_query(F.data.startswith("buy_category_open:"))
|
||||
async def user_buy_category_open(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
category_id = int(call.data.split(":")[1])
|
||||
remover = int(call.data.split(":")[2])
|
||||
|
||||
get_category = await Categoryx().get_required(category_id=category_id)
|
||||
get_positions = await get_positions_items(category_id)
|
||||
|
||||
if len(get_positions) >= 1:
|
||||
await del_message(call.message)
|
||||
|
||||
await call.message.answer(
|
||||
f"<b>🎁 Текущая категория: <code>{get_category.category_name}</code></b>",
|
||||
reply_markup=await prod_item_position_swipe_fp(remover, category_id),
|
||||
)
|
||||
else:
|
||||
await call.answer(
|
||||
f"❕ Товары в категории {get_category.category_name} отсутствуют",
|
||||
True,
|
||||
cache_time=5,
|
||||
)
|
||||
|
||||
|
||||
# Страницы выбора позиции для покупки товара
|
||||
@router.callback_query(F.data.startswith("buy_position_swipe:"))
|
||||
async def user_buy_position_swipe(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
category_id = int(call.data.split(":")[1])
|
||||
remover = int(call.data.split(":")[2])
|
||||
|
||||
get_category = await Categoryx().get_required(category_id=category_id)
|
||||
|
||||
await del_message(call.message)
|
||||
await call.message.answer(
|
||||
f"<b>🎁 Текущая категория: <code>{get_category.category_name}</code></b>",
|
||||
reply_markup=await prod_item_position_swipe_fp(remover, category_id),
|
||||
)
|
||||
|
||||
|
||||
# Открытие позиции для покупки товара
|
||||
@router.callback_query(F.data.startswith("buy_position_open:"))
|
||||
async def user_buy_position_open(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
position_id = int(call.data.split(":")[1])
|
||||
remover = int(call.data.split(":")[2])
|
||||
|
||||
await state.clear()
|
||||
|
||||
await del_message(call.message)
|
||||
await position_open_user(bot, call.from_user.id, position_id, remover)
|
||||
|
||||
|
||||
#################################### ПОКУПКА ###################################
|
||||
# Покупка товара
|
||||
@router.callback_query(F.data.startswith("buy_item_open:"))
|
||||
async def user_buy_open(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
position_id = int(call.data.split(":")[1])
|
||||
remover = int(call.data.split(":")[2])
|
||||
|
||||
get_payments = await Paymentsx().get()
|
||||
get_position = await Positionx().get_required(position_id=position_id)
|
||||
get_items = await Itemx().gets(position_id=position_id)
|
||||
get_user = await Userx().get_required(user_id=call.from_user.id)
|
||||
|
||||
# Проверка, имеется ли на балансе пользователя достаточно средств
|
||||
if get_user.user_balance < get_position.position_price:
|
||||
if get_payments.status_cryptobot == "True" or get_payments.status_yoomoney == "True":
|
||||
await call.message.answer(
|
||||
"<b>❗ На вашем счёте недостаточно средств</b>\n"
|
||||
"💰 Выберите способ пополнения баланса",
|
||||
reply_markup=await refill_method_buy_finl(),
|
||||
)
|
||||
|
||||
return await call.answer(cache_time=5)
|
||||
else:
|
||||
return await call.answer("❗ У вас недостаточно средств. Пополните баланс", True)
|
||||
|
||||
if len(get_items) < 1:
|
||||
return await call.answer("❗ Товаров нет в наличии", True)
|
||||
|
||||
# Максимальное количество товаров к покупке, подстроенные под баланс пользователя
|
||||
if get_position.position_price != 0:
|
||||
max_buy_count = int(get_user.user_balance / get_position.position_price)
|
||||
|
||||
if max_buy_count > len(get_items):
|
||||
available_count = len(get_items)
|
||||
else:
|
||||
available_count = max_buy_count
|
||||
else:
|
||||
available_count = len(get_items)
|
||||
|
||||
# Если в наличии всего один товар, то пропустить ввод количества товаров к покупке
|
||||
if available_count == 1:
|
||||
await state.clear()
|
||||
|
||||
await del_message(call.message)
|
||||
|
||||
await call.message.answer(
|
||||
ded(f"""
|
||||
<b>🎁 Вы действительно хотите купить товар(ы)?</b>
|
||||
➖➖➖➖➖➖➖➖➖➖
|
||||
▪️ Товар: <code>{get_position.position_name}</code>
|
||||
▪️ Количество: <code>1шт</code>
|
||||
▪️ Сумма к покупке: <code>{get_position.position_price}₽</code>
|
||||
"""),
|
||||
reply_markup=products_buy_confirm_finl(position_id, get_position.category_id, 1),
|
||||
)
|
||||
else:
|
||||
await state.update_data(here_buy_position_id=position_id)
|
||||
await state.set_state("here_item_count")
|
||||
|
||||
await del_message(call.message)
|
||||
await call.message.answer(
|
||||
ded(f"""
|
||||
<b>🎁 Введите количество товаров для покупки</b>
|
||||
❕ От <code>1</code> до <code>{available_count}</code>
|
||||
➖➖➖➖➖➖➖➖➖➖
|
||||
▪️ Товар: <code>{get_position.position_name}</code> - <code>{get_position.position_price}₽</code>
|
||||
▪️ Ваш баланс: <code>{get_user.user_balance}₽</code>
|
||||
"""),
|
||||
reply_markup=products_return_finl(position_id, get_position.category_id),
|
||||
)
|
||||
|
||||
|
||||
# Принятие количества товаров для покупки
|
||||
@router.message(F.text, StateFilter("here_item_count"))
|
||||
async def user_buy_count(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
position_id = (await state.get_data())['here_buy_position_id']
|
||||
|
||||
get_position = await Positionx().get_required(position_id=position_id)
|
||||
get_user = await Userx().get_required(user_id=message.from_user.id)
|
||||
get_items = await Itemx().gets(position_id=position_id)
|
||||
|
||||
# Максимальное количество товаров к покупке, подстроенные под баланс пользователя
|
||||
if get_position.position_price != 0:
|
||||
get_count = int(get_user.user_balance / get_position.position_price)
|
||||
|
||||
if get_count > len(get_items):
|
||||
get_count = len(get_items)
|
||||
else:
|
||||
get_count = len(get_items)
|
||||
|
||||
send_message = ded(f"""
|
||||
🎁 Введите количество товаров для покупки
|
||||
❕ От <code>1</code> до <code>{get_count}</code>
|
||||
➖➖➖➖➖➖➖➖➖➖
|
||||
▪️ Товар: <code>{get_position.position_name}</code> - <code>{get_position.position_price}₽</code>
|
||||
▪️ Ваш баланс: <code>{get_user.user_balance}₽</code>
|
||||
""")
|
||||
|
||||
# Если было введено не число
|
||||
if not message.text.isdigit():
|
||||
return await message.answer(
|
||||
f"<b>❌ Данные были введены неверно</b>\n" + send_message,
|
||||
reply_markup=products_return_finl(position_id, get_position.category_id),
|
||||
)
|
||||
|
||||
get_count = int(message.text)
|
||||
amount_pay = round(get_position.position_price * get_count, 2)
|
||||
|
||||
# Если товаров нет в наличии
|
||||
if len(get_items) < 1:
|
||||
await state.clear()
|
||||
return await message.answer("<b>🎁 Товар который вы хотели купить, закончился</b>")
|
||||
|
||||
# Если введено кол-во товаров меньше 1 или меньше кол-ва имеющегося в наличии
|
||||
if get_count < 1 or get_count > len(get_items):
|
||||
return await message.answer(
|
||||
f"<b>❌ Неверное количество товаров</b>\n" + send_message,
|
||||
reply_markup=products_return_finl(position_id, get_position.category_id),
|
||||
)
|
||||
|
||||
# Если баланс пользователя меньше, чем общая цена покупки
|
||||
if get_user.user_balance < amount_pay:
|
||||
return await message.answer(
|
||||
f"<b>❌ Недостаточно средств на счете</b>\n" + send_message,
|
||||
reply_markup=products_return_finl(position_id, get_position.category_id),
|
||||
)
|
||||
|
||||
await state.clear()
|
||||
|
||||
await message.answer(
|
||||
ded(f"""
|
||||
<b>🎁 Вы действительно хотите купить товар(ы)?</b>
|
||||
➖➖➖➖➖➖➖➖➖➖
|
||||
▪️ Товар: <code>{get_position.position_name}</code>
|
||||
▪️ Количество: <code>{get_count}шт</code>
|
||||
▪️ Сумма к покупке: <code>{amount_pay}₽</code>
|
||||
"""),
|
||||
reply_markup=products_buy_confirm_finl(position_id, get_position.category_id, get_count),
|
||||
)
|
||||
|
||||
|
||||
# Подтверждение покупки товара
|
||||
@router.callback_query(F.data.startswith("buy_item_confirm:"))
|
||||
async def user_buy_confirm(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
position_id = int(call.data.split(":")[1])
|
||||
purchase_count = int(call.data.split(":")[2])
|
||||
|
||||
await call.message.edit_text("⏳")
|
||||
|
||||
purchase_result = await Purchasesx().buy(
|
||||
user_id=call.from_user.id,
|
||||
position_id=position_id,
|
||||
requested_count=purchase_count,
|
||||
)
|
||||
|
||||
if purchase_result == "USER_NOT_FOUND":
|
||||
return await call.message.edit_text("<b>❌ Пользователь не был найден</b>")
|
||||
elif purchase_result == "POSITION_NOT_FOUND":
|
||||
return await call.message.edit_text("<b>❌ Позиция не была найдена</b>")
|
||||
elif purchase_result == "NOT_ENOUGH_ITEMS":
|
||||
return await call.message.edit_text("<b>❌ В наличии недостаточно товаров. Попробуйте другое кол-во</b>")
|
||||
elif purchase_result == "NOT_ENOUGH_BALANCE":
|
||||
return await call.message.edit_text("<b>❌ На вашем балансе недостаточно средств</b>")
|
||||
|
||||
get_user = await Userx().get_required(user_id=call.from_user.id)
|
||||
get_settings = await Settingsx().get()
|
||||
|
||||
for items in purchase_result.items:
|
||||
await call.message.answer("\n\n".join(items), parse_mode="None")
|
||||
await asyncio.sleep(0.3)
|
||||
|
||||
await del_message(call.message)
|
||||
await call.message.answer(
|
||||
ded(f"""
|
||||
<b>✅ Вы успешно купили товар(ы)</b>
|
||||
➖➖➖➖➖➖➖➖➖➖
|
||||
▪️ Чек: <code>#{purchase_result.receipt}</code>
|
||||
▪️ Товар: <code>{purchase_result.position_name} | {purchase_result.purchase_count}шт | {purchase_result.purchase_price}₽</code>
|
||||
▪️ Дата покупки: <code>{convert_date(purchase_result.purchase_unix)}</code>
|
||||
"""),
|
||||
reply_markup=menu_frep(call.from_user.id),
|
||||
)
|
||||
|
||||
if get_settings.notification_buy == "True":
|
||||
await send_admins(
|
||||
bot,
|
||||
ded(f"""
|
||||
<b>🎁 Покупка товара</b>
|
||||
|
||||
▪️ Пользователь: <b>@{get_user.user_login}</b> | <a href='tg://user?id={get_user.user_id}'>{get_user.user_name}</a> | <code>{get_user.user_id}</code>
|
||||
▪️ Товар: <code>{purchase_result.position_name} | {purchase_result.purchase_count}шт | {purchase_result.purchase_price}₽</code>
|
||||
▪️ Чек: <code>#{purchase_result.receipt}</code>
|
||||
""")
|
||||
)
|
||||
@@ -0,0 +1,407 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from aiogram import Router, Bot, F
|
||||
from aiogram.filters import StateFilter
|
||||
from aiogram.types import CallbackQuery, Message, PreCheckoutQuery
|
||||
|
||||
from tgbot.database import Paymentsx, Refillx, Userx, Settingsx
|
||||
from tgbot.keyboards.inline_user import refill_bill_finl, refill_method_finl
|
||||
from tgbot.services.api_cryptobot import CryptobotAPI
|
||||
from tgbot.services.api_stars import StarsAPI
|
||||
from tgbot.services.api_yoomoney import YoomoneyAPI
|
||||
from tgbot.utils.const_functions import is_number, to_number, gen_id, ded
|
||||
from tgbot.utils.misc.bot_logging import bot_logger
|
||||
from tgbot.utils.misc.bot_models import FSM, ARS
|
||||
from tgbot.utils.misc_functions import send_admins
|
||||
|
||||
min_refill_rub = 5 # Минимальная сумма пополнения в рублях
|
||||
|
||||
router = Router(name=__name__)
|
||||
|
||||
|
||||
# Выбор способа пополнения
|
||||
@router.callback_query(F.data == "user_refill")
|
||||
async def refill_method_list(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
get_payments = await Paymentsx().get()
|
||||
|
||||
if (
|
||||
get_payments.status_cryptobot == "False" and
|
||||
get_payments.status_yoomoney == "False" and
|
||||
get_payments.status_stars == "False"
|
||||
):
|
||||
return await call.answer("❗️ Пополнения временно недоступны", True)
|
||||
|
||||
await call.message.edit_text(
|
||||
"<b>💰 Выберите способ пополнения баланса</b>",
|
||||
reply_markup=await refill_method_finl(),
|
||||
)
|
||||
|
||||
|
||||
# Выбор способа пополнения
|
||||
@router.callback_query(F.data.startswith("user_refill_method:"))
|
||||
async def refill_method_select(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
refill_method = call.data.split(":")[1]
|
||||
|
||||
get_payments = await Paymentsx().get()
|
||||
|
||||
if refill_method == "Cryptobot" and get_payments.status_cryptobot == "False":
|
||||
return await call.answer("❌ Пополнение данным способом временно недоступно", True)
|
||||
elif refill_method == "Yoomoney" and get_payments.status_yoomoney == "False":
|
||||
return await call.answer("❌ Пополнение данным способом временно недоступно", True)
|
||||
elif refill_method == "Stars" and get_payments.status_stars == "False":
|
||||
return await call.answer("❌ Пополнение данным способом временно недоступно", True)
|
||||
|
||||
await state.update_data(here_refill_method=refill_method)
|
||||
|
||||
await state.set_state("here_refill_amount")
|
||||
await call.message.edit_text("<b>💰 Введите сумму пополнения</b>")
|
||||
|
||||
|
||||
################################################################################
|
||||
################################### ВВОД СУММЫ #################################
|
||||
# Принятие суммы для пополнения средств
|
||||
@router.message(F.text, StateFilter("here_refill_amount"))
|
||||
async def refill_amount_get(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
if not is_number(message.text):
|
||||
return await message.answer(
|
||||
ded(f"""
|
||||
<b>❌ Данные были введены неверно</b>
|
||||
💰 Введите сумму для пополнения средств
|
||||
"""),
|
||||
)
|
||||
|
||||
if to_number(message.text) < min_refill_rub or to_number(message.text) > 150_000:
|
||||
return await message.answer(
|
||||
ded(f"""
|
||||
<b>❌ Неверная сумма пополнения</b>
|
||||
❗️ Cумма не должна быть меньше <code>{min_refill_rub}₽</code> и больше <code>150 000₽</code>
|
||||
💰 Введите сумму для пополнения средств
|
||||
"""),
|
||||
)
|
||||
|
||||
cache_message = await message.answer("<b>♻️ Подождите, платёж генерируется..</b>")
|
||||
|
||||
refill_amount = to_number(message.text)
|
||||
refill_method = (await state.get_data())['here_refill_method']
|
||||
await state.clear()
|
||||
|
||||
# Генерация платежа
|
||||
if refill_method == "Cryptobot":
|
||||
bill_message, bill_link, bill_receipt = await (
|
||||
await CryptobotAPI.connect(
|
||||
bot=bot,
|
||||
arSession=arSession,
|
||||
update=cache_message,
|
||||
)
|
||||
).bill(refill_amount)
|
||||
elif refill_method == "Yoomoney":
|
||||
bill_message, bill_link, bill_receipt = await (
|
||||
await YoomoneyAPI.connect(
|
||||
bot=bot,
|
||||
arSession=arSession,
|
||||
update=cache_message,
|
||||
)
|
||||
).bill(refill_amount)
|
||||
elif refill_method == "Stars":
|
||||
bill_message, bill_link, bill_receipt = await (
|
||||
await StarsAPI.connect(
|
||||
bot=bot,
|
||||
arSession=arSession,
|
||||
update=cache_message,
|
||||
)
|
||||
).bill(refill_amount)
|
||||
else:
|
||||
return await cache_message.edit_text(
|
||||
f"<b>❌ Данный способ пополнения не найден. Попробуйте позже: {refill_method}</b>"
|
||||
)
|
||||
|
||||
# Обработка статуса генерации платежа
|
||||
if bill_message:
|
||||
await cache_message.edit_text(
|
||||
bill_message,
|
||||
reply_markup=refill_bill_finl(bill_link, bill_receipt, refill_method),
|
||||
)
|
||||
else:
|
||||
await cache_message.edit_text(
|
||||
f"<b>❌ Не удалось сгенерировать платёж. Попробуйте позже</b>"
|
||||
)
|
||||
|
||||
|
||||
################################################################################
|
||||
############################### ПРОВЕРКА ПЛАТЕЖЕЙ ##############################
|
||||
# Проверка оплаты - ЮMoney
|
||||
@router.callback_query(F.data.startswith('Pay:Yoomoney'))
|
||||
async def refill_check_yoomoney(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
pay_method = call.data.split(":")[1]
|
||||
pay_receipt = call.data.split(":")[2]
|
||||
|
||||
pay_status, pay_amount = await (
|
||||
await YoomoneyAPI.connect(
|
||||
bot=bot,
|
||||
arSession=arSession,
|
||||
update=call,
|
||||
)
|
||||
).bill_check(pay_receipt)
|
||||
|
||||
if pay_status == 0:
|
||||
refill_status = await refill_success(
|
||||
bot=bot,
|
||||
call=call,
|
||||
pay_method=pay_method,
|
||||
pay_amount=pay_amount,
|
||||
pay_receipt=int(pay_receipt),
|
||||
pay_comment=pay_receipt,
|
||||
)
|
||||
|
||||
if refill_status == "ALREADY":
|
||||
await call.answer("❗ Ваше пополнение уже зачислено.", True, cache_time=60)
|
||||
await delete_refill_message(bot, call.message.chat.id, call.message.message_id)
|
||||
elif refill_status == "USER_NOT_FOUND":
|
||||
await call.answer("❗ Пользователь не найден. Напишите в поддержку.", True, cache_time=30)
|
||||
elif pay_status == 1:
|
||||
await call.answer("❗️ Не удалось проверить платёж. Попробуйте позже", True, cache_time=30)
|
||||
elif pay_status == 2:
|
||||
await call.answer("❗️ Оплата не была найдена. Попробуйте позже", True, cache_time=5)
|
||||
elif pay_status == 3:
|
||||
await call.answer("❗️ Оплата была произведена не в рублях", True, cache_time=5)
|
||||
else:
|
||||
await call.answer(f"❗ Неизвестная ошибка {pay_status}. Обратитесь в поддержку.", True, cache_time=5)
|
||||
|
||||
|
||||
# Проверка оплаты - Cryptobot
|
||||
@router.callback_query(F.data.startswith('Pay:Cryptobot'))
|
||||
async def refill_check_cryptobot(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
pay_method = call.data.split(":")[1]
|
||||
pay_comment = call.data.split(":")[2]
|
||||
|
||||
pay_status, pay_amount = await (
|
||||
await CryptobotAPI.connect(
|
||||
bot=bot,
|
||||
arSession=arSession,
|
||||
update=call,
|
||||
)
|
||||
).bill_check(pay_comment)
|
||||
|
||||
if pay_status == 0:
|
||||
refill_status = await refill_success(
|
||||
bot=bot,
|
||||
call=call,
|
||||
pay_method=pay_method,
|
||||
pay_amount=pay_amount,
|
||||
pay_comment=pay_comment,
|
||||
)
|
||||
|
||||
if refill_status == "ALREADY":
|
||||
await call.answer("❗ Ваше пополнение уже зачислено.", True, cache_time=60)
|
||||
await delete_refill_message(bot, call.message.chat.id, call.message.message_id)
|
||||
elif refill_status == "USER_NOT_FOUND":
|
||||
await call.answer("❗ Пользователь не найден. Напишите в поддержку.", True, cache_time=30)
|
||||
elif pay_status == 1:
|
||||
await call.answer("❗️ Не удалось проверить платёж. Попробуйте позже", True, cache_time=30)
|
||||
elif pay_status == 2:
|
||||
await call.answer("❗️ Оплата не была найдена. Попробуйте позже", True, cache_time=5)
|
||||
elif pay_status == 3:
|
||||
await call.answer("❗️ Вы не успели оплатить счёт", True, cache_time=5)
|
||||
await call.message.edit_reply_markup()
|
||||
else:
|
||||
await call.answer(f"❗ Неизвестная ошибка {pay_status}. Обратитесь в поддержку.", True, cache_time=5)
|
||||
|
||||
|
||||
# Проверка оплаты - Звёзды
|
||||
@router.callback_query(F.data.startswith('Pay:Stars'))
|
||||
async def refill_check_stars(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
pay_method = call.data.split(":")[1]
|
||||
pay_receipt = int(call.data.split(":")[2])
|
||||
|
||||
pay_status, pay_amount = await (
|
||||
await StarsAPI.connect(
|
||||
bot=bot,
|
||||
arSession=arSession,
|
||||
update=call,
|
||||
)
|
||||
).bill_check(pay_receipt)
|
||||
|
||||
if pay_status == 0:
|
||||
refill_status = await refill_success(
|
||||
bot=bot,
|
||||
call=call,
|
||||
pay_method=pay_method,
|
||||
pay_amount=pay_amount,
|
||||
pay_receipt=pay_receipt,
|
||||
)
|
||||
|
||||
if refill_status == "ALREADY":
|
||||
await call.answer("❗ Ваше пополнение уже зачислено.", True, cache_time=60)
|
||||
await delete_refill_message(bot, call.message.chat.id, call.message.message_id)
|
||||
elif refill_status == "USER_NOT_FOUND":
|
||||
await call.answer("❗ Пользователь не найден. Напишите в поддержку.", True, cache_time=30)
|
||||
elif pay_status == 1:
|
||||
await call.answer("❗️ Не удалось проверить платёж. Попробуйте позже", True, cache_time=30)
|
||||
elif pay_status == 2:
|
||||
await call.answer("❗️ Оплата не была найдена. Попробуйте позже", True, cache_time=5)
|
||||
else:
|
||||
await call.answer(f"❗ Неизвестная ошибка {pay_status}. Обратитесь в поддержку.", True, cache_time=5)
|
||||
|
||||
|
||||
################################################################################
|
||||
#################################### ЗВЁЗДЫ ####################################
|
||||
# Подтверждение платежа Telegram Stars
|
||||
@router.pre_checkout_query()
|
||||
async def refill_stars_pre_checkout(query: PreCheckoutQuery, bot: Bot, state: FSM, arSession: ARS):
|
||||
await (
|
||||
await StarsAPI.connect(
|
||||
bot=bot,
|
||||
arSession=arSession,
|
||||
update=query,
|
||||
)
|
||||
).answer_pre_checkout(query)
|
||||
|
||||
|
||||
# Автоматическое зачисление Telegram Stars
|
||||
@router.message(F.successful_payment)
|
||||
async def refill_stars_success(message: Message, bot: Bot, state: FSM, arSession: ARS):
|
||||
payment = message.successful_payment
|
||||
|
||||
if payment is None:
|
||||
return
|
||||
|
||||
stars_api = await StarsAPI.connect(
|
||||
bot=bot,
|
||||
arSession=arSession,
|
||||
update=message,
|
||||
)
|
||||
|
||||
try:
|
||||
pay_receipt, pay_amount, pay_comment, bill_chat_id, bill_message_id = (
|
||||
stars_api.parse_successful_payment(payment)
|
||||
)
|
||||
except ValueError:
|
||||
bot_logger.warning("Некорректная successful_payment для Telegram Stars", exc_info=True)
|
||||
return
|
||||
|
||||
refill_status, receipt = await save_refill_success(
|
||||
bot=bot,
|
||||
user_id=message.from_user.id,
|
||||
pay_method="Stars",
|
||||
pay_amount=pay_amount,
|
||||
pay_receipt=pay_receipt,
|
||||
pay_comment=pay_comment,
|
||||
)
|
||||
|
||||
if refill_status == "ok":
|
||||
if bill_chat_id == message.chat.id:
|
||||
await delete_refill_message(bot, bill_chat_id, bill_message_id)
|
||||
|
||||
await message.answer(
|
||||
ded(f"""
|
||||
<b>💰 Вы пополнили баланс на сумму <code>{pay_amount}₽</code>. Удачи ❤️
|
||||
🧾 Чек: <code>#{receipt}</code></b>
|
||||
""")
|
||||
)
|
||||
elif refill_status == "ALREADY":
|
||||
if bill_chat_id == message.chat.id:
|
||||
await delete_refill_message(bot, bill_chat_id, bill_message_id)
|
||||
|
||||
await message.answer("<b>❗ Ваше пополнение уже было зачислено.</b>")
|
||||
elif refill_status == "USER_NOT_FOUND":
|
||||
await message.answer("<b>❗ Пользователь не найден. Напишите в поддержку.</b>")
|
||||
|
||||
|
||||
################################################################################
|
||||
#################################### ПРОЧЕЕ ####################################
|
||||
# Зачисление средств
|
||||
async def refill_success(
|
||||
bot: Bot,
|
||||
call: CallbackQuery,
|
||||
pay_method: str,
|
||||
pay_amount: float,
|
||||
pay_receipt: Optional[int] = None,
|
||||
pay_comment: Optional[str] = None,
|
||||
):
|
||||
user_id = call.from_user.id
|
||||
|
||||
if pay_receipt is None:
|
||||
pay_receipt = gen_id(12)
|
||||
if pay_comment is None:
|
||||
pay_comment = ""
|
||||
|
||||
response_success, receipt = await save_refill_success(
|
||||
bot=bot,
|
||||
user_id=user_id,
|
||||
pay_method=pay_method,
|
||||
pay_amount=pay_amount,
|
||||
pay_receipt=pay_receipt,
|
||||
pay_comment=pay_comment,
|
||||
)
|
||||
|
||||
if response_success != "ok":
|
||||
return response_success
|
||||
|
||||
await call.message.answer(
|
||||
ded(f"""
|
||||
<b>💰 Вы пополнили баланс на сумму <code>{pay_amount}₽</code>. Удачи ❤️
|
||||
🧾 Чек: <code>#{receipt}</code></b>
|
||||
""")
|
||||
)
|
||||
await delete_refill_message(bot, call.message.chat.id, call.message.message_id)
|
||||
|
||||
return response_success
|
||||
|
||||
|
||||
# Удаление сообщения со счётом
|
||||
async def delete_refill_message(bot: Bot, chat_id: Optional[int], message_id: Optional[int]) -> None:
|
||||
if chat_id is None or message_id is None:
|
||||
return
|
||||
|
||||
try:
|
||||
await bot.delete_message(chat_id=chat_id, message_id=message_id)
|
||||
except Exception:
|
||||
bot_logger.warning("Не удалось удалить сообщение со счётом", exc_info=True)
|
||||
|
||||
|
||||
# Сохранение пополнения и уведомление админов
|
||||
async def save_refill_success(
|
||||
bot: Bot,
|
||||
user_id: int,
|
||||
pay_method: str,
|
||||
pay_amount: float,
|
||||
pay_receipt: int,
|
||||
pay_comment: str,
|
||||
) -> Tuple[str, int]:
|
||||
if pay_method == "Yoomoney":
|
||||
text_method = "ЮMoney"
|
||||
elif pay_method == "Cryptobot":
|
||||
text_method = "CryptoBot"
|
||||
elif pay_method == "Stars":
|
||||
text_method = "Telegram Stars"
|
||||
else:
|
||||
text_method = f"Unknown - {pay_method}"
|
||||
|
||||
response_success = await Refillx().success(
|
||||
user_id=user_id,
|
||||
pay_receipt=pay_receipt,
|
||||
pay_comment=pay_comment,
|
||||
pay_amount=pay_amount,
|
||||
pay_method=pay_method,
|
||||
)
|
||||
|
||||
if response_success != "ok":
|
||||
return response_success, pay_receipt
|
||||
|
||||
get_user = await Userx().get_required(user_id=user_id)
|
||||
get_settings = await Settingsx().get()
|
||||
|
||||
if get_settings.notification_refill == "True":
|
||||
await send_admins(
|
||||
bot,
|
||||
ded(f"""
|
||||
<b>💰 Пополнение баланса</b>
|
||||
|
||||
▪️ Пользователь: <b>@{get_user.user_login}</b> | <a href='tg://user?id={user_id}'>{get_user.user_name}</a> | <code>{user_id}</code>
|
||||
▪️ Сумма пополнения: <code>{pay_amount}₽</code> <code>({text_method})</code>
|
||||
▪️ Чек: <code>#{pay_receipt}</code>
|
||||
""")
|
||||
)
|
||||
|
||||
return response_success, pay_receipt
|
||||
@@ -0,0 +1,219 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
import json
|
||||
from typing import Any, Dict, Optional, Tuple, Union
|
||||
|
||||
from aiogram import Bot
|
||||
from aiogram.types import CallbackQuery, Message
|
||||
from aiohttp import ClientConnectorCertificateError
|
||||
|
||||
from tgbot.database import Paymentsx
|
||||
from tgbot.utils.const_functions import ded, to_number
|
||||
from tgbot.utils.misc.bot_logging import bot_logger
|
||||
from tgbot.utils.misc.bot_models import ARS
|
||||
from tgbot.utils.misc_functions import send_admins
|
||||
|
||||
# Список поддерживаемых валют
|
||||
ALLOW_CURRENCIES = ['BTC', 'ETH', 'LTC', 'USDT', 'USDC', 'TRX', 'TON', 'BNB', 'SOL', 'DOGE']
|
||||
|
||||
|
||||
# АПИ для работы с CryptoBot
|
||||
class CryptobotAPI:
|
||||
# Настройка клиента CryptoBot
|
||||
def __init__(
|
||||
self,
|
||||
bot: Bot,
|
||||
arSession: ARS,
|
||||
update: Optional[Union[Message, CallbackQuery]] = None,
|
||||
token: str = "None",
|
||||
adding: bool = False,
|
||||
skipping_error: bool = False,
|
||||
):
|
||||
self.bot = bot
|
||||
self.arSession = arSession
|
||||
self.update = update
|
||||
self.token = token
|
||||
self.adding = adding
|
||||
self.skipping_error = skipping_error
|
||||
|
||||
# Инициализация данных
|
||||
@classmethod
|
||||
async def connect(
|
||||
cls,
|
||||
bot: Bot,
|
||||
arSession: ARS,
|
||||
update: Optional[Union[Message, CallbackQuery]] = None,
|
||||
token: Optional[str] = None,
|
||||
skipping_error: bool = False,
|
||||
) -> "CryptobotAPI":
|
||||
adding = token is not None
|
||||
|
||||
if token is None:
|
||||
payments = await Paymentsx().get()
|
||||
token = payments.cryptobot_token
|
||||
adding = False
|
||||
|
||||
return cls(
|
||||
bot=bot,
|
||||
arSession=arSession,
|
||||
update=update,
|
||||
token=token,
|
||||
adding=adding,
|
||||
skipping_error=skipping_error,
|
||||
)
|
||||
|
||||
# Уведомления о неработоспособности кассы/кошелька
|
||||
async def error_notification(self, error_code: str = "Unknown"):
|
||||
bot_logger.warning("CryptoBot недоступен: %s", error_code)
|
||||
|
||||
if not self.skipping_error:
|
||||
if self.adding and self.update is not None:
|
||||
await self.update.edit_text(
|
||||
f"<b>🔷 Не удалось добавить CryptoBot кассу ❌</b>\n"
|
||||
f"❗️ Ошибка: <code>{error_code}</code>"
|
||||
)
|
||||
else:
|
||||
await send_admins(
|
||||
self.bot,
|
||||
f"<b>🔷 CryptoBot недоступен. Как можно быстрее его замените</b>\n"
|
||||
f"❗️ Ошибка: <code>{error_code}</code>"
|
||||
)
|
||||
|
||||
# Проверка кассы/кошелька
|
||||
async def check(self) -> Tuple[bool, str]:
|
||||
status, response = await self._request("getMe")
|
||||
|
||||
if status and response['ok']:
|
||||
return True, ded(f"""
|
||||
<b>🔷 CryptoBot кошелёк полностью функционирует ✅</b>
|
||||
➖➖➖➖➖➖➖➖➖➖
|
||||
▪️ Токен: <code>{self.token}</code>
|
||||
▪️ Айди: <code>{response['result']['app_id']}</code>
|
||||
▪️ Имя: <code>{response['result']['name']}</code>
|
||||
""")
|
||||
|
||||
return False, "<b>🔷 Не удалось проверить CryptoBot кошелёк ❌</b>"
|
||||
|
||||
# Получение баланса
|
||||
async def balance(self) -> str:
|
||||
status, response = await self._request("getBalance")
|
||||
|
||||
if status and response['ok']:
|
||||
save_currencies = []
|
||||
|
||||
response_balances = sorted(
|
||||
response['result'],
|
||||
reverse=True,
|
||||
key=lambda balance: to_number(balance['available']),
|
||||
)
|
||||
|
||||
for currency in response_balances:
|
||||
if currency['currency_code'] in ALLOW_CURRENCIES:
|
||||
save_currencies.append(
|
||||
f"▪️ {currency['currency_code']}: <code>{currency['available']}</code>"
|
||||
)
|
||||
|
||||
save_currencies = "\n".join(save_currencies)
|
||||
|
||||
return ded(f"""
|
||||
<b>🔷 Баланс CryptoBot кошелька составляет</b>
|
||||
➖➖➖➖➖➖➖➖➖➖
|
||||
{save_currencies}
|
||||
""")
|
||||
|
||||
return "<b>🔷 Не удалось получить баланс CryptoBot кошелька ❌</b>"
|
||||
|
||||
# Создание счета на оплату
|
||||
async def bill(self, pay_amount: Union[float, int]) -> Tuple[Union[str, bool], str, str]:
|
||||
assets_currencies = ",".join(ALLOW_CURRENCIES)
|
||||
|
||||
payload = {
|
||||
'currency_type': 'fiat',
|
||||
'fiat': 'RUB',
|
||||
'amount': str(pay_amount),
|
||||
'expires_in': 10800,
|
||||
'accepted_assets': assets_currencies
|
||||
}
|
||||
|
||||
status, response = await self._request("createInvoice", payload)
|
||||
|
||||
if status and response['ok']:
|
||||
bill_message = ded(f"""
|
||||
<b>💰 Пополнение баланса</b>
|
||||
➖➖➖➖➖➖➖➖➖➖
|
||||
▪️ Для пополнения баланса, нажмите на кнопку ниже
|
||||
<code>Перейти к оплате</code> и оплатите выставленный вам счёт
|
||||
▪️ У вас имеется 3 часа на оплату счета
|
||||
▪️ Сумма пополнения: <code>{pay_amount}₽</code>
|
||||
➖➖➖➖➖➖➖➖➖➖
|
||||
❗️ После оплаты, нажмите на <code>Проверить оплату</code>
|
||||
""")
|
||||
|
||||
return bill_message, response['result']['mini_app_invoice_url'], response['result']['invoice_id']
|
||||
|
||||
return False, "", ""
|
||||
|
||||
# Проверка счета на оплату
|
||||
async def bill_check(self, bill_receipt: Optional[Union[str, int]] = None, records: int = 1) -> Tuple[int, float]:
|
||||
payload = {
|
||||
'invoice_ids': f'{bill_receipt}',
|
||||
'fiat': 'RUB',
|
||||
}
|
||||
|
||||
status, response = await self._request("getInvoices", payload)
|
||||
|
||||
pay_status, pay_amount = 1, 0
|
||||
|
||||
if status and response['ok']:
|
||||
get_invoice = response['result']['items'][0]
|
||||
|
||||
if get_invoice['status'] == "active":
|
||||
pay_status = 2
|
||||
elif get_invoice['status'] == "expired":
|
||||
pay_status = 3
|
||||
else:
|
||||
pay_status = 0
|
||||
pay_amount = to_number(get_invoice['amount'])
|
||||
|
||||
return pay_status, pay_amount
|
||||
|
||||
# Генерация запроса
|
||||
async def _request(
|
||||
self,
|
||||
method: str,
|
||||
data: Optional[Dict[str, Any]] = None,
|
||||
) -> Tuple[bool, Any]:
|
||||
session = await self.arSession.get_session()
|
||||
|
||||
base_url = 'https://pay.crypt.bot/api/'
|
||||
headers = {
|
||||
'Crypto-Pay-API-Token': self.token,
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
}
|
||||
|
||||
url = base_url + method
|
||||
|
||||
try:
|
||||
response = await session.post(
|
||||
url=url,
|
||||
headers=headers,
|
||||
data=data,
|
||||
)
|
||||
|
||||
response_data = json.loads((await response.read()).decode())
|
||||
|
||||
if response.status == 200:
|
||||
return True, response_data
|
||||
else:
|
||||
await self.error_notification(f"{response.status} - {str(response_data)}")
|
||||
|
||||
return False, response_data
|
||||
except ClientConnectorCertificateError:
|
||||
bot_logger.warning("Ошибка SSL при запросе CryptoBot", exc_info=True)
|
||||
await self.error_notification("CERTIFICATE_VERIFY_FAILED")
|
||||
|
||||
return False, "CERTIFICATE_VERIFY_FAILED"
|
||||
except Exception as ex:
|
||||
bot_logger.warning("Ошибка запроса CryptoBot", exc_info=True)
|
||||
await self.error_notification(str(ex))
|
||||
|
||||
return False, str(ex)
|
||||
@@ -0,0 +1,239 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
import asyncio
|
||||
import json
|
||||
from io import BytesIO
|
||||
from typing import Any, Dict, Optional, Tuple, Union
|
||||
|
||||
import ujson
|
||||
from aiogram import Bot
|
||||
from aiogram.types import CallbackQuery, Message
|
||||
from aiohttp import ClientConnectorCertificateError, FormData
|
||||
|
||||
from tgbot.database import Settingsx
|
||||
from tgbot.utils.const_functions import gen_id, send_errors
|
||||
from tgbot.utils.misc.bot_logging import bot_logger
|
||||
from tgbot.utils.misc.bot_models import ARS
|
||||
from tgbot.utils.misc_functions import send_admins
|
||||
|
||||
|
||||
class DiscordAPI:
|
||||
# Настройка клиента Discord webhook
|
||||
def __init__(
|
||||
self,
|
||||
bot: Bot,
|
||||
arSession: ARS,
|
||||
update: Optional[Union[Message, CallbackQuery]] = None,
|
||||
webhook_url: str = "None",
|
||||
adding: bool = False,
|
||||
skipping_error: bool = False,
|
||||
):
|
||||
self.bot = bot
|
||||
self.arSession = arSession
|
||||
self.update = update
|
||||
self.adding = adding
|
||||
self.skipping_error = skipping_error
|
||||
|
||||
self.webhook_username = "Djimbo Shop | Free Bot"
|
||||
self.base_url = "https://djimbo.dev/dsapi"
|
||||
|
||||
webhook_url = self._normalize_webhook_url(webhook_url)
|
||||
|
||||
if "/" in webhook_url:
|
||||
self.webhook_id = webhook_url.split("/")[0]
|
||||
self.webhook_token = webhook_url.split("/")[1]
|
||||
else:
|
||||
self.webhook_id = ""
|
||||
self.webhook_token = ""
|
||||
|
||||
# Нормализация Discord webhook URL
|
||||
@staticmethod
|
||||
def _normalize_webhook_url(webhook_url: str) -> str:
|
||||
if webhook_url is None:
|
||||
return "None"
|
||||
|
||||
if webhook_url.startswith("https://discord.com/api/"):
|
||||
webhook_url = webhook_url[33:]
|
||||
if webhook_url.startswith("discord.com/api/webhooks/"):
|
||||
webhook_url = webhook_url[25:]
|
||||
|
||||
return webhook_url
|
||||
|
||||
# Инициализация данных
|
||||
@classmethod
|
||||
async def connect(
|
||||
cls,
|
||||
bot: Bot,
|
||||
arSession: ARS,
|
||||
update: Optional[Union[Message, CallbackQuery]] = None,
|
||||
webhook_url: Optional[str] = None,
|
||||
skipping_error: bool = False,
|
||||
) -> "DiscordAPI":
|
||||
adding = webhook_url is not None
|
||||
|
||||
if webhook_url is None:
|
||||
settings = await Settingsx().get()
|
||||
webhook_url = settings.misc_discord_webhook_url
|
||||
adding = False
|
||||
|
||||
return cls(
|
||||
bot=bot,
|
||||
arSession=arSession,
|
||||
update=update,
|
||||
webhook_url=webhook_url,
|
||||
adding=adding,
|
||||
skipping_error=skipping_error,
|
||||
)
|
||||
|
||||
# Рассылка админам о нерабочем вебхуке
|
||||
async def error_account_admin(self, error_code: str = "Unknown"):
|
||||
bot_logger.warning("Discord webhook недоступен: %s", error_code)
|
||||
|
||||
if not self.skipping_error:
|
||||
await send_admins(
|
||||
self.bot,
|
||||
f"<b>🧿 Дискорд вебхук недоступен. Как можно быстрее его замените</b>\n"
|
||||
f"❗️ Ошибка: <code>{error_code}</code>"
|
||||
)
|
||||
|
||||
# Проверка вебхука
|
||||
async def check(self) -> Tuple[bool, str]:
|
||||
request_url = f"{self.base_url}/webhooks/{self.webhook_id}/{self.webhook_token}"
|
||||
|
||||
status, response = await self._request(
|
||||
request_url=request_url,
|
||||
request_method="GET",
|
||||
)
|
||||
|
||||
if status and "channel_id" in response:
|
||||
discord_channel_id = response['channel_id']
|
||||
discord_hook_name = response['name']
|
||||
|
||||
return True, discord_hook_name
|
||||
|
||||
return False, ""
|
||||
|
||||
# Загрузка фотографий
|
||||
async def upload_photo(self, photo_data: Union[BytesIO, bytes], photo_name: Optional[str] = None) -> Tuple[
|
||||
bool, str]:
|
||||
request_url = f"{self.base_url}/webhooks/{self.webhook_id}/{self.webhook_token}"
|
||||
|
||||
if photo_name is None:
|
||||
photo_name = str(gen_id(24))
|
||||
|
||||
if not photo_name.endswith(".png") and not photo_name.endswith(".jpg"):
|
||||
photo_name = f"{photo_name}.png"
|
||||
|
||||
send_json = {
|
||||
'username': self.webhook_username,
|
||||
'content': '',
|
||||
}
|
||||
|
||||
data = FormData()
|
||||
data.add_field('file', photo_data, filename=photo_name)
|
||||
data.add_field('payload_json', ujson.dumps(send_json))
|
||||
|
||||
await asyncio.sleep(1)
|
||||
status, response = await self._request(
|
||||
request_url=request_url,
|
||||
request_method="POST",
|
||||
request_data=data,
|
||||
)
|
||||
|
||||
if "id" in response:
|
||||
channel_id = response['channel_id']
|
||||
message_id = response['id']
|
||||
|
||||
get_discord_forevercdn = await (
|
||||
DiscordDJ(
|
||||
arSession=self.arSession,
|
||||
bot=self.bot,
|
||||
)
|
||||
).export_forevercdn()
|
||||
|
||||
photo_url = f"{get_discord_forevercdn}/attachments/{channel_id}/{message_id}"
|
||||
|
||||
return True, photo_url
|
||||
|
||||
return False, "None"
|
||||
|
||||
# Запрос
|
||||
async def _request(
|
||||
self,
|
||||
request_url: str,
|
||||
request_method: str,
|
||||
request_data: Optional[Union[Dict[str, Any], FormData]] = None,
|
||||
) -> Tuple[bool, Any]:
|
||||
session = await self.arSession.get_session()
|
||||
|
||||
await asyncio.sleep(1)
|
||||
|
||||
try:
|
||||
response = await session.request(
|
||||
method=request_method,
|
||||
url=request_url,
|
||||
data=request_data,
|
||||
headers={"Accept-Encoding": "gzip, deflate"},
|
||||
)
|
||||
|
||||
response_data = json.loads((await response.read()).decode())
|
||||
|
||||
if response.status == 200:
|
||||
return True, response_data
|
||||
else:
|
||||
await self.error_account_admin(f"{response.status} - {str(response_data)}")
|
||||
|
||||
return False, response_data
|
||||
except ClientConnectorCertificateError:
|
||||
bot_logger.warning("Ошибка SSL при запросе Discord webhook", exc_info=True)
|
||||
await self.error_account_admin("CERTIFICATE_VERIFY_FAILED")
|
||||
|
||||
return False, "CERTIFICATE_VERIFY_FAILED"
|
||||
except Exception as ex:
|
||||
bot_logger.warning("Ошибка запроса Discord webhook", exc_info=True)
|
||||
await self.error_account_admin(str(ex))
|
||||
|
||||
return False, str(ex)
|
||||
|
||||
|
||||
# Извлечение ссылок для работы с локальным АПИ дискорда
|
||||
class DiscordDJ:
|
||||
# Настройка клиента сервисных ссылок Discord
|
||||
def __init__(self, arSession: ARS, bot: Bot):
|
||||
self.arSession = arSession
|
||||
self.bot = bot
|
||||
|
||||
self.const_url = "https://djimbo.dev/autoshop_discord.json"
|
||||
|
||||
# Экспорт публичного дискорд вебхука
|
||||
async def export_webhook(self) -> str:
|
||||
session = await self.arSession.get_session()
|
||||
|
||||
try:
|
||||
response = await session.get(
|
||||
self.const_url,
|
||||
headers={"Accept-Encoding": "gzip, deflate"},
|
||||
)
|
||||
|
||||
response_data = json.loads((await response.read()).decode())
|
||||
except Exception as ex:
|
||||
await send_errors(self.bot, 7729051, f"Error getting Discord Webhook - {ex}")
|
||||
return "None"
|
||||
else:
|
||||
return response_data['webhook']
|
||||
|
||||
# Экспорт ссылки на постоянный CDN
|
||||
async def export_forevercdn(self) -> str:
|
||||
session = await self.arSession.get_session()
|
||||
|
||||
try:
|
||||
response = await session.get(
|
||||
self.const_url,
|
||||
headers={"Accept-Encoding": "gzip, deflate"},
|
||||
)
|
||||
|
||||
response_data = json.loads((await response.read()).decode())
|
||||
except Exception as ex:
|
||||
await send_errors(self.bot, 7729051, f"Error getting Discord ForeverCDN - {ex}")
|
||||
return "None"
|
||||
else:
|
||||
return response_data['forevercdn']
|
||||
@@ -0,0 +1,157 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
import asyncio
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
|
||||
from aiogram import Bot
|
||||
|
||||
from tgbot.database import Settingsx
|
||||
from tgbot.services.api_telegraph import TelegraphAPI
|
||||
from tgbot.utils.const_functions import send_errors
|
||||
from tgbot.utils.misc.bot_logging import bot_logger
|
||||
from tgbot.utils.misc.bot_models import ARS
|
||||
|
||||
|
||||
# Model Hosting Text
|
||||
@dataclass
|
||||
class ModelHostingText:
|
||||
telegraph: str = "telegraph"
|
||||
pastie: str = "pastie"
|
||||
friendpaste: str = "friendpaste"
|
||||
snippet: str = "snippet"
|
||||
|
||||
|
||||
# API для работы с текстовыми хостингами
|
||||
class HostingAPI:
|
||||
# Настройка клиента текстового хостинга
|
||||
def __init__(
|
||||
self,
|
||||
arSession: ARS,
|
||||
bot: Bot,
|
||||
text_hosting: str,
|
||||
):
|
||||
self.arSession = arSession
|
||||
self.bot = bot
|
||||
self.text_hosting = text_hosting
|
||||
|
||||
# Инициализация данных
|
||||
@classmethod
|
||||
async def connect(cls, arSession: ARS, bot: Bot):
|
||||
settings = await Settingsx().get()
|
||||
|
||||
return cls(
|
||||
arSession=arSession,
|
||||
bot=bot,
|
||||
text_hosting=settings.misc_hosting_text, # telegraph, pastie, friendpaste, snippet
|
||||
)
|
||||
|
||||
# Загрузка текста на хостинг
|
||||
async def upload_text(self, text: str) -> str:
|
||||
session = await self.arSession.get_session()
|
||||
|
||||
max_attempts = 10
|
||||
|
||||
for attempt in range(max_attempts):
|
||||
status_success = True
|
||||
return_link = "None"
|
||||
|
||||
############################################################
|
||||
######################## TELEGRAPH #########################
|
||||
if self.text_hosting == ModelHostingText.telegraph:
|
||||
telegraph_api = await TelegraphAPI.connect(arSession=self.arSession)
|
||||
status, response = await telegraph_api.upload_text(text)
|
||||
|
||||
if status:
|
||||
return_link = response
|
||||
else:
|
||||
await send_errors(self.bot, 4044321, str(response))
|
||||
status_success = False
|
||||
|
||||
############################################################
|
||||
########################## PASTIE ##########################
|
||||
elif self.text_hosting == ModelHostingText.pastie:
|
||||
try:
|
||||
response = await session.request(
|
||||
method="post",
|
||||
url="http://pastie.org/pastes/create/",
|
||||
data={
|
||||
'language': 'plaintext',
|
||||
'content': text,
|
||||
},
|
||||
)
|
||||
except Exception as ex:
|
||||
bot_logger.warning("Ошибка загрузки текста в Pastie", exc_info=True)
|
||||
await send_errors(self.bot, 4719012, str(ex))
|
||||
status_success = False
|
||||
else:
|
||||
cache_link = response.url
|
||||
|
||||
if "create" in str(cache_link) or str(cache_link) == "http://pastie.org/":
|
||||
status_success = False
|
||||
else:
|
||||
return_link = cache_link
|
||||
|
||||
############################################################
|
||||
######################## FRIENDPASTE #######################
|
||||
elif self.text_hosting == ModelHostingText.friendpaste:
|
||||
try:
|
||||
response = await session.request(
|
||||
method="post",
|
||||
url="https://www.friendpaste.com/",
|
||||
json={
|
||||
'language': 'text',
|
||||
'title': '',
|
||||
'snippet': text,
|
||||
},
|
||||
)
|
||||
cache_link = json.loads((await response.read()).decode())['url']
|
||||
except Exception as ex:
|
||||
bot_logger.warning("Ошибка загрузки текста в FriendPaste", exc_info=True)
|
||||
await send_errors(self.bot, 8434711, str(ex))
|
||||
status_success = False
|
||||
else:
|
||||
return_link = cache_link
|
||||
|
||||
############################################################
|
||||
######################### SNIPPET ##########################
|
||||
elif self.text_hosting == ModelHostingText.snippet:
|
||||
try:
|
||||
response = await session.request(
|
||||
method="post",
|
||||
url="https://snippet.host/",
|
||||
data={
|
||||
'title': '',
|
||||
'content': text,
|
||||
'visibility': '1',
|
||||
'expires': 'never',
|
||||
'language': 'plain text'
|
||||
},
|
||||
)
|
||||
cache_link = response.url
|
||||
except Exception as ex:
|
||||
bot_logger.warning("Ошибка загрузки текста в Snippet", exc_info=True)
|
||||
await send_errors(self.bot, 8900012, str(ex))
|
||||
status_success = False
|
||||
else:
|
||||
return_link = cache_link
|
||||
|
||||
if status_success:
|
||||
return str(return_link)
|
||||
|
||||
# pastie -> friendpaste
|
||||
if self.text_hosting == ModelHostingText.pastie:
|
||||
self.text_hosting = ModelHostingText.friendpaste
|
||||
# friendpaste -> snippet
|
||||
elif self.text_hosting == ModelHostingText.friendpaste:
|
||||
self.text_hosting = ModelHostingText.snippet
|
||||
# snippet -> telegraph
|
||||
elif self.text_hosting == ModelHostingText.snippet:
|
||||
self.text_hosting = ModelHostingText.telegraph
|
||||
# telegraph -> pastie
|
||||
elif self.text_hosting == ModelHostingText.telegraph:
|
||||
self.text_hosting = ModelHostingText.pastie
|
||||
|
||||
if attempt < max_attempts - 1:
|
||||
await asyncio.sleep(3)
|
||||
|
||||
return "None"
|
||||
@@ -0,0 +1,31 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
import ssl
|
||||
from typing import Optional
|
||||
|
||||
import aiohttp
|
||||
import certifi
|
||||
|
||||
|
||||
# Асинхронная сессия для запросов
|
||||
class AsyncRequestSession:
|
||||
# Создание ленивой aiohttp-сессии
|
||||
def __init__(self) -> None:
|
||||
self._session: Optional[aiohttp.ClientSession] = None
|
||||
|
||||
# Получение объекта сессии
|
||||
async def get_session(self) -> aiohttp.ClientSession:
|
||||
if self._session is None:
|
||||
ssl_context = ssl.create_default_context(cafile=certifi.where())
|
||||
timeout = aiohttp.ClientTimeout(total=15, connect=5, sock_read=10)
|
||||
connector = aiohttp.TCPConnector(ssl=ssl_context)
|
||||
|
||||
self._session = aiohttp.ClientSession(timeout=timeout, connector=connector)
|
||||
|
||||
return self._session
|
||||
|
||||
# Закрытие сессии
|
||||
async def close(self) -> None:
|
||||
if self._session is None:
|
||||
return None
|
||||
|
||||
await self._session.close()
|
||||
@@ -0,0 +1,297 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
import math
|
||||
from typing import Optional, Tuple, Union
|
||||
|
||||
from aiogram import Bot
|
||||
from aiogram.types import CallbackQuery, LabeledPrice, Message, PreCheckoutQuery, SuccessfulPayment
|
||||
|
||||
from tgbot.database import Paymentsx
|
||||
from tgbot.utils.const_functions import ded, gen_id, to_number
|
||||
from tgbot.utils.misc.bot_logging import bot_logger
|
||||
from tgbot.utils.misc.bot_models import ARS
|
||||
from tgbot.utils.misc_functions import send_admins
|
||||
|
||||
STARS_CURRENCY = "XTR"
|
||||
STARS_PAYLOAD_PREFIX = "stars_refill"
|
||||
|
||||
|
||||
# АПИ для работы с Telegram Stars
|
||||
class StarsAPI:
|
||||
# Настройка клиента Telegram Stars
|
||||
def __init__(
|
||||
self,
|
||||
bot: Bot,
|
||||
arSession: ARS,
|
||||
update: Optional[Union[Message, CallbackQuery, PreCheckoutQuery]] = None,
|
||||
stars_course: float = 1.5,
|
||||
skipping_error: bool = False,
|
||||
):
|
||||
self.bot = bot
|
||||
self.arSession = arSession
|
||||
self.update = update
|
||||
self.stars_course = float(stars_course or 1.5)
|
||||
self.skipping_error = skipping_error
|
||||
|
||||
# Инициализация данных
|
||||
@classmethod
|
||||
async def connect(
|
||||
cls,
|
||||
bot: Bot,
|
||||
arSession: ARS,
|
||||
update: Optional[Union[Message, CallbackQuery, PreCheckoutQuery]] = None,
|
||||
skipping_error: bool = False,
|
||||
) -> "StarsAPI":
|
||||
payments = await Paymentsx().get()
|
||||
|
||||
return cls(
|
||||
bot=bot,
|
||||
arSession=arSession,
|
||||
update=update,
|
||||
stars_course=payments.stars_course,
|
||||
skipping_error=skipping_error,
|
||||
)
|
||||
|
||||
# Уведомление админов о проблеме со Stars
|
||||
async def error_notification(self, error_code: str = "Unknown") -> None:
|
||||
bot_logger.warning("Telegram Stars недоступны: %s", error_code)
|
||||
|
||||
if self.skipping_error:
|
||||
return
|
||||
|
||||
await send_admins(
|
||||
self.bot,
|
||||
f"<b>⭐️ Telegram Stars недоступны</b>\n"
|
||||
f"❗️ Ошибка: <code>{error_code}</code>"
|
||||
)
|
||||
|
||||
# Проверка доступности Stars для бота
|
||||
async def check(self) -> Tuple[bool, str]:
|
||||
try:
|
||||
bot_info = await self.bot.get_me()
|
||||
balance = await self.bot.get_my_star_balance()
|
||||
except Exception as ex:
|
||||
bot_logger.warning("Не удалось проверить Telegram Stars", exc_info=True)
|
||||
await self.error_notification(str(ex))
|
||||
|
||||
return False, "<b>⭐️ Не удалось проверить Telegram Stars ❌</b>"
|
||||
|
||||
rub_balance = self.stars_to_rub(balance.amount)
|
||||
|
||||
return True, ded(f"""
|
||||
<b>⭐️ Telegram Stars полностью функционируют ✅</b>
|
||||
➖➖➖➖➖➖➖➖➖➖
|
||||
▪️ Бот: <code>@{bot_info.username}</code>
|
||||
▪️ Баланс: <code>{balance.amount} ⭐️</code> ≈ <code>{rub_balance}₽</code>
|
||||
▪️ Курс: <code>1 ⭐️ = {self.stars_course}₽</code>
|
||||
""")
|
||||
|
||||
# Получение баланса Stars у бота
|
||||
async def balance(self) -> str:
|
||||
try:
|
||||
balance = await self.bot.get_my_star_balance()
|
||||
except Exception as ex:
|
||||
bot_logger.warning("Не удалось получить баланс Telegram Stars", exc_info=True)
|
||||
await self.error_notification(str(ex))
|
||||
|
||||
return "<b>⭐️ Не удалось получить баланс Telegram Stars ❌</b>"
|
||||
|
||||
rub_balance = self.stars_to_rub(balance.amount)
|
||||
nanostars = balance.nanostar_amount or 0
|
||||
|
||||
return ded(f"""
|
||||
<b>⭐️ Баланс Telegram Stars</b>
|
||||
➖➖➖➖➖➖➖➖➖➖
|
||||
▪️ Звёзды: <code>{balance.amount} ⭐️</code>
|
||||
▪️ Нанозвёзды: <code>{nanostars}</code>
|
||||
▪️ В рублях по курсу: <code>{rub_balance}₽</code>
|
||||
▪️ Курс: <code>1 ⭐️ = {self.stars_course}₽</code>
|
||||
""")
|
||||
|
||||
# Создание ссылки на счёт Telegram Stars
|
||||
async def bill(self, pay_amount: Union[float, int]) -> Tuple[Union[str, bool], str, int]:
|
||||
rub_amount = round(float(pay_amount), 2)
|
||||
stars_amount = self.rub_to_stars(rub_amount)
|
||||
bill_chat_id, bill_message_id = self.get_bill_message()
|
||||
receipt = gen_id(12)
|
||||
|
||||
payload = self.make_payload(
|
||||
receipt,
|
||||
rub_amount,
|
||||
stars_amount,
|
||||
bill_chat_id,
|
||||
bill_message_id,
|
||||
)
|
||||
|
||||
try:
|
||||
bill_link = await self.bot.create_invoice_link(
|
||||
title="Пополнение баланса",
|
||||
description=f"Пополнение баланса на {rub_amount}₽ через Telegram Stars",
|
||||
payload=payload,
|
||||
provider_token="",
|
||||
currency=STARS_CURRENCY,
|
||||
prices=[
|
||||
LabeledPrice(
|
||||
label=f"Пополнение на {rub_amount}₽",
|
||||
amount=stars_amount,
|
||||
)
|
||||
],
|
||||
)
|
||||
except Exception as ex:
|
||||
bot_logger.warning("Не удалось создать счёт Telegram Stars", exc_info=True)
|
||||
await self.error_notification(str(ex))
|
||||
|
||||
return False, "", 0
|
||||
|
||||
bill_message = ded(f"""
|
||||
<b>💰 Пополнение баланса через Telegram Stars</b>
|
||||
➖➖➖➖➖➖➖➖➖➖
|
||||
▪️ Сумма пополнения: <code>{rub_amount}₽</code>
|
||||
▪️ К оплате: <code>{stars_amount} ⭐️</code>
|
||||
▪️ Курс: <code>1 ⭐️ = {self.stars_course}₽</code>
|
||||
➖➖➖➖➖➖➖➖➖➖
|
||||
❗️ После оплаты средства автоматически зачислятся на ваш баланс
|
||||
Если сообщение не обновилось, нажмите <code>Проверить оплату</code>
|
||||
""")
|
||||
|
||||
return bill_message, bill_link, receipt
|
||||
|
||||
# Проверка оплаты по истории Stars
|
||||
async def bill_check(self, bill_receipt: Optional[Union[str, int]] = None, records: int = 100) -> Tuple[int, float]:
|
||||
if bill_receipt is None:
|
||||
return 1, 0
|
||||
|
||||
try:
|
||||
transactions = await self.bot.get_star_transactions(limit=records)
|
||||
except Exception as ex:
|
||||
bot_logger.warning("Не удалось проверить оплату Telegram Stars", exc_info=True)
|
||||
await self.error_notification(str(ex))
|
||||
|
||||
return 1, 0
|
||||
|
||||
receipt = str(bill_receipt)
|
||||
|
||||
for transaction in transactions.transactions:
|
||||
source = transaction.source
|
||||
payload = getattr(source, "invoice_payload", None)
|
||||
|
||||
if not payload:
|
||||
continue
|
||||
|
||||
payload_data = self.parse_payload(payload)
|
||||
|
||||
if payload_data is None or str(payload_data[0]) != receipt:
|
||||
continue
|
||||
|
||||
_receipt, rub_amount, stars_amount, _bill_chat_id, _bill_message_id = payload_data
|
||||
|
||||
if transaction.amount != stars_amount:
|
||||
bot_logger.warning(
|
||||
"Сумма Telegram Stars не совпала с payload: receipt=%s expected=%s actual=%s",
|
||||
receipt,
|
||||
stars_amount,
|
||||
transaction.amount,
|
||||
)
|
||||
return 1, 0
|
||||
|
||||
return 0, rub_amount
|
||||
|
||||
return 2, 0
|
||||
|
||||
# Подтверждение автоматической оплаты
|
||||
async def answer_pre_checkout(self, query: PreCheckoutQuery) -> None:
|
||||
payload_data = self.parse_payload(query.invoice_payload)
|
||||
|
||||
if query.currency != STARS_CURRENCY or payload_data is None or query.total_amount != payload_data[2]:
|
||||
await self.bot.answer_pre_checkout_query(
|
||||
pre_checkout_query_id=query.id,
|
||||
ok=False,
|
||||
error_message="Некорректный счёт Telegram Stars",
|
||||
)
|
||||
return
|
||||
|
||||
await self.bot.answer_pre_checkout_query(
|
||||
pre_checkout_query_id=query.id,
|
||||
ok=True,
|
||||
)
|
||||
|
||||
# Получение данных успешной оплаты
|
||||
def parse_successful_payment(
|
||||
self,
|
||||
payment: SuccessfulPayment,
|
||||
) -> Tuple[int, float, str, Optional[int], Optional[int]]:
|
||||
payload_data = self.parse_payload(payment.invoice_payload)
|
||||
|
||||
if payment.currency != STARS_CURRENCY or payload_data is None or payment.total_amount != payload_data[2]:
|
||||
raise ValueError("Некорректная успешная оплата Telegram Stars")
|
||||
|
||||
receipt, rub_amount, _stars_amount, bill_chat_id, bill_message_id = payload_data
|
||||
|
||||
return receipt, rub_amount, payment.telegram_payment_charge_id, bill_chat_id, bill_message_id
|
||||
|
||||
# Возврат оплаты пользователю
|
||||
async def refund(self, user_id: int, telegram_payment_charge_id: str) -> bool:
|
||||
try:
|
||||
return await self.bot.refund_star_payment(
|
||||
user_id=user_id,
|
||||
telegram_payment_charge_id=telegram_payment_charge_id,
|
||||
)
|
||||
except Exception as ex:
|
||||
bot_logger.warning("Не удалось вернуть оплату Telegram Stars", exc_info=True)
|
||||
await self.error_notification(str(ex))
|
||||
|
||||
return False
|
||||
|
||||
# Конвертация рублей в звёзды
|
||||
def rub_to_stars(self, rub_amount: Union[float, int]) -> int:
|
||||
if self.stars_course <= 0:
|
||||
raise ValueError("Курс Telegram Stars должен быть больше нуля")
|
||||
|
||||
return max(1, math.ceil(float(rub_amount) / self.stars_course))
|
||||
|
||||
# Конвертация звёзд в рубли
|
||||
def stars_to_rub(self, stars_amount: Union[float, int]) -> float:
|
||||
return round(float(stars_amount) * self.stars_course, 2)
|
||||
|
||||
# Получение данных сообщения со счётом
|
||||
def get_bill_message(self) -> Tuple[Optional[int], Optional[int]]:
|
||||
if isinstance(self.update, Message):
|
||||
return self.update.chat.id, self.update.message_id
|
||||
|
||||
return None, None
|
||||
|
||||
# Сбор payload для телеграм инвойса
|
||||
@staticmethod
|
||||
def make_payload(
|
||||
receipt: int,
|
||||
rub_amount: float,
|
||||
stars_amount: int,
|
||||
bill_chat_id: Optional[int] = None,
|
||||
bill_message_id: Optional[int] = None,
|
||||
) -> str:
|
||||
payload = f"{STARS_PAYLOAD_PREFIX}:{receipt}:{rub_amount}:{stars_amount}"
|
||||
|
||||
if bill_chat_id is not None and bill_message_id is not None:
|
||||
payload += f":{bill_chat_id}:{bill_message_id}"
|
||||
|
||||
return payload
|
||||
|
||||
# Разбор payload телеграм инвойса
|
||||
@staticmethod
|
||||
def parse_payload(
|
||||
payload: str,
|
||||
) -> Optional[Tuple[int, float, int, Optional[int], Optional[int]]]:
|
||||
payload_parts = str(payload).split(":")
|
||||
|
||||
if len(payload_parts) not in (4, 6) or payload_parts[0] != STARS_PAYLOAD_PREFIX:
|
||||
return None
|
||||
|
||||
try:
|
||||
receipt = int(payload_parts[1])
|
||||
rub_amount = float(to_number(payload_parts[2]))
|
||||
stars_amount = int(payload_parts[3])
|
||||
bill_chat_id = int(payload_parts[4]) if len(payload_parts) == 6 else None
|
||||
bill_message_id = int(payload_parts[5]) if len(payload_parts) == 6 else None
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
return receipt, rub_amount, stars_amount, bill_chat_id, bill_message_id
|
||||
@@ -0,0 +1,87 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
import json
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from tgbot.database import Settingsx
|
||||
from tgbot.utils.const_functions import gen_id
|
||||
from tgbot.utils.misc.bot_logging import bot_logger
|
||||
from tgbot.utils.misc.bot_models import ARS
|
||||
|
||||
|
||||
# API для работы с телеграфом
|
||||
class TelegraphAPI:
|
||||
# Настройка клиента Telegraph
|
||||
def __init__(self, arSession: ARS, token: Optional[str] = None):
|
||||
self.arSession = arSession
|
||||
self.token = token
|
||||
|
||||
self.domain = "https://api.telegra.ph"
|
||||
|
||||
# Инициализация данных
|
||||
@classmethod
|
||||
async def connect(cls, arSession: ARS, token: Optional[str] = None):
|
||||
if token is None:
|
||||
settings = await Settingsx().get()
|
||||
token = settings.misc_token_telegraph
|
||||
|
||||
return cls(arSession=arSession, token=token)
|
||||
|
||||
# Загрузка текста в телеграф
|
||||
async def upload_text(self, text: str) -> Tuple[bool, str]:
|
||||
session = await self.arSession.get_session()
|
||||
|
||||
if self.token == "None" or self.token is None:
|
||||
status, response = await self.generate_token()
|
||||
|
||||
if status:
|
||||
self.token = response
|
||||
else:
|
||||
return False, "Error generate token"
|
||||
|
||||
try:
|
||||
response = await session.request(
|
||||
method="POST",
|
||||
url=f"{self.domain}/createPage",
|
||||
data={
|
||||
'access_token': self.token,
|
||||
'title': f'shop{gen_id(18)}',
|
||||
'content': json.dumps([{'tag': 'p', 'children': [text]}]),
|
||||
}
|
||||
)
|
||||
|
||||
response_data = json.loads((await response.read()).decode())
|
||||
except Exception:
|
||||
bot_logger.warning("Не удалось загрузить текст в Telegraph", exc_info=True)
|
||||
return False, "None"
|
||||
else:
|
||||
if "ok" in response_data and response_data['ok']:
|
||||
return True, response_data['result']['url'].replace("\\\\", "")
|
||||
else:
|
||||
return False, response_data
|
||||
|
||||
# Генерация нового Telegraph-токена
|
||||
async def generate_token(self) -> Tuple[bool, str]:
|
||||
session = await self.arSession.get_session()
|
||||
|
||||
try:
|
||||
response = await session.request(
|
||||
method="POST",
|
||||
url=f"{self.domain}/createAccount",
|
||||
data={
|
||||
'short_name': 'dj shop',
|
||||
}
|
||||
)
|
||||
|
||||
response_data = json.loads((await response.read()).decode())
|
||||
except Exception:
|
||||
bot_logger.warning("Не удалось создать Telegraph token", exc_info=True)
|
||||
return False, "None"
|
||||
else:
|
||||
if "ok" in response_data and response_data['ok']:
|
||||
await Settingsx().update(
|
||||
misc_token_telegraph=response_data['result']['access_token']
|
||||
)
|
||||
|
||||
return True, response_data['result']['access_token']
|
||||
else:
|
||||
return False, response_data
|
||||
@@ -0,0 +1,339 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
import json
|
||||
from typing import Any, Dict, Optional, Tuple, Union
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from aiogram import Bot
|
||||
from aiogram.types import CallbackQuery, Message
|
||||
from aiohttp import ClientConnectorCertificateError
|
||||
|
||||
from tgbot.data.config import YOOMONEY_CLIENT_ID
|
||||
from tgbot.database import Paymentsx
|
||||
from tgbot.utils.const_functions import ded, gen_id
|
||||
from tgbot.utils.misc.bot_logging import bot_logger
|
||||
from tgbot.utils.misc.bot_models import ARS
|
||||
from tgbot.utils.misc_functions import send_admins
|
||||
|
||||
|
||||
# АПИ для работы с ЮMoney
|
||||
class YoomoneyAPI:
|
||||
# Настройка клиента ЮMoney
|
||||
def __init__(
|
||||
self,
|
||||
bot: Bot,
|
||||
arSession: ARS,
|
||||
update: Optional[Union[Message, CallbackQuery]] = None,
|
||||
token: str = "None",
|
||||
adding: bool = False,
|
||||
skipping_error: bool = False,
|
||||
):
|
||||
self.token = token
|
||||
self.adding = adding
|
||||
|
||||
self.base_url = 'https://yoomoney.ru/api/'
|
||||
self.headers = {
|
||||
'Authorization': f'Bearer {self.token}',
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
}
|
||||
|
||||
self.bot = bot
|
||||
self.arSession = arSession
|
||||
self.update = update
|
||||
self.skipping_error = skipping_error
|
||||
|
||||
# Инициализация данных
|
||||
@classmethod
|
||||
async def connect(
|
||||
cls,
|
||||
bot: Bot,
|
||||
arSession: ARS,
|
||||
update: Optional[Union[Message, CallbackQuery]] = None,
|
||||
token: Optional[str] = None,
|
||||
skipping_error: bool = False,
|
||||
) -> "YoomoneyAPI":
|
||||
adding = token is not None
|
||||
|
||||
if token is None:
|
||||
payments = await Paymentsx().get()
|
||||
token = payments.yoomoney_token
|
||||
adding = False
|
||||
|
||||
return cls(
|
||||
bot=bot,
|
||||
arSession=arSession,
|
||||
update=update,
|
||||
token=token,
|
||||
adding=adding,
|
||||
skipping_error=skipping_error,
|
||||
)
|
||||
|
||||
# Уведомления о неработоспособности кассы/кошелька
|
||||
async def error_notification(self, error_code: str = "Unknown"):
|
||||
bot_logger.warning("ЮMoney недоступен: %s", error_code)
|
||||
|
||||
if not self.skipping_error:
|
||||
if self.adding and self.update is not None:
|
||||
await self.update.edit_text(
|
||||
f"<b>🔮 Не удалось добавить ЮMoney кассу ❌</b>\n"
|
||||
f"❗️ Ошибка: <code>{error_code}</code>"
|
||||
)
|
||||
else:
|
||||
await send_admins(
|
||||
self.bot,
|
||||
f"<b>🔮 ЮMoney недоступен. Как можно быстрее его замените</b>\n"
|
||||
f"❗️ Ошибка: <code>{error_code}</code>"
|
||||
)
|
||||
|
||||
# Проверка кассы/кошелька
|
||||
async def check(self) -> str:
|
||||
status, response = await self._request("account-info")
|
||||
|
||||
if status:
|
||||
if len(response) >= 1:
|
||||
if response['identified']:
|
||||
text_identified = "Присутствует"
|
||||
else:
|
||||
text_identified = "Отсутствует"
|
||||
|
||||
if response['account_status'] == "identified":
|
||||
text_status = "Идентифицированный счет"
|
||||
elif response['account_status'] == "anonymous":
|
||||
text_status = "Анонимный счет"
|
||||
elif response['account_status'] == "named":
|
||||
text_status = "Именной счет"
|
||||
else:
|
||||
text_status = response['account_status']
|
||||
|
||||
if response['account_type'] == "personal":
|
||||
text_type = "Пользовательский счет"
|
||||
elif response['account_type'] == "professional":
|
||||
text_type = "Профессиональный счет"
|
||||
else:
|
||||
text_type = response['account_type']
|
||||
|
||||
return ded(f"""
|
||||
<b>🔮 ЮMoney кошелёк полностью функционирует ✅</b>
|
||||
➖➖➖➖➖➖➖➖➖➖
|
||||
▪️ Кошелёк: <code>{response['account']}</code>
|
||||
▪️ Идентификация: <code>{text_identified}</code>
|
||||
▪️ Статус аккаунта: <code>{text_status}</code>
|
||||
▪️ Тип счета: <code>{text_type}</code>
|
||||
""")
|
||||
|
||||
return "<b>🔮 Не удалось проверить ЮMoney кошелёк ❌</b>"
|
||||
|
||||
# Получение баланса
|
||||
async def balance(self) -> str:
|
||||
status, response = await self._request("account-info")
|
||||
|
||||
if status:
|
||||
wallet_balance = response['balance']
|
||||
|
||||
wallet_status, wallet_number = await self.account_info()
|
||||
|
||||
if wallet_status:
|
||||
return ded(f"""
|
||||
<b>🔮 Баланс ЮMoney кошелька составляет</b>
|
||||
➖➖➖➖➖➖➖➖➖➖
|
||||
▪️ Кошелёк: <code>{wallet_number}</code>
|
||||
▪️ Баланс: <code>{wallet_balance}₽</code>
|
||||
""")
|
||||
|
||||
return "<b>🔮 Не удалось получить баланс ЮMoney кошелька ❌</b>"
|
||||
|
||||
# Информация об аккаунте
|
||||
async def account_info(self) -> Tuple[bool, str]:
|
||||
status, response = await self._request("account-info")
|
||||
|
||||
try:
|
||||
return True, response['account']
|
||||
except Exception:
|
||||
bot_logger.warning("Не удалось получить номер ЮMoney", exc_info=True)
|
||||
return False, ""
|
||||
|
||||
# Получение ссылки на авторизацию
|
||||
async def authorization_get(self) -> str:
|
||||
if not YOOMONEY_CLIENT_ID:
|
||||
return "В .env не заполнен YOOMONEY_CLIENT_ID"
|
||||
|
||||
session = await self.arSession.get_session()
|
||||
|
||||
headers = {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
|
||||
url = "https://yoomoney.ru/oauth/authorize?" + urlencode({
|
||||
"client_id": YOOMONEY_CLIENT_ID,
|
||||
"response_type": "code",
|
||||
"redirect_uri": "https://yoomoney.ru",
|
||||
"scope": "account-info operation-history operation-details",
|
||||
})
|
||||
|
||||
response = await session.post(
|
||||
url=url,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
return str(response.url)
|
||||
|
||||
# Принятие кода авторизации и получение токена
|
||||
async def authorization_enter(self, get_code: str) -> Tuple[bool, str, str]:
|
||||
if not YOOMONEY_CLIENT_ID:
|
||||
return False, "", "<b>❌ В .env не заполнен YOOMONEY_CLIENT_ID</b>"
|
||||
|
||||
session = await self.arSession.get_session()
|
||||
|
||||
headers = {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
|
||||
url = "https://yoomoney.ru/oauth/token?" + urlencode({
|
||||
"code": get_code,
|
||||
"client_id": YOOMONEY_CLIENT_ID,
|
||||
"grant_type": "authorization_code",
|
||||
"redirect_uri": "https://yoomoney.ru",
|
||||
})
|
||||
|
||||
response = await session.post(
|
||||
url=url,
|
||||
headers=headers,
|
||||
)
|
||||
response_data = json.loads((await response.read()).decode())
|
||||
|
||||
if "error" in response_data:
|
||||
error = response_data['error']
|
||||
|
||||
if error == "invalid_request":
|
||||
return_message = ded(f"""
|
||||
<b>❌ Требуемые параметры запроса отсутствуют или имеют неправильные или недопустимые значения</b>
|
||||
""")
|
||||
elif error == "unauthorized_client":
|
||||
return_message = ded(f"""
|
||||
<b>❌ Недопустимое значение параметра 'client_id' или 'client_secret', или приложение
|
||||
не имеет права запрашивать авторизацию (например, ЮMoney заблокировал его 'client_id')</b>
|
||||
""")
|
||||
elif error == "invalid_grant":
|
||||
return_message = ded(f"""
|
||||
<b>❌ В выпуске 'access_token' отказано. ЮMoney не выпускал временный токен,
|
||||
срок действия токена истек или этот временный токен уже выдан
|
||||
'access_token' (повторный запрос токена авторизации с тем же временным токеном)</b>
|
||||
""")
|
||||
else:
|
||||
return_message = f"Unknown error: {error}"
|
||||
|
||||
return False, "", return_message
|
||||
elif response_data['access_token'] == "":
|
||||
return False, "", "<b>❌ Не удалось получить токен. Попробуйте всё снова</b>"
|
||||
|
||||
return True, response_data['access_token'], "<b>🔮 ЮMoney кошелёк был успешно изменён ✅</b>"
|
||||
|
||||
# Создание счета на оплату
|
||||
async def bill(self, pay_amount: Union[float, int]) -> Tuple[Union[str, bool], str, str]:
|
||||
session = await self.arSession.get_session()
|
||||
|
||||
bill_receipt = str(gen_id(12))
|
||||
url = "https://yoomoney.ru/quickpay/confirm.xml?"
|
||||
|
||||
wallet_status, wallet_number = await self.account_info()
|
||||
|
||||
if wallet_status:
|
||||
pay_amount_bill = pay_amount + (pay_amount * 0.031)
|
||||
|
||||
if float(pay_amount_bill) < 2:
|
||||
pay_amount_bill = 2.04
|
||||
|
||||
payload = {
|
||||
'receiver': wallet_number,
|
||||
'quickpay_form': 'button',
|
||||
'targets': 'Добровольное пожертвование',
|
||||
'paymentType': 'SB',
|
||||
'sum': pay_amount_bill,
|
||||
'label': bill_receipt,
|
||||
}
|
||||
|
||||
for value in payload:
|
||||
url += str(value).replace("_", "-") + "=" + str(payload[value])
|
||||
url += "&"
|
||||
|
||||
bill_link = str((await session.post(url[:-1].replace(" ", "%20"))).url)
|
||||
|
||||
bill_message = ded(f"""
|
||||
<b>💰 Пополнение баланса</b>
|
||||
➖➖➖➖➖➖➖➖➖➖
|
||||
▪️ Для пополнения баланса, нажмите на кнопку ниже
|
||||
<code>Перейти к оплате</code> и оплатите выставленный вам счёт
|
||||
▪️ У вас имеется 60 минут на оплату счета
|
||||
▪️ Сумма пополнения: <code>{pay_amount}₽</code>
|
||||
➖➖➖➖➖➖➖➖➖➖
|
||||
❗️ После оплаты, нажмите на <code>Проверить оплату</code>
|
||||
""")
|
||||
|
||||
return bill_message, bill_link, bill_receipt
|
||||
|
||||
return False, "", ""
|
||||
|
||||
# Проверка счета на оплату
|
||||
async def bill_check(self, bill_receipt: Optional[Union[str, int]] = None, records: int = 1) -> Tuple[int, float]:
|
||||
data = {
|
||||
'type': 'deposition',
|
||||
'details': 'true',
|
||||
}
|
||||
|
||||
if bill_receipt is not None:
|
||||
data['label'] = bill_receipt
|
||||
if records is not None:
|
||||
data['records'] = str(records)
|
||||
|
||||
status, response = await self._request("operation-history", data)
|
||||
|
||||
pay_status, pay_amount, pay_currency = 1, 0, 0
|
||||
|
||||
if status:
|
||||
pay_status = 2
|
||||
|
||||
if len(response['operations']) >= 1:
|
||||
pay_currency = response['operations'][0]['amount_currency']
|
||||
pay_amount = response['operations'][0]['amount']
|
||||
|
||||
pay_status = 3
|
||||
|
||||
if pay_currency == "RUB":
|
||||
pay_status = 0
|
||||
|
||||
return pay_status, pay_amount
|
||||
|
||||
# Генерация запроса
|
||||
async def _request(
|
||||
self,
|
||||
method: str,
|
||||
data: Optional[Dict[str, Any]] = None,
|
||||
) -> Tuple[bool, Any]:
|
||||
session = await self.arSession.get_session()
|
||||
|
||||
url = self.base_url + method
|
||||
|
||||
try:
|
||||
response = await session.post(
|
||||
url=url,
|
||||
headers=self.headers,
|
||||
data=data,
|
||||
)
|
||||
|
||||
response_data = json.loads((await response.read()).decode())
|
||||
|
||||
if response.status == 200:
|
||||
return True, response_data
|
||||
else:
|
||||
await self.error_notification(f"{response.status} - {str(response_data)}")
|
||||
|
||||
return False, response_data
|
||||
except ClientConnectorCertificateError:
|
||||
bot_logger.warning("Ошибка SSL при запросе ЮMoney", exc_info=True)
|
||||
await self.error_notification("CERTIFICATE_VERIFY_FAILED")
|
||||
|
||||
return False, "CERTIFICATE_VERIFY_FAILED"
|
||||
except Exception as ex:
|
||||
bot_logger.warning("Ошибка запроса ЮMoney", exc_info=True)
|
||||
await self.error_notification(str(ex))
|
||||
|
||||
return False, str(ex)
|
||||
@@ -0,0 +1,388 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
import html
|
||||
import secrets
|
||||
import string
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from aiogram import Bot
|
||||
from aiogram.types import (
|
||||
CopyTextButton,
|
||||
InlineKeyboardButton,
|
||||
InlineKeyboardMarkup,
|
||||
KeyboardButton,
|
||||
Message,
|
||||
ReplyKeyboardMarkup,
|
||||
WebAppInfo,
|
||||
)
|
||||
from pytz import timezone
|
||||
|
||||
from tgbot.data.config import BOT_TIMEZONE, get_admins
|
||||
from tgbot.utils.misc.bot_logging import bot_logger
|
||||
|
||||
|
||||
# Быстрая сборка reply-кнопки
|
||||
def rkb(text: str) -> KeyboardButton:
|
||||
return KeyboardButton(text=text)
|
||||
|
||||
|
||||
# Быстрая сборка inline-кнопки
|
||||
def ikb(
|
||||
text: str,
|
||||
data: Optional[str] = None,
|
||||
url: Optional[str] = None,
|
||||
switch: Optional[str] = None,
|
||||
web: Optional[str] = None,
|
||||
copy: Optional[str] = None,
|
||||
login: Optional[str] = None,
|
||||
) -> InlineKeyboardButton:
|
||||
if data is not None:
|
||||
return InlineKeyboardButton(text=text, callback_data=data)
|
||||
if url is not None:
|
||||
return InlineKeyboardButton(text=text, url=url)
|
||||
if switch is not None:
|
||||
return InlineKeyboardButton(text=text, switch_inline_query=switch)
|
||||
if web is not None:
|
||||
return InlineKeyboardButton(text=text, web_app=WebAppInfo(url=web))
|
||||
if copy is not None:
|
||||
return InlineKeyboardButton(text=text, copy_text=CopyTextButton(text=copy))
|
||||
if login is not None:
|
||||
return InlineKeyboardButton(text=text, url=f"https://t.me/{login}")
|
||||
|
||||
raise ValueError("Не указано действие для inline-кнопки")
|
||||
|
||||
|
||||
# Удаление сообщения с логированием ошибок
|
||||
async def del_message(message: Message) -> None:
|
||||
try:
|
||||
await message.delete()
|
||||
except Exception:
|
||||
bot_logger.debug("Не удалось удалить сообщение", exc_info=True)
|
||||
|
||||
|
||||
# Отправка фото или обычного сообщения
|
||||
async def smart_message(
|
||||
bot: Bot,
|
||||
user_id: int,
|
||||
text: str,
|
||||
keyboard: Optional[Union[InlineKeyboardMarkup, ReplyKeyboardMarkup]] = None,
|
||||
photo: Optional[str] = None,
|
||||
) -> None:
|
||||
if photo is not None and photo.title() != "None":
|
||||
await bot.send_photo(
|
||||
chat_id=user_id,
|
||||
photo=photo,
|
||||
caption=text,
|
||||
reply_markup=keyboard,
|
||||
)
|
||||
else:
|
||||
await bot.send_message(
|
||||
chat_id=user_id,
|
||||
text=text,
|
||||
reply_markup=keyboard,
|
||||
)
|
||||
|
||||
|
||||
# Отправка сообщения всем админам
|
||||
async def send_admins(
|
||||
bot: Bot,
|
||||
text: str,
|
||||
keyboard: Optional[InlineKeyboardMarkup] = None,
|
||||
markup: Optional[InlineKeyboardMarkup] = None,
|
||||
not_me: int = 0,
|
||||
) -> None:
|
||||
reply_markup = markup or keyboard
|
||||
|
||||
for admin in get_admins():
|
||||
try:
|
||||
if str(admin) != str(not_me):
|
||||
await bot.send_message(
|
||||
admin,
|
||||
text,
|
||||
reply_markup=reply_markup,
|
||||
disable_web_page_preview=True,
|
||||
)
|
||||
except Exception:
|
||||
bot_logger.warning("Не удалось отправить сообщение админу %s", admin, exc_info=True)
|
||||
|
||||
|
||||
# Логирование ошибки и отправка админам
|
||||
async def send_errors(bot: Bot, error_code: int, error_text: str = "") -> None:
|
||||
text_error = f"myError {error_code}: {error_text}"
|
||||
|
||||
bot_logger.warning(text_error)
|
||||
await send_admins(bot, text_error)
|
||||
|
||||
|
||||
# Очистка лишних отступов в многострочном тексте
|
||||
def ded(get_text: str) -> str:
|
||||
if get_text is not None:
|
||||
split_text = get_text.split("\n")
|
||||
|
||||
if split_text[0] == "": split_text.pop(0)
|
||||
if split_text[-1] == "": split_text.pop()
|
||||
save_text = []
|
||||
|
||||
for text in split_text:
|
||||
while text.startswith(" "):
|
||||
text = text[1:]
|
||||
|
||||
save_text.append(text)
|
||||
get_text = "\n".join(save_text)
|
||||
else:
|
||||
get_text = ""
|
||||
|
||||
return get_text
|
||||
|
||||
|
||||
# Экранирование HTML-символов для телеграм разметки
|
||||
def clear_html(get_text: Optional[str]) -> str:
|
||||
return html.escape(get_text or "", quote=False)
|
||||
|
||||
|
||||
# Очистка пустых и мусорных элементов из списка
|
||||
def clear_list(get_list: list) -> list:
|
||||
trash = {"", " ", ".", ",", "\r", "\n"}
|
||||
|
||||
return [value for value in get_list if value not in trash]
|
||||
|
||||
|
||||
# Склейка нескольких списков в один
|
||||
def convert_list(get_lists: List[list]) -> list:
|
||||
save_lists = []
|
||||
|
||||
for select_list in get_lists:
|
||||
cache_list = clear_list(select_list)
|
||||
|
||||
if len(cache_list) > 0:
|
||||
save_lists += cache_list
|
||||
|
||||
return save_lists
|
||||
|
||||
|
||||
# Разделение списка на части нужного размера
|
||||
def split_list(get_list: list, count: int) -> List[list]:
|
||||
return [get_list[i:i + count] for i in range(0, len(get_list), count)]
|
||||
|
||||
|
||||
# Старое имя для разделения сообщений
|
||||
def split_messages(get_list: list, count: int) -> List[list]:
|
||||
return split_list(get_list, count)
|
||||
|
||||
|
||||
# Получение текущей даты
|
||||
def get_date(full: bool = True) -> str:
|
||||
bot_timezone = timezone(BOT_TIMEZONE)
|
||||
|
||||
if full:
|
||||
return datetime.now(bot_timezone).strftime("%d.%m.%Y %H:%M:%S")
|
||||
|
||||
return datetime.now(bot_timezone).strftime("%d.%m.%Y")
|
||||
|
||||
|
||||
# Получение Unix-времени в секундах или наносекундах
|
||||
def get_unix(full: bool = False, nano: bool = False) -> int:
|
||||
if full or nano:
|
||||
return time.time_ns()
|
||||
|
||||
return int(time.time())
|
||||
|
||||
|
||||
# Конвертация даты в Unix и обратно
|
||||
def convert_date(from_time, full=True, second=True) -> Union[str, int]:
|
||||
bot_timezone = timezone(BOT_TIMEZONE)
|
||||
from_time = str(from_time).strip().replace("-", ".")
|
||||
|
||||
if from_time.isdigit():
|
||||
from_timestamp = int(from_time)
|
||||
|
||||
if full:
|
||||
return datetime.fromtimestamp(from_timestamp, bot_timezone).strftime("%d.%m.%Y %H:%M:%S")
|
||||
if second:
|
||||
return datetime.fromtimestamp(from_timestamp, bot_timezone).strftime("%d.%m.%Y %H:%M")
|
||||
|
||||
return datetime.fromtimestamp(from_timestamp, bot_timezone).strftime("%d.%m.%Y")
|
||||
|
||||
parts = from_time.split()
|
||||
|
||||
if len(parts) == 2 and ":" in parts[0]:
|
||||
time_part, date_part = parts
|
||||
elif len(parts) == 2:
|
||||
date_part, time_part = parts
|
||||
else:
|
||||
date_part, time_part = from_time, "00:00:00"
|
||||
|
||||
date_values = date_part.split(".")
|
||||
time_values = time_part.split(":")
|
||||
|
||||
if len(time_values) == 2:
|
||||
time_values.append("0")
|
||||
|
||||
if len(date_values[0]) == 4:
|
||||
x_year, x_month, x_day = date_values[0], date_values[1], date_values[2]
|
||||
else:
|
||||
x_day, x_month, x_year = date_values[0], date_values[1], date_values[2]
|
||||
|
||||
date_time = datetime(
|
||||
int(x_year),
|
||||
int(x_month),
|
||||
int(x_day),
|
||||
int(time_values[0]),
|
||||
int(time_values[1]),
|
||||
int(time_values[2]),
|
||||
)
|
||||
date_time = bot_timezone.localize(date_time)
|
||||
|
||||
return int(date_time.timestamp())
|
||||
|
||||
|
||||
# Генерация числового ID
|
||||
def gen_id(len_id: int = 16) -> int:
|
||||
if len_id <= 0:
|
||||
raise ValueError("Длина ID должна быть больше нуля")
|
||||
|
||||
first_digit = secrets.choice("123456789")
|
||||
other_digits = "".join(secrets.choice(string.digits) for _ in range(len_id - 1))
|
||||
|
||||
return int(f"{first_digit}{other_digits}")
|
||||
|
||||
|
||||
# Генерация пароля под разные сценарии
|
||||
def gen_password(len_password: int = 16, type_password: str = "default") -> str:
|
||||
if len_password <= 0:
|
||||
raise ValueError("Длина пароля должна быть больше нуля")
|
||||
|
||||
if type_password == "default":
|
||||
alphabet = string.ascii_letters + string.digits
|
||||
elif type_password == "letter":
|
||||
alphabet = string.ascii_letters
|
||||
elif type_password == "number":
|
||||
alphabet = string.digits
|
||||
elif type_password == "onechar":
|
||||
alphabet = string.digits
|
||||
else:
|
||||
raise ValueError("Неизвестный тип пароля")
|
||||
|
||||
random_chars = "".join(secrets.choice(alphabet) for _ in range(len_password))
|
||||
|
||||
if type_password == "onechar":
|
||||
random_chars = f"{secrets.choice(string.ascii_letters)}{random_chars[1:]}"
|
||||
|
||||
return random_chars
|
||||
|
||||
|
||||
# Склонение единицы времени под число
|
||||
def convert_times(get_time: int, get_type: str = "day") -> str:
|
||||
get_time = int(get_time)
|
||||
|
||||
if get_time < 0:
|
||||
get_time = 0
|
||||
|
||||
if get_type == "second":
|
||||
values = ["секунда", "секунды", "секунд"]
|
||||
elif get_type == "minute":
|
||||
values = ["минута", "минуты", "минут"]
|
||||
elif get_type == "hour":
|
||||
values = ["час", "часа", "часов"]
|
||||
elif get_type == "day":
|
||||
values = ["день", "дня", "дней"]
|
||||
elif get_type == "month":
|
||||
values = ["месяц", "месяца", "месяцев"]
|
||||
else:
|
||||
values = ["год", "года", "лет"]
|
||||
|
||||
if get_time % 10 == 1 and get_time % 100 != 11:
|
||||
count = 0
|
||||
elif 2 <= get_time % 10 <= 4 and (get_time % 100 < 10 or get_time % 100 >= 20):
|
||||
count = 1
|
||||
else:
|
||||
count = 2
|
||||
|
||||
return f"{get_time} {values[count]}"
|
||||
|
||||
|
||||
# Старое имя для склонения дней
|
||||
def convert_day(day: int) -> str:
|
||||
return convert_times(day)
|
||||
|
||||
|
||||
# Приведение строки или числа к bool
|
||||
def is_bool(value: Union[bool, str, int]) -> bool:
|
||||
value = str(value).strip().lower()
|
||||
|
||||
if value in ("y", "yes", "t", "true", "on", "1"):
|
||||
return True
|
||||
if value in ("n", "no", "f", "false", "off", "0"):
|
||||
return False
|
||||
|
||||
raise ValueError(f"Некорректное bool-значение: {value}")
|
||||
|
||||
|
||||
# Форматирование числа без лишних нулей
|
||||
def snum(amount: Union[int, float], remains: int = 2) -> str:
|
||||
format_str = "{:." + str(remains) + "f}"
|
||||
str_amount = format_str.format(float(amount))
|
||||
|
||||
if remains != 0 and "." in str_amount:
|
||||
remains_find = str_amount.find(".")
|
||||
remains_save = remains_find + 8 - (8 - remains) + 1
|
||||
str_amount = str_amount[:remains_save]
|
||||
|
||||
if "." in str(str_amount):
|
||||
while str(str_amount).endswith("0"):
|
||||
str_amount = str(str_amount)[:-1]
|
||||
|
||||
if str(str_amount).endswith("."):
|
||||
str_amount = str(str_amount)[:-1]
|
||||
|
||||
return str(str_amount)
|
||||
|
||||
|
||||
# Приведение значения к int или float
|
||||
def to_float(get_number, remains: int = 2) -> Union[int, float]:
|
||||
value = str(get_number).strip().replace(" ", "").replace(",", ".")
|
||||
number = round(float(value), remains)
|
||||
|
||||
if number.is_integer():
|
||||
return int(number)
|
||||
|
||||
return number
|
||||
|
||||
|
||||
# Старое имя для приведения числа
|
||||
def to_number(get_number, remains: int = 2) -> Union[int, float]:
|
||||
return to_float(get_number, remains)
|
||||
|
||||
|
||||
# Округление числа до int
|
||||
def to_int(get_number: float) -> int:
|
||||
value = str(get_number).replace(",", ".")
|
||||
|
||||
return int(round(float(value)))
|
||||
|
||||
|
||||
# Проверка, является ли значение числом
|
||||
def is_number(get_number: Union[str, int, float]) -> bool:
|
||||
if str(get_number).isdigit():
|
||||
return True
|
||||
|
||||
value = str(get_number).replace(",", ".")
|
||||
|
||||
try:
|
||||
float(value)
|
||||
return True
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
|
||||
|
||||
# Форматирование числа с разделением тысяч
|
||||
def format_rate(amount: Union[float, int], around: int = 2) -> str:
|
||||
value = str(amount).strip().replace(" ", "").replace(",", ".")
|
||||
number = round(float(value), around)
|
||||
response = f"{number:,.{around}f}".replace(",", " ")
|
||||
|
||||
if "." in response:
|
||||
response = response.rstrip("0").rstrip(".")
|
||||
|
||||
return response
|
||||
@@ -0,0 +1,33 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
from aiogram import Bot
|
||||
from aiogram.types import BotCommand, BotCommandScopeChat, BotCommandScopeDefault
|
||||
|
||||
from tgbot.data.config import get_admins
|
||||
from tgbot.utils.misc.bot_logging import bot_logger
|
||||
|
||||
# Команды для юзеров
|
||||
user_commands = [
|
||||
BotCommand(command='start', description='♻️ Перезапустить бота'),
|
||||
BotCommand(command='support', description='☎️ Поддержка'),
|
||||
BotCommand(command='faq', description='❔ FAQ'),
|
||||
]
|
||||
|
||||
# Команды для админов
|
||||
admin_commands = [
|
||||
BotCommand(command='start', description='♻️ Перезапустить бота'),
|
||||
BotCommand(command='support', description='☎️ Поддержка'),
|
||||
BotCommand(command='faq', description='❔ FAQ'),
|
||||
BotCommand(command='db', description='📦 Получить Базу Данных'),
|
||||
BotCommand(command='log', description='🖨 Получить логи'),
|
||||
]
|
||||
|
||||
|
||||
# Установка команд
|
||||
async def set_commands(bot: Bot):
|
||||
await bot.set_my_commands(user_commands, scope=BotCommandScopeDefault())
|
||||
|
||||
for admin in get_admins():
|
||||
try:
|
||||
await bot.set_my_commands(admin_commands, scope=BotCommandScopeChat(chat_id=admin))
|
||||
except Exception:
|
||||
bot_logger.warning("Не удалось установить команды админу %s", admin, exc_info=True)
|
||||
@@ -0,0 +1,67 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
from typing import Union
|
||||
|
||||
from aiogram.filters import BaseFilter
|
||||
from aiogram.types import CallbackQuery, Message
|
||||
|
||||
from tgbot.data.config import get_admins
|
||||
from tgbot.database import Settingsx
|
||||
|
||||
|
||||
class IsAdmin(BaseFilter):
|
||||
# Проверка, что пользователь админ
|
||||
async def __call__(self, event: Union[Message, CallbackQuery]) -> bool:
|
||||
user = getattr(event, "from_user", None)
|
||||
|
||||
return bool(user and user.id in get_admins())
|
||||
|
||||
|
||||
class IsPrivate(BaseFilter):
|
||||
# Проверка приватного чата
|
||||
async def __call__(self, event: Union[Message, CallbackQuery]) -> bool:
|
||||
chat = getattr(event, "chat", None)
|
||||
message = getattr(event, "message", None)
|
||||
|
||||
if chat is None and message is not None:
|
||||
chat = message.chat
|
||||
|
||||
if chat is None:
|
||||
return True
|
||||
|
||||
return chat.type == "private"
|
||||
|
||||
|
||||
class IsWork(BaseFilter):
|
||||
# Проверка режима тех. работ
|
||||
async def __call__(self, event: Union[Message, CallbackQuery]) -> bool:
|
||||
user = getattr(event, "from_user", None)
|
||||
settings = await Settingsx().get()
|
||||
|
||||
if settings.status_work == "False" or (user and user.id in get_admins()):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class IsRefill(BaseFilter):
|
||||
# Проверка доступности пополнений
|
||||
async def __call__(self, event: Union[Message, CallbackQuery]) -> bool:
|
||||
user = getattr(event, "from_user", None)
|
||||
settings = await Settingsx().get()
|
||||
|
||||
if settings.status_refill == "True" or (user and user.id in get_admins()):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class IsBuy(BaseFilter):
|
||||
# Проверка доступности покупок
|
||||
async def __call__(self, event: Union[Message, CallbackQuery]) -> bool:
|
||||
user = getattr(event, "from_user", None)
|
||||
settings = await Settingsx().get()
|
||||
|
||||
if settings.status_buy == "True" or (user and user.id in get_admins()):
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -0,0 +1,52 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
from pathlib import Path
|
||||
|
||||
import colorlog
|
||||
|
||||
from tgbot.data.config import PATH_LOGS
|
||||
|
||||
LOG_FILE_MAX_BYTES = 5 * 1024 * 1024
|
||||
LOG_FILE_BACKUP_COUNT = 5
|
||||
|
||||
log_path = Path(PATH_LOGS)
|
||||
log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
bot_logger = logging.getLogger("tgbot")
|
||||
bot_logger.setLevel(logging.INFO)
|
||||
bot_logger.propagate = False
|
||||
|
||||
if not bot_logger.handlers:
|
||||
file_formatter = logging.Formatter(
|
||||
"%(levelname)s | %(asctime)s | %(name)s | %(filename)s:%(lineno)d | %(message)s",
|
||||
datefmt="%d-%m-%Y %H:%M:%S",
|
||||
)
|
||||
console_formatter = colorlog.ColoredFormatter(
|
||||
"%(log_color)s%(levelname)s%(reset)s | %(blue)s%(asctime)s%(reset)s | "
|
||||
"%(purple)s%(filename)s:%(lineno)d%(reset)s | %(message)s",
|
||||
datefmt="%d-%m-%Y %H:%M:%S",
|
||||
log_colors={
|
||||
"DEBUG": "cyan",
|
||||
"INFO": "green",
|
||||
"WARNING": "yellow",
|
||||
"ERROR": "red",
|
||||
"CRITICAL": "bold_red",
|
||||
},
|
||||
)
|
||||
|
||||
file_handler = RotatingFileHandler(
|
||||
log_path,
|
||||
maxBytes=LOG_FILE_MAX_BYTES,
|
||||
backupCount=LOG_FILE_BACKUP_COUNT,
|
||||
encoding="utf-8",
|
||||
)
|
||||
file_handler.setFormatter(file_formatter)
|
||||
file_handler.setLevel(logging.INFO)
|
||||
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setFormatter(console_formatter)
|
||||
console_handler.setLevel(logging.INFO)
|
||||
|
||||
bot_logger.addHandler(file_handler)
|
||||
bot_logger.addHandler(console_handler)
|
||||
@@ -0,0 +1,7 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
from aiogram.fsm.context import FSMContext
|
||||
|
||||
from tgbot.services.api_session import AsyncRequestSession
|
||||
|
||||
FSM = FSMContext
|
||||
ARS = AsyncRequestSession
|
||||
@@ -0,0 +1,196 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
import asyncio
|
||||
import json
|
||||
from datetime import datetime
|
||||
from typing import Union
|
||||
|
||||
from aiogram import Bot
|
||||
from aiogram.types import FSInputFile, CallbackQuery, Message
|
||||
|
||||
from tgbot.data.config import BOT_DATABASE_EXPORT, BOT_STATUS_NOTIFICATION, BOT_VERSION, PATH_DATABASE, get_admins, \
|
||||
get_text_desc
|
||||
from tgbot.database import Userx, Settingsx
|
||||
from tgbot.utils.const_functions import get_unix, get_date, ded, send_admins
|
||||
from tgbot.utils.misc.bot_logging import bot_logger
|
||||
from tgbot.utils.misc.bot_models import ARS
|
||||
from tgbot.utils.text_functions import get_statistics
|
||||
|
||||
|
||||
# Автоматическая очистка ежедневной статистики после 00:00:15
|
||||
async def update_profit_day(bot: Bot):
|
||||
await send_admins(bot, await get_statistics())
|
||||
|
||||
await Settingsx().update(misc_profit_day=get_unix())
|
||||
|
||||
|
||||
# Автоматическая очистка еженедельной статистики в понедельник 00:00:10
|
||||
async def update_profit_week():
|
||||
await Settingsx().update(misc_profit_week=get_unix())
|
||||
|
||||
|
||||
# Автоматическое обновление счётчика каждый месяц первого числа в 00:00:05
|
||||
async def update_profit_month():
|
||||
await Settingsx().update(misc_profit_month=get_unix())
|
||||
|
||||
|
||||
# Автонастройка UNIX времени в БД
|
||||
async def autosettings_unix():
|
||||
now_day = datetime.now().day
|
||||
now_week = datetime.now().weekday()
|
||||
now_month = datetime.now().month
|
||||
now_year = datetime.now().year
|
||||
|
||||
unix_day = int(datetime.strptime(f"{now_day}.{now_month}.{now_year} 0:0:0", "%d.%m.%Y %H:%M:%S").timestamp())
|
||||
unix_week = unix_day - (now_week * 86400)
|
||||
unix_month = int(datetime.strptime(f"1.{now_month}.{now_year} 0:0:0", "%d.%m.%Y %H:%M:%S").timestamp())
|
||||
|
||||
await Settingsx().update(
|
||||
misc_profit_day=unix_day,
|
||||
misc_profit_week=unix_week,
|
||||
misc_profit_month=unix_month,
|
||||
)
|
||||
|
||||
|
||||
# Проверка на перенесение БД из старого бота в нового или указание токена нового бота
|
||||
async def check_bot_username(bot: Bot):
|
||||
get_login = await Settingsx().get()
|
||||
get_bot = await bot.get_me()
|
||||
|
||||
if get_bot.username != get_login.misc_bot:
|
||||
await Settingsx().update(misc_bot=get_bot.username)
|
||||
|
||||
|
||||
# Уведомление и проверка обновления при запуске бота
|
||||
async def startup_notify(bot: Bot, arSession: ARS):
|
||||
if len(get_admins()) >= 1 and BOT_STATUS_NOTIFICATION:
|
||||
await send_admins(
|
||||
bot,
|
||||
ded(f"""
|
||||
<b>✅ Бот был успешно запущен</b>
|
||||
➖➖➖➖➖➖➖➖➖➖
|
||||
{get_text_desc()}
|
||||
➖➖➖➖➖➖➖➖➖➖
|
||||
<code>❗ Данное сообщение видят только администраторы бота.</code>
|
||||
"""),
|
||||
)
|
||||
|
||||
await check_update(bot, arSession)
|
||||
|
||||
|
||||
# Автобэкапы БД для админов
|
||||
async def autobackup_admin(bot: Bot):
|
||||
if not BOT_DATABASE_EXPORT:
|
||||
return
|
||||
|
||||
for admin in get_admins():
|
||||
try:
|
||||
await bot.send_document(
|
||||
admin,
|
||||
FSInputFile(PATH_DATABASE),
|
||||
caption=f"<b>📦 #BACKUP | <code>{get_date(full=False)}</code></b>",
|
||||
disable_notification=True,
|
||||
)
|
||||
except Exception:
|
||||
bot_logger.warning("Не удалось отправить автобэкап админу %s", admin, exc_info=True)
|
||||
|
||||
|
||||
# Проверка наличия обновлений бота
|
||||
async def check_update(bot: Bot, arSession: ARS):
|
||||
session = await arSession.get_session()
|
||||
|
||||
try:
|
||||
response = await session.get(
|
||||
"https://djimbo.dev/autoshop_update.json",
|
||||
headers={"Accept-Encoding": "gzip, deflate"},
|
||||
)
|
||||
|
||||
response_data = json.loads((await response.read()).decode())
|
||||
|
||||
if float(response_data['version']) > float(BOT_VERSION):
|
||||
await send_admins(
|
||||
bot,
|
||||
ded(f"""
|
||||
<b>❇️ Вышло обновление: <a href='{response_data['download']}'>Скачать</a></b>
|
||||
➖➖➖➖➖➖➖➖➖➖
|
||||
{response_data['text']}
|
||||
➖➖➖➖➖➖➖➖➖➖
|
||||
<code>❗ Данное сообщение видят только администраторы бота.</code>
|
||||
"""),
|
||||
)
|
||||
except Exception:
|
||||
bot_logger.warning("Не удалось проверить обновления", exc_info=True)
|
||||
|
||||
|
||||
# Расссылка админам о критических ошибках и обновлениях
|
||||
async def check_mail(bot: Bot, arSession: ARS):
|
||||
session = await arSession.get_session()
|
||||
|
||||
try:
|
||||
response = await session.get(
|
||||
"https://djimbo.dev/autoshop_mail.json",
|
||||
headers={"Accept-Encoding": "gzip, deflate"},
|
||||
)
|
||||
response_data = json.loads((await response.read()).decode())
|
||||
|
||||
if response_data['status']:
|
||||
await send_admins(
|
||||
bot,
|
||||
ded(f"""
|
||||
{response_data['text']}
|
||||
➖➖➖➖➖➖➖➖➖➖
|
||||
<code>❗ Данное сообщение видят только администраторы бота.</code>
|
||||
"""),
|
||||
)
|
||||
except Exception:
|
||||
bot_logger.warning("Не удалось проверить сервисные уведомления", exc_info=True)
|
||||
|
||||
|
||||
# Вставка кастомных тэгов юзера в текст
|
||||
async def insert_tags(user_id: Union[int, str], text: str) -> str:
|
||||
get_user = await Userx().get_required(user_id=user_id)
|
||||
|
||||
if "{user_id}" in text:
|
||||
text = text.replace("{user_id}", f"<b>{get_user.user_id}</b>")
|
||||
if "{username}" in text:
|
||||
text = text.replace("{username}", f"<b>{get_user.user_login}</b>")
|
||||
if "{firstname}" in text:
|
||||
text = text.replace("{firstname}", f"<b>{get_user.user_name}</b>")
|
||||
|
||||
return text
|
||||
|
||||
|
||||
# Отправка рассылки
|
||||
async def functions_mail_make(bot: Bot, message: Message, call: CallbackQuery):
|
||||
users_receive, users_block, users_count = 0, 0, 0
|
||||
|
||||
get_users = await Userx().get_all()
|
||||
get_time = get_unix()
|
||||
|
||||
for user in get_users:
|
||||
try:
|
||||
await bot.copy_message(
|
||||
chat_id=user.user_id,
|
||||
from_chat_id=message.from_user.id,
|
||||
message_id=message.message_id,
|
||||
)
|
||||
users_receive += 1
|
||||
except Exception:
|
||||
users_block += 1
|
||||
bot_logger.debug("Пользователь %s не получил рассылку", user.user_id, exc_info=True)
|
||||
|
||||
users_count += 1
|
||||
|
||||
if users_count % 10 == 0:
|
||||
await call.message.edit_text(f"<b>📢 Рассылка началась... ({users_count}/{len(get_users)})</b>")
|
||||
|
||||
await asyncio.sleep(0.07)
|
||||
|
||||
await call.message.edit_text(
|
||||
ded(f"""
|
||||
<b>📢 Рассылка была завершена за <code>{get_unix() - get_time}сек</code></b>
|
||||
➖➖➖➖➖➖➖➖➖➖
|
||||
👤 Всего пользователей: <code>{len(get_users)}</code>
|
||||
✅ Пользователей получило сообщение: <code>{users_receive}</code>
|
||||
❌ Пользователей не получило сообщение: <code>{users_block}</code>
|
||||
""")
|
||||
)
|
||||
@@ -0,0 +1,143 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
from collections import defaultdict
|
||||
from typing import DefaultDict, List, Optional, Tuple
|
||||
|
||||
from tgbot.database import Categoryx, Itemx, Settingsx, Positionx, ModelCategory, ModelPosition
|
||||
|
||||
SYMBOLS_LIMIT = 4_000 # Кол-во макс символов в сообщение
|
||||
|
||||
|
||||
# Наличие товаров
|
||||
async def get_items_available(remover: int) -> Tuple[str, int, int]:
|
||||
get_categories = await Categoryx().get_all()
|
||||
get_positions = await Positionx().get_all()
|
||||
|
||||
item_counts = await Positionx().item_counts()
|
||||
|
||||
positions_by_category: DefaultDict[int, List[Tuple[str, float, int]]] = defaultdict(list)
|
||||
for position in get_positions:
|
||||
in_stock = item_counts.get(position.position_id, 0)
|
||||
|
||||
if in_stock > 0:
|
||||
positions_by_category[position.category_id].append(
|
||||
(position.position_name, position.position_price, in_stock)
|
||||
)
|
||||
|
||||
category_sections: List[Tuple[str, List[Tuple[str, float, int]]]] = []
|
||||
for category in get_categories:
|
||||
category_positions = positions_by_category.get(category.category_id, [])
|
||||
|
||||
if len(category_positions) == 0:
|
||||
continue
|
||||
|
||||
category_sections.append((category.category_name, category_positions))
|
||||
|
||||
pages = parse_items_available(category_sections)
|
||||
max_page = len(pages)
|
||||
|
||||
if max_page == 0:
|
||||
return "", 0, 0
|
||||
|
||||
remover = max(0, min(remover, max_page - 1))
|
||||
|
||||
return pages[remover], max_page, remover
|
||||
|
||||
|
||||
# Форматирование товаров для вывода наличия
|
||||
def _format_position_line(
|
||||
position_name: str,
|
||||
position_price: float,
|
||||
in_stock: int,
|
||||
max_len: Optional[int] = None,
|
||||
) -> str:
|
||||
prefix = "• "
|
||||
suffix = f" - {position_price}₽ - {in_stock} шт"
|
||||
full_line = f"{prefix}{position_name}{suffix}"
|
||||
|
||||
if max_len is None or len(full_line) <= max_len:
|
||||
return full_line
|
||||
|
||||
max_name_len = max(1, max_len - len(prefix) - len(suffix) - 1)
|
||||
short_name = f"{position_name[:max_name_len]}..."
|
||||
|
||||
return f"{prefix}{short_name}{suffix}"
|
||||
|
||||
|
||||
# Перебор товаров в списке для вывода
|
||||
def parse_items_available(category_sections: List[Tuple[str, List[Tuple[str, float, int]]]]) -> List[str]:
|
||||
current_page = ""
|
||||
pages: List[str] = []
|
||||
|
||||
for category_name, positions in category_sections:
|
||||
header_category = f"<b>➖➖➖ {category_name} ➖➖➖</b>"
|
||||
|
||||
header_block = header_category if current_page == "" else f"\n\n{header_category}"
|
||||
candidate_with_header = current_page + header_block
|
||||
|
||||
if len(candidate_with_header) > SYMBOLS_LIMIT:
|
||||
if current_page != "":
|
||||
pages.append(current_page)
|
||||
current_page = header_category
|
||||
else:
|
||||
current_page = candidate_with_header
|
||||
|
||||
for position_name, position_price, in_stock in positions:
|
||||
line = _format_position_line(position_name, position_price, in_stock)
|
||||
candidate_with_line = current_page + f"\n{line}"
|
||||
|
||||
if len(candidate_with_line) > SYMBOLS_LIMIT:
|
||||
if current_page != "":
|
||||
pages.append(current_page)
|
||||
|
||||
current_page = header_category
|
||||
|
||||
remain_len = SYMBOLS_LIMIT - len(current_page) - 1
|
||||
line = _format_position_line(position_name, position_price, in_stock, max_len=remain_len)
|
||||
current_page += f"\n{line}"
|
||||
else:
|
||||
current_page = candidate_with_line
|
||||
|
||||
if current_page != "":
|
||||
pages.append(current_page)
|
||||
|
||||
return pages
|
||||
|
||||
|
||||
# Получение категорий с товарами
|
||||
async def get_categories_items() -> List[ModelCategory]:
|
||||
get_settings = await Settingsx().get()
|
||||
|
||||
get_categories = await Categoryx().get_all()
|
||||
|
||||
save_categories = []
|
||||
|
||||
if get_settings.misc_hide_category == "True":
|
||||
for category in get_categories:
|
||||
get_positions = await get_positions_items(category.category_id)
|
||||
|
||||
if len(get_positions) >= 1:
|
||||
save_categories.append(category)
|
||||
else:
|
||||
save_categories = get_categories
|
||||
|
||||
return save_categories
|
||||
|
||||
|
||||
# Получение позиций с товарами
|
||||
async def get_positions_items(category_id: int) -> List[ModelPosition]:
|
||||
get_settings = await Settingsx().get()
|
||||
|
||||
get_positions = await Positionx().gets(category_id=category_id)
|
||||
|
||||
save_positions = []
|
||||
|
||||
if get_settings.misc_hide_position == "True":
|
||||
for position in get_positions:
|
||||
get_items = await Itemx().gets(position_id=position.position_id)
|
||||
|
||||
if len(get_items) >= 1:
|
||||
save_positions.append(position)
|
||||
else:
|
||||
save_positions = get_positions
|
||||
|
||||
return save_positions
|
||||
@@ -0,0 +1,438 @@
|
||||
# - *- coding: utf- 8 - *-
|
||||
from datetime import datetime
|
||||
from typing import Union
|
||||
|
||||
import pytz
|
||||
from aiogram import Bot
|
||||
from aiogram.types import LinkPreviewOptions
|
||||
from aiogram.utils.markdown import hide_link
|
||||
|
||||
from tgbot.data.config import BOT_TIMEZONE
|
||||
from tgbot.database import (
|
||||
Categoryx,
|
||||
Positionx,
|
||||
Itemx,
|
||||
Purchasesx,
|
||||
Refillx,
|
||||
Settingsx,
|
||||
Userx,
|
||||
ModelPurchases,
|
||||
ModelRefill,
|
||||
ModelUser,
|
||||
)
|
||||
from tgbot.keyboards.inline_admin import profile_edit_finl
|
||||
from tgbot.keyboards.inline_admin_products import position_edit_open_finl, category_edit_open_finl, item_delete_finl
|
||||
from tgbot.keyboards.inline_user import user_profile_finl
|
||||
from tgbot.keyboards.inline_user_products import products_open_finl
|
||||
from tgbot.services.api_hosting_text import HostingAPI
|
||||
from tgbot.utils.const_functions import ded, get_unix, convert_day, convert_date
|
||||
from tgbot.utils.misc.bot_models import ARS
|
||||
|
||||
|
||||
################################################################################
|
||||
################################# ПОЛЬЗОВАТЕЛЬ #################################
|
||||
# Открытие профиля пользователем
|
||||
async def open_profile_user(bot: Bot, user_id: Union[int, str]):
|
||||
get_purchases = await Purchasesx().gets(user_id=user_id)
|
||||
get_user = await Userx().get_required(user_id=user_id)
|
||||
|
||||
how_days = int(get_unix() - get_user.user_unix) // 60 // 60 // 24
|
||||
count_items = sum([purchase.purchase_count for purchase in get_purchases])
|
||||
|
||||
await bot.send_message(
|
||||
chat_id=user_id,
|
||||
text=ded(f"""
|
||||
<b>👤 Ваш профиль</b>
|
||||
➖➖➖➖➖➖➖➖➖➖
|
||||
🆔 ID: <code>{get_user.user_id}</code>
|
||||
💰 Баланс: <code>{get_user.user_balance}₽</code>
|
||||
🎁 Куплено товаров: <code>{count_items}шт</code>
|
||||
|
||||
🕰 Регистрация: <code>{convert_date(get_user.user_unix, False, False)} ({convert_day(how_days)})</code>
|
||||
"""),
|
||||
reply_markup=user_profile_finl(),
|
||||
)
|
||||
|
||||
|
||||
# Открытие позиции пользователем
|
||||
async def position_open_user(bot: Bot, user_id: int, position_id: int, remover: int):
|
||||
get_items = await Itemx().gets(position_id=position_id)
|
||||
get_position = await Positionx().get_required(position_id=position_id)
|
||||
get_category = await Categoryx().get_required(category_id=get_position.category_id)
|
||||
|
||||
if get_position.position_desc != "None":
|
||||
text_desc = f"▪️ Описание: {get_position.position_desc}"
|
||||
else:
|
||||
text_desc = ""
|
||||
|
||||
await bot.send_message(
|
||||
chat_id=user_id,
|
||||
text=ded(f"""
|
||||
<b>🎁 Покупка товара</b>{hide_link(get_position.position_photo)}
|
||||
➖➖➖➖➖➖➖➖➖➖
|
||||
▪️ Название: <code>{get_position.position_name}</code>
|
||||
▪️ Категория: <code>{get_category.category_name}</code>
|
||||
▪️ Стоимость: <code>{get_position.position_price}₽</code>
|
||||
▪️ Количество: <code>{len(get_items)}шт</code>
|
||||
{text_desc}
|
||||
"""),
|
||||
link_preview_options=LinkPreviewOptions(show_above_text=True),
|
||||
reply_markup=products_open_finl(position_id, get_position.category_id, remover),
|
||||
|
||||
)
|
||||
|
||||
|
||||
################################################################################
|
||||
#################################### АДМИН #####################################
|
||||
# Открытие профиля админом
|
||||
async def open_profile_admin(bot: Bot, user_id: int, get_user: ModelUser):
|
||||
get_purchases = await Purchasesx().gets(user_id=get_user.user_id)
|
||||
|
||||
how_days = int(get_unix() - get_user.user_unix) // 60 // 60 // 24
|
||||
count_items = sum([purchase.purchase_count for purchase in get_purchases])
|
||||
|
||||
await bot.send_message(
|
||||
chat_id=user_id,
|
||||
text=ded(f"""
|
||||
<b>👤 Профиль пользователя: <a href='tg://user?id={get_user.user_id}'>{get_user.user_name}</a></b>
|
||||
➖➖➖➖➖➖➖➖➖➖
|
||||
▪️ ID: <code>{get_user.user_id}</code>
|
||||
▪️ Логин: <b>@{get_user.user_login}</b>
|
||||
▪️ Имя: <a href='tg://user?id={get_user.user_id}'>{get_user.user_name}</a>
|
||||
▪️ Регистрация: <code>{convert_date(get_user.user_unix, False, False)} ({convert_day(how_days)})</code>
|
||||
|
||||
▪️ Баланс: <code>{get_user.user_balance}₽</code>
|
||||
▪️ Всего выдано: <code>{get_user.user_give}₽</code>
|
||||
▪️ Всего пополнено: <code>{get_user.user_refill}₽</code>
|
||||
▪️ Куплено товаров: <code>{count_items}шт</code>
|
||||
"""),
|
||||
reply_markup=profile_edit_finl(get_user.user_id),
|
||||
)
|
||||
|
||||
|
||||
# Открытие пополнения админом
|
||||
async def refill_open_admin(bot: Bot, user_id: int, get_refill: ModelRefill):
|
||||
get_user = await Userx().get_required(user_id=get_refill.user_id)
|
||||
|
||||
if get_refill.refill_method in ['Form', 'Nickname', 'Number', 'QIWI']:
|
||||
pay_method = "QIWI 🥝"
|
||||
elif get_refill.refill_method == "Yoomoney":
|
||||
pay_method = "ЮMoney 🔮"
|
||||
elif get_refill.refill_method == "Cryptobot":
|
||||
pay_method = "CryptoBot 🔷"
|
||||
else:
|
||||
pay_method = f"{get_refill.refill_method}"
|
||||
|
||||
await bot.send_message(
|
||||
chat_id=user_id,
|
||||
text=ded(f"""
|
||||
<b>🧾 Чек: <code>#{get_refill.refill_receipt}</code></b>
|
||||
➖➖➖➖➖➖➖➖➖➖
|
||||
▪️ Пользователь: <a href='tg://user?id={get_user.user_id}'>{get_user.user_name}</a> | <code>{get_user.user_id}</code>
|
||||
▪️ Сумма пополнения: <code>{get_refill.refill_amount}₽</code>
|
||||
▪️ Способ пополнения: <code>{pay_method}</code>
|
||||
▪️ Комментарий: <code>{get_refill.refill_comment}</code>
|
||||
▪️ Дата пополнения: <code>{convert_date(get_refill.refill_unix)}</code>
|
||||
"""),
|
||||
)
|
||||
|
||||
|
||||
# Открытие покупки админом
|
||||
async def purchase_open_admin(bot: Bot, arSession: ARS, user_id: int, get_purchase: ModelPurchases):
|
||||
get_user = await Userx().get_required(user_id=get_purchase.user_id)
|
||||
|
||||
link_items = await (
|
||||
await HostingAPI.connect(
|
||||
bot=bot,
|
||||
arSession=arSession,
|
||||
)
|
||||
).upload_text(get_purchase.purchase_data)
|
||||
|
||||
await bot.send_message(
|
||||
chat_id=user_id,
|
||||
text=ded(f"""
|
||||
<b>🧾 Чек: <code>#{get_purchase.purchase_receipt}</code></b>
|
||||
➖➖➖➖➖➖➖➖➖➖
|
||||
▪️ Пользователь: <a href='tg://user?id={get_user.user_id}'>{get_user.user_name}</a> | <code>{get_user.user_id}</code>
|
||||
|
||||
▪️ Название товара: <code>{get_purchase.purchase_position_name}</code>
|
||||
▪️ Куплено товаров: <code>{get_purchase.purchase_count}шт</code>
|
||||
▪️ Цена одного товара: <code>{get_purchase.purchase_price_one}₽</code>
|
||||
▪️ Сумма покупки: <code>{get_purchase.purchase_price}₽</code>
|
||||
|
||||
▪️ Баланс до покупки: <code>{get_purchase.user_balance_before}₽</code>
|
||||
▪️ Баланс после покупки: <code>{get_purchase.user_balance_after}₽</code>
|
||||
|
||||
▪️ Товары: <a href='{link_items}'>кликабельно</a>
|
||||
▪️ Дата покупки: <code>{convert_date(get_purchase.purchase_unix)}</code>
|
||||
"""),
|
||||
disable_web_page_preview=True,
|
||||
)
|
||||
|
||||
|
||||
# Открытие категории админом
|
||||
async def category_open_admin(bot: Bot, user_id: int, category_id: int, remover: int):
|
||||
profit_amount_all, profit_amount_day, profit_amount_week, profit_amount_month = 0, 0, 0, 0
|
||||
profit_count_all, profit_count_day, profit_count_week, profit_count_month = 0, 0, 0, 0
|
||||
|
||||
get_items = await Itemx().gets(category_id=category_id)
|
||||
get_category = await Categoryx().get_required(category_id=category_id)
|
||||
get_positions = await Positionx().gets(category_id=category_id)
|
||||
|
||||
get_purchases = await Purchasesx().gets(purchase_category_id=category_id)
|
||||
get_settings = await Settingsx().get()
|
||||
|
||||
for purchase in get_purchases:
|
||||
profit_amount_all += purchase.purchase_price
|
||||
profit_count_all += purchase.purchase_count
|
||||
|
||||
if purchase.purchase_unix - get_settings.misc_profit_day >= 0:
|
||||
profit_amount_day += purchase.purchase_price
|
||||
profit_count_day += purchase.purchase_count
|
||||
if purchase.purchase_unix - get_settings.misc_profit_week >= 0:
|
||||
profit_amount_week += purchase.purchase_price
|
||||
profit_count_week += purchase.purchase_count
|
||||
if purchase.purchase_unix - get_settings.misc_profit_month >= 0:
|
||||
profit_amount_month += purchase.purchase_price
|
||||
profit_count_month += purchase.purchase_count
|
||||
|
||||
await bot.send_message(
|
||||
chat_id=user_id,
|
||||
text=ded(f"""
|
||||
<b>🗃️ Редактирование категории</b>
|
||||
➖➖➖➖➖➖➖➖➖➖
|
||||
▪️ Категория: <code>{get_category.category_name}</code>
|
||||
▪️ Кол-во позиций: <code>{len(get_positions)}шт</code>
|
||||
▪️ Кол-во товаров: <code>{len(get_items)}шт</code>
|
||||
▪️ Дата создания: <code>{convert_date(get_category.category_unix)}шт</code>
|
||||
|
||||
💸 Продаж за День: <code>{profit_count_day}шт</code> - <code>{round(profit_amount_day, 2)}₽</code>
|
||||
💸 Продаж за Неделю: <code>{profit_count_week}шт</code> - <code>{round(profit_amount_week, 2)}₽</code>
|
||||
💸 Продаж за Месяц: <code>{profit_count_month}шт</code> - <code>{round(profit_amount_month, 2)}₽</code>
|
||||
💸 Продаж за Всё время: <code>{profit_count_all}шт</code> - <code>{round(profit_amount_all, 2)}₽</code>
|
||||
"""),
|
||||
reply_markup=await category_edit_open_finl(bot, category_id, remover),
|
||||
)
|
||||
|
||||
|
||||
# Открытие позиции админом
|
||||
async def position_open_admin(bot: Bot, position_id: int, user_id: int):
|
||||
profit_amount_all, profit_amount_day, profit_amount_week, profit_amount_month = 0, 0, 0, 0
|
||||
profit_count_all, profit_count_day, profit_count_week, profit_count_month = 0, 0, 0, 0
|
||||
|
||||
get_items = await Itemx().gets(position_id=position_id)
|
||||
get_position = await Positionx().get_required(position_id=position_id)
|
||||
get_category = await Categoryx().get_required(category_id=get_position.category_id)
|
||||
|
||||
get_settings = await Settingsx().get()
|
||||
get_purchases = await Purchasesx().gets(purchase_position_id=position_id)
|
||||
|
||||
# Наличие фото
|
||||
if get_position.position_photo != "None":
|
||||
position_photo_text = "<code>Присутствует ✅</code>"
|
||||
else:
|
||||
position_photo_text = "<code>Отсутствует ❌</code>"
|
||||
|
||||
# Наличие описания
|
||||
if get_position.position_desc != "None":
|
||||
position_desc = f"{get_position.position_desc}"
|
||||
else:
|
||||
position_desc = "<code>Отсутствует ❌</code>"
|
||||
|
||||
# Статистика позиции
|
||||
for purchase in get_purchases:
|
||||
profit_amount_all += purchase.purchase_price
|
||||
profit_count_all += purchase.purchase_count
|
||||
|
||||
if purchase.purchase_unix - get_settings.misc_profit_day >= 0:
|
||||
profit_amount_day += purchase.purchase_price
|
||||
profit_count_day += purchase.purchase_count
|
||||
if purchase.purchase_unix - get_settings.misc_profit_week >= 0:
|
||||
profit_amount_week += purchase.purchase_price
|
||||
profit_count_week += purchase.purchase_count
|
||||
if purchase.purchase_unix - get_settings.misc_profit_month >= 0:
|
||||
profit_amount_month += purchase.purchase_price
|
||||
profit_count_month += purchase.purchase_count
|
||||
|
||||
await bot.send_message(
|
||||
chat_id=user_id,
|
||||
text=ded(f"""
|
||||
<b>📁 Редактирование позиции</b>{hide_link(get_position.position_photo)}
|
||||
➖➖➖➖➖➖➖➖➖➖
|
||||
▪️ Категория: <code>{get_category.category_name}</code>
|
||||
▪️ Позиция: <code>{get_position.position_name}</code>
|
||||
▪️ Стоимость: <code>{get_position.position_price}₽</code>
|
||||
▪️ Количество: <code>{len(get_items)}шт</code>
|
||||
▪️ Дата создания: <code>{convert_date(get_category.category_unix)}</code>
|
||||
▪️ Изображение: {position_photo_text}
|
||||
▪️ Описание: {position_desc}
|
||||
|
||||
💸 Продаж за День: <code>{profit_count_day}шт</code> - <code>{round(profit_amount_day, 2)}₽</code>
|
||||
💸 Продаж за Неделю: <code>{profit_count_week}шт</code> - <code>{round(profit_amount_week, 2)}₽</code>
|
||||
💸 Продаж за Месяц: <code>{profit_count_month}шт</code> - <code>{round(profit_amount_month, 2)}₽</code>
|
||||
💸 Продаж за Всё время: <code>{profit_count_all}шт</code> - <code>{round(profit_amount_all, 2)}₽</code>
|
||||
"""),
|
||||
link_preview_options=LinkPreviewOptions(show_above_text=True),
|
||||
reply_markup=await position_edit_open_finl(bot, position_id, 0),
|
||||
|
||||
)
|
||||
|
||||
|
||||
# Открытие товара админом
|
||||
async def item_open_admin(bot: Bot, item_id: int, user_id: int):
|
||||
get_item = await Itemx().get_required(item_id=item_id)
|
||||
|
||||
get_position = await Positionx().get_required(position_id=get_item.position_id)
|
||||
get_category = await Categoryx().get_required(category_id=get_item.category_id)
|
||||
|
||||
await bot.send_message(
|
||||
chat_id=user_id,
|
||||
text=ded(f"""
|
||||
<b>🎁️ Редактирование товара</b>
|
||||
➖➖➖➖➖➖➖➖➖➖
|
||||
▪️ Категория: <code>{get_category.category_name}</code>
|
||||
▪️ Позиция: <code>{get_position.position_name}</code>
|
||||
▪️ Дата добавления: <code>{convert_date(get_item.item_unix)}</code>
|
||||
▪️ Товар: <code>{get_item.item_data}</code>
|
||||
"""),
|
||||
reply_markup=item_delete_finl(get_item.item_id, get_item.position_id),
|
||||
)
|
||||
|
||||
|
||||
################################################################################
|
||||
################################################################################
|
||||
# Статистика бота
|
||||
async def get_statistics() -> str:
|
||||
refill_amount_all, refill_amount_day, refill_amount_week, refill_amount_month = 0, 0, 0, 0
|
||||
refill_count_all, refill_count_day, refill_count_week, refill_count_month = 0, 0, 0, 0
|
||||
profit_amount_all, profit_amount_day, profit_amount_week, profit_amount_month = 0, 0, 0, 0
|
||||
profit_count_all, profit_count_day, profit_count_week, profit_count_month = 0, 0, 0, 0
|
||||
users_all, users_day, users_week, users_month, users_money_have, users_money_give = 0, 0, 0, 0, 0, 0
|
||||
refill_cryptobot_count, refill_cryptobot_amount = 0, 0
|
||||
refill_yoomoney_count, refill_yoomoney_amount = 0, 0
|
||||
refill_stars_count, refill_stars_amount = 0, 0
|
||||
|
||||
get_categories = await Categoryx().get_all()
|
||||
get_positions = await Positionx().get_all()
|
||||
get_purchases = await Purchasesx().get_all()
|
||||
get_refill = await Refillx().get_all()
|
||||
get_items = await Itemx().get_all()
|
||||
get_users = await Userx().get_all()
|
||||
get_settings = await Settingsx().get()
|
||||
|
||||
# Покупки
|
||||
for purchase in get_purchases:
|
||||
profit_amount_all += purchase.purchase_price
|
||||
profit_count_all += purchase.purchase_count
|
||||
|
||||
if purchase.purchase_unix - get_settings.misc_profit_day >= 0:
|
||||
profit_amount_day += purchase.purchase_price
|
||||
profit_count_day += purchase.purchase_count
|
||||
if purchase.purchase_unix - get_settings.misc_profit_week >= 0:
|
||||
profit_amount_week += purchase.purchase_price
|
||||
profit_count_week += purchase.purchase_count
|
||||
if purchase.purchase_unix - get_settings.misc_profit_month >= 0:
|
||||
profit_amount_month += purchase.purchase_price
|
||||
profit_count_month += purchase.purchase_count
|
||||
|
||||
# Пополнения
|
||||
for refill in get_refill:
|
||||
refill_amount_all += refill.refill_amount
|
||||
refill_count_all += 1
|
||||
|
||||
if refill.refill_method == "Yoomoney":
|
||||
refill_yoomoney_count += 1
|
||||
refill_yoomoney_amount += refill.refill_amount
|
||||
elif refill.refill_method == "Cryptobot":
|
||||
refill_cryptobot_count += 1
|
||||
refill_cryptobot_amount += refill.refill_amount
|
||||
elif refill.refill_method == "Stars":
|
||||
refill_stars_count += 1
|
||||
refill_stars_amount += refill.refill_amount
|
||||
|
||||
if refill.refill_unix - get_settings.misc_profit_day >= 0:
|
||||
refill_amount_day += refill.refill_amount
|
||||
refill_count_day += 1
|
||||
if refill.refill_unix - get_settings.misc_profit_week >= 0:
|
||||
refill_amount_week += refill.refill_amount
|
||||
refill_count_week += 1
|
||||
if refill.refill_unix - get_settings.misc_profit_month >= 0:
|
||||
refill_amount_month += refill.refill_amount
|
||||
refill_count_month += 1
|
||||
|
||||
# Пользователи и средства
|
||||
for user in get_users:
|
||||
users_money_have += user.user_balance
|
||||
users_money_give += user.user_give
|
||||
users_all += 1
|
||||
|
||||
if user.user_unix - get_settings.misc_profit_day >= 0:
|
||||
users_day += 1
|
||||
if user.user_unix - get_settings.misc_profit_week >= 0:
|
||||
users_week += 1
|
||||
if user.user_unix - get_settings.misc_profit_month >= 0:
|
||||
users_month += 1
|
||||
|
||||
# Даты обновления статистики
|
||||
all_days = [
|
||||
'Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота', 'Воскресенье',
|
||||
]
|
||||
|
||||
all_months = [
|
||||
'Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь', 'Июль', 'Август', 'Сентябрь',
|
||||
'Октябрь', 'Ноябрь', 'Декабрь'
|
||||
]
|
||||
|
||||
now_day = datetime.now().day
|
||||
now_week = datetime.now().weekday()
|
||||
now_month = datetime.now().month
|
||||
now_year = datetime.now().year
|
||||
|
||||
unix_day = int(datetime.strptime(f"{now_day}.{now_month}.{now_year} 0:0:0", "%d.%m.%Y %H:%M:%S").timestamp())
|
||||
unix_week = unix_day - (now_week * 86400)
|
||||
|
||||
week_day = int(datetime.fromtimestamp(unix_week, pytz.timezone(BOT_TIMEZONE)).strftime("%d"))
|
||||
week_month = int(datetime.fromtimestamp(unix_week, pytz.timezone(BOT_TIMEZONE)).strftime("%m"))
|
||||
week_week = int(datetime.fromtimestamp(unix_week, pytz.timezone(BOT_TIMEZONE)).weekday())
|
||||
|
||||
return ded(f"""
|
||||
<b>📊 СТАТИСТИКА БОТА</b>
|
||||
➖➖➖➖➖➖➖➖➖➖
|
||||
<b>👤 Пользователи</b>
|
||||
┣ Юзеров за День: <code>{users_day}</code>
|
||||
┣ Юзеров за Неделю: <code>{users_week}</code>
|
||||
┣ Юзеров за Месяц: <code>{users_month}</code>
|
||||
┗ Юзеров за Всё время: <code>{users_all}</code>
|
||||
|
||||
<b>💰 Средства</b>
|
||||
┣‒ Продажи (кол-во, сумма)
|
||||
┣ За День: <code>{profit_count_day}шт</code> - <code>{round(profit_amount_day, 2)}₽</code>
|
||||
┣ За Неделю: <code>{profit_count_week}шт</code> - <code>{round(profit_amount_week, 2)}₽</code>
|
||||
┣ За Месяц: <code>{profit_count_month}шт</code> - <code>{round(profit_amount_month, 2)}₽</code>
|
||||
┣ За Всё время: <code>{profit_count_all}шт</code> - <code>{round(profit_amount_all, 2)}₽</code>
|
||||
┃
|
||||
┣‒ Пополнения (кол-во, сумма)
|
||||
┣ За День: <code>{refill_count_day}шт</code> - <code>{round(refill_amount_day, 2)}₽</code>
|
||||
┣ За Неделю: <code>{refill_count_week}шт</code> - <code>{round(refill_amount_week, 2)}₽</code>
|
||||
┣ За Месяц: <code>{refill_count_month}шт</code> - <code>{round(refill_amount_month, 2)}₽</code>
|
||||
┣ За Всё время: <code>{refill_count_all}шт</code> - <code>{round(refill_amount_all, 2)}₽</code>
|
||||
┃
|
||||
┣‒ Платежные системы (всего)
|
||||
┣ CryptoBot: <code>{refill_cryptobot_count}шт</code> - <code>{round(refill_cryptobot_amount, 2)}₽</code>
|
||||
┣ TG Stars: <code>{refill_stars_count}шт</code> - <code>{round(refill_stars_amount, 2)}₽</code>
|
||||
┣ ЮMoney: <code>{refill_yoomoney_count}шт</code> - <code>{round(refill_yoomoney_amount, 2)}₽</code>
|
||||
┃
|
||||
┣‒ Остальные
|
||||
┣ Средств выдано: <code>{round(users_money_give, 2)}₽</code>
|
||||
┗ Средств в системе: <code>{round(users_money_have, 2)}₽</code>
|
||||
|
||||
<b>🎁 Товары</b>
|
||||
┣ Товаров: <code>{len(get_items)}шт</code>
|
||||
┣ Позиций: <code>{len(get_positions)}шт</code>
|
||||
┗ Категорий: <code>{len(get_categories)}шт</code>
|
||||
|
||||
<b>🕰 Даты статистики</b>
|
||||
┣ Дневная: <code>{now_day} {all_months[now_month - 1].title()}</code>
|
||||
┣ Недельная: <code>{week_day} {all_months[week_month - 1].title()}, {all_days[week_week]}</code>
|
||||
┗ Месячная: <code>1 {all_months[now_month - 1].title()}, {now_year}г</code>
|
||||
""")
|
||||
Reference in New Issue
Block a user