Initial local state

This commit is contained in:
2026-07-15 07:17:25 +05:00
commit 68221821a5
78 changed files with 10318 additions and 0 deletions
+35
View File
@@ -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
+10
View File
@@ -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
View File
@@ -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
View File
@@ -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"]
+61
View File
@@ -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
View File
@@ -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
View File
@@ -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
+11
View File
@@ -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
+196
View File
@@ -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
View File
@@ -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()
+184
View File
@@ -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())
+24
View File
@@ -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")
@@ -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 ###
@@ -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 ###
+31
View File
@@ -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*"]
+16
View File
@@ -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
View File
View File
+168
View File
@@ -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> бота, он ничем не сможет помочь. Бот является
открытым и распространяется публично. Абсолютно любой желающий мог его запустить.
""")
+20
View File
@@ -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
+47
View File
@@ -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()
+47
View File
@@ -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)
+73
View File
@@ -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)
+64
View File
@@ -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)
+79
View File
@@ -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()}
+331
View File
@@ -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
+146
View File
@@ -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"
+76
View File
@@ -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)
+128
View File
@@ -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
+110
View File
@@ -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
+55
View File
@@ -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")
+162
View File
@@ -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("База данных готова")
View File
+255
View File
@@ -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()
+188
View File
@@ -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()
+198
View File
@@ -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()
+82
View File
@@ -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
+84
View File
@@ -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()
+98
View File
@@ -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()
+41
View File
@@ -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()
+80
View File
@@ -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)
+20
View File
@@ -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)
+50
View File
@@ -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)
+53
View File
@@ -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) # Роутер пропущенных апдейтов
View File
+327
View File
@@ -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)
+120
View File
@@ -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>")
+355
View File
@@ -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(),
)
+969
View File
@@ -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>"
)
+376
View File
@@ -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>
""")
)
+35
View File
@@ -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,
)
+37
View File
@@ -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
"""),
)
+146
View File
@@ -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),
)
View File
+187
View File
@@ -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),
)
+276
View File
@@ -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>
""")
)
+407
View File
@@ -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
View File
+219
View File
@@ -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)
+239
View File
@@ -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']
+157
View File
@@ -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"
+31
View File
@@ -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()
+297
View File
@@ -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
+87
View File
@@ -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
+339
View File
@@ -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)
View File
+388
View File
@@ -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
View File
+33
View File
@@ -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)
+67
View File
@@ -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
+52
View File
@@ -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)
+7
View File
@@ -0,0 +1,7 @@
# - *- coding: utf- 8 - *-
from aiogram.fsm.context import FSMContext
from tgbot.services.api_session import AsyncRequestSession
FSM = FSMContext
ARS = AsyncRequestSession
+196
View File
@@ -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>
""")
)
+143
View File
@@ -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
+438
View File
@@ -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>
""")