From 562d7f1acf68e5caa80c1f7182fa5b3acdb4b963 Mon Sep 17 00:00:00 2001 From: djimbo Date: Sat, 13 Aug 2022 09:18:51 +0300 Subject: [PATCH] Update --- .gitignore | 189 ++++++++++++++++++++++++ main.py | 72 +++++++++ requirements.txt | 5 + tgbot/__init__.py | 1 + tgbot/config.py | 36 +++++ tgbot/data/__init__.py | 0 tgbot/data/config.py | 38 +++++ tgbot/keyboards/__init__.py | 0 tgbot/keyboards/inline_main.py | 22 +++ tgbot/keyboards/inline_misc.py | 22 +++ tgbot/keyboards/reply_main.py | 20 +++ tgbot/keyboards/reply_misc.py | 22 +++ tgbot/middlewares/__init__.py | 12 ++ tgbot/middlewares/exists_user.py | 48 ++++++ tgbot/middlewares/throttling.py | 59 ++++++++ tgbot/routers/__init__.py | 30 ++++ tgbot/routers/admin/__init__.py | 0 tgbot/routers/admin/admin_menu.py | 46 ++++++ tgbot/routers/main_errors.py | 23 +++ tgbot/routers/main_missed.py | 40 +++++ tgbot/routers/main_start.py | 19 +++ tgbot/routers/user/__init__.py | 0 tgbot/routers/user/user_menu.py | 34 +++++ tgbot/services/__init__.py | 1 + tgbot/services/api_session.py | 25 ++++ tgbot/services/api_sqlite.py | 125 ++++++++++++++++ tgbot/utils/__init__.py | 1 + tgbot/utils/const_functions.py | 235 ++++++++++++++++++++++++++++++ tgbot/utils/misc/__init__.py | 1 + tgbot/utils/misc/bot_commands.py | 30 ++++ tgbot/utils/misc/bot_filters.py | 14 ++ tgbot/utils/misc/bot_logging.py | 32 ++++ tgbot/utils/misc/bot_models.py | 10 ++ tgbot/utils/misc_functions.py | 24 +++ 34 files changed, 1236 insertions(+) create mode 100644 .gitignore create mode 100644 main.py create mode 100644 requirements.txt create mode 100644 tgbot/__init__.py create mode 100644 tgbot/config.py create mode 100644 tgbot/data/__init__.py create mode 100644 tgbot/data/config.py create mode 100644 tgbot/keyboards/__init__.py create mode 100644 tgbot/keyboards/inline_main.py create mode 100644 tgbot/keyboards/inline_misc.py create mode 100644 tgbot/keyboards/reply_main.py create mode 100644 tgbot/keyboards/reply_misc.py create mode 100644 tgbot/middlewares/__init__.py create mode 100644 tgbot/middlewares/exists_user.py create mode 100644 tgbot/middlewares/throttling.py create mode 100644 tgbot/routers/__init__.py create mode 100644 tgbot/routers/admin/__init__.py create mode 100644 tgbot/routers/admin/admin_menu.py create mode 100644 tgbot/routers/main_errors.py create mode 100644 tgbot/routers/main_missed.py create mode 100644 tgbot/routers/main_start.py create mode 100644 tgbot/routers/user/__init__.py create mode 100644 tgbot/routers/user/user_menu.py create mode 100644 tgbot/services/__init__.py create mode 100644 tgbot/services/api_session.py create mode 100644 tgbot/services/api_sqlite.py create mode 100644 tgbot/utils/__init__.py create mode 100644 tgbot/utils/const_functions.py create mode 100644 tgbot/utils/misc/__init__.py create mode 100644 tgbot/utils/misc/bot_commands.py create mode 100644 tgbot/utils/misc/bot_filters.py create mode 100644 tgbot/utils/misc/bot_logging.py create mode 100644 tgbot/utils/misc/bot_models.py create mode 100644 tgbot/utils/misc_functions.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..841ed68 --- /dev/null +++ b/.gitignore @@ -0,0 +1,189 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# 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.db +/tgbot/data/logs.log +settings.ini diff --git a/main.py b/main.py new file mode 100644 index 0000000..c34d51f --- /dev/null +++ b/main.py @@ -0,0 +1,72 @@ +# - *- coding: utf- 8 - *- +import asyncio +import os +import sys + +import colorama +from aiogram import Bot, Dispatcher + +from tgbot.config import BOT_TOKEN, scheduler, get_admins +from tgbot.middlewares import register_all_middlwares +from tgbot.routers import register_all_routers +from tgbot.services.api_session import RequestsSession +from tgbot.services.api_sqlite import create_dbx +from tgbot.utils.misc.bot_commands import set_commands +from tgbot.utils.misc.bot_logging import bot_logger +from tgbot.utils.misc_functions import autobackup, startup_notify + +colorama.init() + + +# Запуск шедулеров +async def scheduler_start(bot): + scheduler.add_job(autobackup, "cron", hour=00, args=(bot,)) # Автобэкап в 12:00 + scheduler.add_job(autobackup, "cron", hour=12, args=(bot,)) # Автобэкап в 00:00 + + +# Запуск бота и функций +async def main(): + scheduler.start() # Запуск Шедулера + dp = Dispatcher() # Создание Диспетчера + rSession = RequestsSession() # Асинхронные Запросы + bot = Bot(token=BOT_TOKEN, parse_mode="HTML") # Образ Бота + + register_all_middlwares(dp) # Регистрация всех мидлварей + register_all_routers(dp) # Регистрация всех роутеров + + try: + await set_commands(bot) + await startup_notify(bot) + await scheduler_start(bot) + + bot_logger.warning("Bot was started") + print(colorama.Fore.LIGHTYELLOW_EX + "~~~~~ Bot was started ~~~~~") + print(colorama.Fore.LIGHTBLUE_EX + "~~~~~ TG developer: @djimbox ~~~~~") + print(colorama.Fore.RESET) + + if len(get_admins()) == 0: print("***** ENTER ADMIN ID IN settings.ini *****") + + await bot.delete_webhook() + await bot.get_updates(offset=-1) + await dp.start_polling(bot, allowed_updates=dp.resolve_used_update_types(), rSession=rSession) + finally: + await rSession.close() + await bot.session.close() + + +if __name__ == "__main__": + create_dbx() + + try: + # Исправление "RuntimeError: Event loop is closed" для Windows + # if sys.version_info[0] == 3 and sys.version_info[1] >= 8 and sys.platform.startswith("win"): + # asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + + asyncio.run(main()) + except (KeyboardInterrupt, SystemExit): + bot_logger.warning("Bot was stopped") + finally: + if sys.platform.startswith("win"): + os.system("cls") + else: + os.system("clear") diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6968abc --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +colorlog +aiogram>=3.0.0b2 +aiohttp +typing +APScheduler==3.8.0 \ No newline at end of file diff --git a/tgbot/__init__.py b/tgbot/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/tgbot/__init__.py @@ -0,0 +1 @@ + diff --git a/tgbot/config.py b/tgbot/config.py new file mode 100644 index 0000000..525c76c --- /dev/null +++ b/tgbot/config.py @@ -0,0 +1,36 @@ +# - *- coding: utf- 8 - *- +import configparser + +from apscheduler.schedulers.asyncio import AsyncIOScheduler + +from tgbot.utils.const_functions import clear_list + +scheduler = AsyncIOScheduler() + +read_config = configparser.ConfigParser() +read_config.read("settings.ini") + +BOT_TOKEN = read_config["settings"]["token"].strip() +PATH_DATABASE = "tgbot/data/database.db" # Путь к БД +PATH_LOGS = "tgbot/data/logs.log" # Путь к Логам + + +# Получение администраторов бота +def get_admins(): + read_admins = configparser.ConfigParser() + read_admins.read("settings.ini") + + admins = read_admins["settings"]["admin_id"].strip() + admins = admins.replace(" ", "") + + if "," in admins: + admins = admins.split(",") + else: + if len(admins) >= 1: + admins = [admins] + else: + admins = [] + + admins = list(map(int, clear_list(admins))) + + return admins diff --git a/tgbot/data/__init__.py b/tgbot/data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tgbot/data/config.py b/tgbot/data/config.py new file mode 100644 index 0000000..d0cd133 --- /dev/null +++ b/tgbot/data/config.py @@ -0,0 +1,38 @@ +# - *- coding: utf- 8 - *- +import configparser + +from apscheduler.schedulers.asyncio import AsyncIOScheduler + +scheduler = AsyncIOScheduler() + +read_config = configparser.ConfigParser() +read_config.read("settings.ini") + +BOT_TOKEN = read_config['settings']['token'].strip() # Токен бота +PATH_DATABASE = "tgbot/data/database.db" # Путь к БД +PATH_LOGS = "tgbot/data/logs.log" # Путь к Логам + + +# Получение администраторов бота +def get_admins(): + read_admins = configparser.ConfigParser() + read_admins.read('settings.ini') + admins = read_admins['settings']['admin_id'].strip() + admins = admins.replace(' ', '') + + if ',' in admins: + admins = admins.split(',') + else: + if len(admins) >= 1: + admins = [admins] + else: + admins = [] + + while "" in admins: admins.remove("") + while " " in admins: admins.remove(" ") + while "," in admins: admins.remove(",") + while "\r" in admins: admins.remove("\r") + + admins = list(map(int, admins)) + + return admins diff --git a/tgbot/keyboards/__init__.py b/tgbot/keyboards/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tgbot/keyboards/inline_main.py b/tgbot/keyboards/inline_main.py new file mode 100644 index 0000000..a186d52 --- /dev/null +++ b/tgbot/keyboards/inline_main.py @@ -0,0 +1,22 @@ +# - *- coding: utf- 8 - *- +from aiogram.utils.keyboard import InlineKeyboardBuilder + +from tgbot.data.config import get_admins +from tgbot.utils.const_functions import ikb + + +# Кнопки инлайн меню +def menu_finl(user_id): + keyboard = InlineKeyboardBuilder( + ).row( + ikb("User 1", data="..."), + ikb("User 2", data="...") + ) + + if user_id in get_admins(): + keyboard.row( + ikb("Admin 1", data="..."), + ikb("Admin 2", data="...") + ) + + return keyboard.as_markup() diff --git a/tgbot/keyboards/inline_misc.py b/tgbot/keyboards/inline_misc.py new file mode 100644 index 0000000..73aa71c --- /dev/null +++ b/tgbot/keyboards/inline_misc.py @@ -0,0 +1,22 @@ +# - *- coding: utf- 8 - *- +from aiogram.utils.keyboard import InlineKeyboardBuilder + +from tgbot.utils.const_functions import ikb + +# Тестовые админ инлайн кнопки +admin_inl = InlineKeyboardBuilder( +).row( + ikb("Admin Inline 1", data="..."), + ikb("Admin Inline 2", data="..."), +).row( + ikb("Admin Inline 3", data="..."), +).as_markup() + +# Тестовые юзер инлайн кнопки +user_inl = InlineKeyboardBuilder( +).row( + ikb("User Inline 1", data="..."), + ikb("User Inline 2", data="..."), +).row( + ikb("User Inline 3", data="..."), +).as_markup() diff --git a/tgbot/keyboards/reply_main.py b/tgbot/keyboards/reply_main.py new file mode 100644 index 0000000..1523eb0 --- /dev/null +++ b/tgbot/keyboards/reply_main.py @@ -0,0 +1,20 @@ +# - *- coding: utf- 8 - *- +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): + keyboard = ReplyKeyboardBuilder( + ).row( + rkb("User Inline"), rkb("User Reply"), + ) + + if user_id in get_admins(): + keyboard.row( + rkb("Admin Inline"), rkb("Admin Reply"), + ) + + return keyboard.as_markup(resize_keyboard=True) diff --git a/tgbot/keyboards/reply_misc.py b/tgbot/keyboards/reply_misc.py new file mode 100644 index 0000000..36e9a30 --- /dev/null +++ b/tgbot/keyboards/reply_misc.py @@ -0,0 +1,22 @@ +# - *- coding: utf- 8 - *- +from aiogram.utils.keyboard import ReplyKeyboardBuilder + +from tgbot.utils.const_functions import rkb + +# Тестовые админ реплай кнопки +admin_rep = ReplyKeyboardBuilder( +).row( + rkb("Admin Reply 1"), + rkb("Admin Reply 2"), +).row( + rkb("⬅ Главное меню"), +).as_markup(resize_keyboard=True) + +# Тестовые юзер реплай кнопки +user_rep = ReplyKeyboardBuilder( +).row( + rkb("User Reply 1"), + rkb("User Reply 2"), +).row( + rkb("⬅ Главное меню"), +).as_markup(resize_keyboard=True) diff --git a/tgbot/middlewares/__init__.py b/tgbot/middlewares/__init__.py new file mode 100644 index 0000000..b1561ea --- /dev/null +++ b/tgbot/middlewares/__init__.py @@ -0,0 +1,12 @@ +# - *- coding: utf- 8 - *- +from aiogram import Dispatcher + +from tgbot.middlewares.exists_user import ExistsUserMiddleware +from tgbot.middlewares.throttling import ThrottlingMiddleware + + +def register_all_middlwares(dp: Dispatcher): + dp.callback_query.outer_middleware(ExistsUserMiddleware()) + dp.message.outer_middleware(ExistsUserMiddleware()) + + dp.message.middleware(ThrottlingMiddleware()) diff --git a/tgbot/middlewares/exists_user.py b/tgbot/middlewares/exists_user.py new file mode 100644 index 0000000..7d55a17 --- /dev/null +++ b/tgbot/middlewares/exists_user.py @@ -0,0 +1,48 @@ +# - *- coding: utf- 8 - *- +from aiogram import BaseMiddleware + +from tgbot.services.api_sqlite import get_userx, add_userx, update_userx +from tgbot.utils.const_functions import clear_html + + +# Проверка юзера в БД и его добавление +class ExistsUserMiddleware(BaseMiddleware): + async def __call__(self, handler, event, data): + this_user = data.get("event_from_user") + + if not this_user.is_bot: + get_user = get_userx(user_id=this_user.id) + + user_id = this_user.id + user_login = this_user.username + user_name = clear_html(this_user.first_name) + user_fullname = clear_html(this_user.first_name) + user_surname = clear_html(this_user.last_name) + user_language = this_user.language_code + + if user_language != "ru": user_language = "en" + if user_login is None: user_login = "" + if user_name is None: user_name = "" + if user_fullname is None: user_fullname = "" + if user_surname is None: user_surname = "" + + if len(user_surname) >= 1: user_fullname += f" {user_surname}" + + if get_user is None: + add_userx(user_id, user_login.lower(), user_name, user_surname, user_fullname) + else: + if user_name != get_user['user_name']: + update_userx(get_user['user_id'], user_name=user_name) + + if user_surname != get_user['user_surname']: + update_userx(get_user['user_id'], user_surname=user_surname) + + if user_fullname != get_user['user_fullname']: + update_userx(get_user['user_id'], user_fullname=user_fullname) + + if user_login.lower() != get_user['user_login']: + update_userx(get_user['user_id'], user_login=user_login.lower()) + + data['my_user'] = get_userx(user_id=user_id) + + return await handler(event, data) diff --git a/tgbot/middlewares/throttling.py b/tgbot/middlewares/throttling.py new file mode 100644 index 0000000..c8d0a7d --- /dev/null +++ b/tgbot/middlewares/throttling.py @@ -0,0 +1,59 @@ +# - *- coding: utf- 8 - *- +import time +from typing import Any, Awaitable, Callable, Dict + +from aiogram import BaseMiddleware +from aiogram.dispatcher.event.handler import HandlerObject +from aiogram.types import Message + +from tgbot.services.api_sqlite import get_userx + + +# Антиспам +class ThrottlingMiddleware(BaseMiddleware): + def __init__(self, default_rate: int = 0.6) -> None: + self.default_rate = default_rate + self.now_rate = default_rate + + self.last_throttled = int(time.time()) + self.count_throttled = 0 + + async def __call__(self, handler: Callable[[Message, Dict[str, Any]], Awaitable[Any]], event: Message, data): + real_handler: HandlerObject = data['handler'] + skip_pass = True + + this_user = data.get("event_from_user") + if not this_user.is_bot: + data['get_user'] = get_userx(user_id=this_user.id) + + if real_handler.flags.get("skip_pass") is not None: + skip_pass = real_handler.flags.get("skip_pass") + + if skip_pass: + if int(time.time()) - self.last_throttled >= self.now_rate: + self.last_throttled = int(time.time()) + self.now_rate = self.default_rate + self.count_throttled = 0 + + return await handler(event, data) + else: + if self.count_throttled == 0: + self.count_throttled += 1 + self.now_rate = self.default_rate * 2 + + return await handler(event, data) + elif self.count_throttled == 1: + self.count_throttled += 1 + self.now_rate = self.default_rate * 2 + + await event.reply("❗ Пожалуйста, не спамьте.") + else: + self.now_rate = 3 + + if self.count_throttled == 2: + self.count_throttled = 3 + await event.reply("❗ Бот не будет отвечать до прекращения спама.") + + self.last_throttled = int(time.time()) + else: + return await handler(event, data) diff --git a/tgbot/routers/__init__.py b/tgbot/routers/__init__.py new file mode 100644 index 0000000..b89c403 --- /dev/null +++ b/tgbot/routers/__init__.py @@ -0,0 +1,30 @@ +# - *- coding: utf- 8 - *- +from aiogram import Dispatcher, F + +from tgbot.routers import main_errors, main_missed, main_start +from tgbot.routers.admin import admin_menu +from tgbot.routers.user import user_menu +from tgbot.utils.misc.bot_filters import IsAdmin + + +# Регистрация роутеров +def register_all_routers(dp: Dispatcher): + # Подключение фильтров + main_errors.router.message.filter(F.chat.type == "private") + main_start.router.message.filter(F.chat.type == "private") + main_missed.router.message.filter(F.chat.type == "private") + + user_menu.router.message.filter(F.chat.type == "private") + + admin_menu.router.message.filter(F.chat.type == "private", IsAdmin()) + + # Инициализация обязательных роутеров + 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(main_missed.router) diff --git a/tgbot/routers/admin/__init__.py b/tgbot/routers/admin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tgbot/routers/admin/admin_menu.py b/tgbot/routers/admin/admin_menu.py new file mode 100644 index 0000000..03801b1 --- /dev/null +++ b/tgbot/routers/admin/admin_menu.py @@ -0,0 +1,46 @@ +# - *- coding: utf- 8 - *- +from aiogram import Router, Bot +from aiogram.types import FSInputFile, Message + +from tgbot.config import PATH_DATABASE, PATH_LOGS +from tgbot.data.config import PATH_DATABASE, PATH_LOGS +from tgbot.keyboards.inline_misc import admin_inl +from tgbot.keyboards.reply_misc import admin_rep +from tgbot.utils.const_functions import get_date +from tgbot.utils.misc.bot_models import FSM, RS + +router = Router() + + +# Кнопка - Admin Inline +@router.message(text="Admin Inline") +async def admin_button_inline(message: Message, bot: Bot, state: FSM, rSession: RS, my_user): + await state.clear() + + await message.answer("Click Button - Admin Inline", reply_markup=admin_inl) + + +# Кнопка - Admin Reply +@router.message(text="Admin Reply") +async def admin_button_reply(message: Message, bot: Bot, state: FSM, rSession: RS, my_user): + await state.clear() + + await message.answer("Click Button - Admin Reply", reply_markup=admin_rep) + + +# Получение Базы Данных +@router.message(commands=['db', 'database']) +async def admin_database(message: Message, bot: Bot, state: FSM, rSession: RS, my_user): + await state.clear() + + await message.answer_document(FSInputFile(PATH_DATABASE), + caption=f"📦 BACKUP\n" + f"🕰 {get_date()}") + + +# Получение логов +@router.message(commands=['log', 'logs']) +async def admin_log(message: Message, bot: Bot, state: FSM, rSession: RS, my_user): + await state.clear() + + await message.answer_document(FSInputFile(PATH_LOGS), caption=f"🕰 {get_date()}") diff --git a/tgbot/routers/main_errors.py b/tgbot/routers/main_errors.py new file mode 100644 index 0000000..b8ca250 --- /dev/null +++ b/tgbot/routers/main_errors.py @@ -0,0 +1,23 @@ +# - *- coding: utf- 8 - *- +from aiogram import Router, Bot +from aiogram.exceptions import TelegramAPIError +from aiogram.types import Update + +from tgbot.utils.misc.bot_logging import bot_logger +from tgbot.utils.misc_functions import send_admins + +router = Router() + + +# Обработка ошибок +@router.errors() +async def processing_errors(update: Update, exception: TelegramAPIError, bot: Bot): + print(f"-Exception | {exception}") + await send_admins(bot, f"❌ Ошибка\n\n" + f"Exception: {exception}\n\n" + f"Update: {update.dict()}") + + bot_logger.exception( + f"Exception: {exception}\n" + f"Update: {update.dict()}" + ) diff --git a/tgbot/routers/main_missed.py b/tgbot/routers/main_missed.py new file mode 100644 index 0000000..84f15e6 --- /dev/null +++ b/tgbot/routers/main_missed.py @@ -0,0 +1,40 @@ +# - *- coding: utf- 8 - *- +from aiogram import types, Router +from aiogram.types import CallbackQuery + +from tgbot.keyboards.reply_main import menu_frep +from tgbot.utils.misc.bot_models import FSM + +router = Router() + + +# Колбэк с удалением сообщения +@router.callback_query(text="close_this") +async def processing_callback_remove(call: CallbackQuery, state: FSM): + await call.message.delete() + + +# Колбэк с обработкой кнопки +@router.callback_query(text="...") +async def processing_callback_answer(call: CallbackQuery, state: FSM): + await call.answer(cache_time=20) + + +# Колбэк с обработкой удаления сообщений потерявших стейт +@router.callback_query() +async def processing_callback_missed(call: CallbackQuery, state: FSM): + try: + await call.message.delete() + except: + pass + + await call.message.answer("❌ Данные не были найдены из-за перезапуска скрипта.\n" + "♻ Выполните действие заново.", + reply_markup=menu_frep(call.from_user.id)) + + +# Обработка всех неизвестных команд +@router.message() +async def processing_message_missed(message: types.Message, state: FSM): + await message.answer("♦ Неизвестная команда.\n" + "▶ Введите /start") diff --git a/tgbot/routers/main_start.py b/tgbot/routers/main_start.py new file mode 100644 index 0000000..b917844 --- /dev/null +++ b/tgbot/routers/main_start.py @@ -0,0 +1,19 @@ +# - *- coding: utf- 8 - *- +from aiogram import Router, Bot +from aiogram.types import Message + +from tgbot.keyboards.reply_main import menu_frep +from tgbot.utils.misc.bot_models import FSM, RS + +router = Router() + + +# Открытие главного меню +@router.message(text_startswith=["⬅ Главное меню", "/start"]) +async def main_start(message: Message, bot: Bot, state: FSM, rSession: RS, my_user): + await state.clear() + + await message.answer("🔸 Бот готов к использованию.\n" + "🔸 Если не появились вспомогательные кнопки\n" + "▶ Введите /start", + reply_markup=menu_frep(message.from_user.id)) diff --git a/tgbot/routers/user/__init__.py b/tgbot/routers/user/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tgbot/routers/user/user_menu.py b/tgbot/routers/user/user_menu.py new file mode 100644 index 0000000..fd066f5 --- /dev/null +++ b/tgbot/routers/user/user_menu.py @@ -0,0 +1,34 @@ +# - *- coding: utf- 8 - *- +from aiogram import Router, Bot +from aiogram.types import Message + +from tgbot.keyboards.inline_main import menu_finl +from tgbot.keyboards.inline_misc import user_inl +from tgbot.keyboards.reply_misc import user_rep +from tgbot.utils.misc.bot_models import FSM, RS + +router = Router() + + +# Кнопка - User Inline +@router.message(text="User Inline") +async def user_button_inline(message: Message, bot: Bot, state: FSM, rSession: RS, my_user): + await state.clear() + + await message.answer("Click Button - User Inline", reply_markup=user_inl) + + +# Кнопка - User Reply +@router.message(text="User Reply") +async def user_button_reply(message: Message, bot: Bot, state: FSM, rSession: RS, my_user): + await state.clear() + + await message.answer("Click Button - User Reply", reply_markup=user_rep) + + +# Команда - /inline +@router.message(commands="inline") +async def user_command_inline(message: Message, bot: Bot, state: FSM, rSession: RS, my_user): + await state.clear() + + await message.answer("Click command - /inline", reply_markup=menu_finl(message.from_user.id)) diff --git a/tgbot/services/__init__.py b/tgbot/services/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/tgbot/services/__init__.py @@ -0,0 +1 @@ + diff --git a/tgbot/services/api_session.py b/tgbot/services/api_session.py new file mode 100644 index 0000000..b58fd0b --- /dev/null +++ b/tgbot/services/api_session.py @@ -0,0 +1,25 @@ +# - *- coding: utf- 8 - *- +from typing import Optional + +import aiohttp + + +# Асинхронная сессия для запросов +class RequestsSession: + def __init__(self) -> None: + self._session: Optional[aiohttp.ClientSession] = None + + # Получение сессии + async def get_session(self) -> aiohttp.ClientSession: + if self._session is None: + new_session = aiohttp.ClientSession() + self._session = new_session + + return self._session + + # Закрытие сессии + async def close(self) -> None: + if self._session is None: + return None + + await self._session.close() diff --git a/tgbot/services/api_sqlite.py b/tgbot/services/api_sqlite.py new file mode 100644 index 0000000..c96985c --- /dev/null +++ b/tgbot/services/api_sqlite.py @@ -0,0 +1,125 @@ +# - *- coding: utf- 8 - *- +import sqlite3 + +from tgbot.config import PATH_DATABASE +from tgbot.utils.const_functions import get_unix, get_date + + +# Преобразование БД словаря +def dict_factory(cursor, row): + this_dict = {} + + for idx, col in enumerate(cursor.description): + this_dict[col[0]] = row[idx] + + return this_dict + + +#################################################################################################### +##################################### ФОРМАТИРОВАНИЕ ЗАПРОСОВ ###################################### +# Форматирование запроса без аргументов +def update_format_with_args(sql, parameters: dict): + if "XXX" not in sql: + sql += " XXX " + + values = ", ".join([ + f"{item} = ?" for item in parameters + ]) + sql = sql.replace("XXX", values) + + return sql, list(parameters.values()) + + +# Форматирование запроса с аргументами +def update_format_args(sql, parameters: dict): + sql = f"{sql} WHERE " + + sql += " AND ".join([ + f"{item} = ?" for item in parameters + ]) + + return sql, list(parameters.values()) + + +####################################### ЗАПРОСЫ К БАЗЕ ДАННЫХ ###################################### +#################################################################################################### +# Добавление пользователя +def add_userx(user_id, user_login, user_name, user_surname, user_fullname): + with sqlite3.connect(PATH_DATABASE) as con: + con.row_factory = dict_factory + con.execute("INSERT INTO storage_users " + "(user_id, user_login, user_name, user_surname, user_fullname, user_date, user_unix) " + "VALUES (?, ?, ?, ?, ?, ?, ?)", + [user_id, user_login, user_name, user_surname, user_fullname, get_date(), get_unix()]) + con.commit() + + +# Получение пользователя +def get_userx(**kwargs): + with sqlite3.connect(PATH_DATABASE) as con: + con.row_factory = dict_factory + sql = "SELECT * FROM storage_users" + sql, parameters = update_format_args(sql, kwargs) + return con.execute(sql, parameters).fetchone() + + +# Получение пользователей +def get_usersx(**kwargs): + with sqlite3.connect(PATH_DATABASE) as con: + con.row_factory = dict_factory + sql = "SELECT * FROM storage_users" + sql, parameters = update_format_args(sql, kwargs) + return con.execute(sql, parameters).fetchall() + + +# Получение всех пользователей +def get_all_usersx(): + with sqlite3.connect(PATH_DATABASE) as con: + con.row_factory = dict_factory + sql = "SELECT * FROM storage_users" + return con.execute(sql).fetchall() + + +# Редактирование пользователя +def update_userx(user_id, **kwargs): + with sqlite3.connect(PATH_DATABASE) as con: + con.row_factory = dict_factory + sql = f"UPDATE storage_users SET" + sql, parameters = update_format_with_args(sql, kwargs) + parameters.append(user_id) + con.execute(sql + "WHERE user_id = ?", parameters) + con.commit() + + +# Удаление пользователя +def delete_userx(**kwargs): + with sqlite3.connect(PATH_DATABASE) as con: + con.row_factory = dict_factory + sql = "DELETE FROM storage_users" + sql, parameters = update_format_args(sql, kwargs) + con.execute(sql, parameters) + con.commit() + + +######################################## СОЗДАНИЕ БАЗЫ ДАННЫХ ###################################### +# Создание всех таблиц для Базы Данных +def create_dbx(): + with sqlite3.connect(PATH_DATABASE) as con: + con.row_factory = dict_factory + + # Таблица с хранением пользователей + if len(con.execute("PRAGMA table_info(storage_users)").fetchall()) == 8: + print("DB was found(1/1)") + else: + con.execute("CREATE TABLE storage_users(" + "increment INTEGER PRIMARY KEY AUTOINCREMENT, " + "user_id INTEGER," + "user_login TEXT," + "user_name TEXT," + "user_surname TEXT," + "user_fullname TEXT," + "user_date TIMESTAMP," + "user_unix INTEGER)") + print("DB was not found(1/1) | Creating...") + + con.commit() diff --git a/tgbot/utils/__init__.py b/tgbot/utils/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/tgbot/utils/__init__.py @@ -0,0 +1 @@ + diff --git a/tgbot/utils/const_functions.py b/tgbot/utils/const_functions.py new file mode 100644 index 0000000..bca6e9f --- /dev/null +++ b/tgbot/utils/const_functions.py @@ -0,0 +1,235 @@ +# - *- coding: utf- 8 - *- +import random +import time +from datetime import datetime + +from aiogram import Bot, types +from aiogram.types import InlineKeyboardButton, KeyboardButton + +######################################## AIOGRAM ######################################## +# Генерация реплай кнопки +from tgbot.config import get_admins + + +def rkb(text): + return KeyboardButton(text=text) + + +# Генерация инлайн кнопки +def ikb(text, data=None, url=None, switch=None): + if data is not None: + return InlineKeyboardButton(text=text, callback_data=data) + elif url is not None: + return InlineKeyboardButton(text=text, url=url) + else: + return InlineKeyboardButton(text=text, switch_inline_query=switch) + + +# Отправка сообщения всем админам +async def send_admins(bot: Bot, message, markup=None, not_me=0): + for admin in get_admins(): + try: + if str(admin) != str(not_me): + await bot.send_message(admin, message, reply_markup=markup, disable_web_page_preview=True) + except: + pass + + +# Умная отправка сообщений +async def smart_send(bot: Bot, message: types.Message, user_id, text_add=None, reply=None): + if message.text is not None: + get_text = message.text + elif message.caption is not None: + get_text = message.caption + else: + get_text = "" + + if text_add is not None: + get_text = text_add.format(get_text) + + if message.photo is not None: + await bot.send_photo(user_id, message.photo[-1].file_id, caption=get_text, reply_markup=reply) + elif message.video is not None: + await bot.send_video(user_id, message.video.file_id, caption=get_text, reply_markup=reply) + elif message.document is not None: + await bot.send_document(user_id, message.document.file_id, caption=get_text, reply_markup=reply) + elif message.audio is not None: + await bot.send_audio(user_id, message.audio.file_id, caption=get_text, reply_markup=reply) + elif message.voice is not None: + await bot.send_voice(user_id, message.voice.file_id, caption=get_text, reply_markup=reply) + elif message.animation is not None: + await bot.send_animation(user_id, message.animation.file_id, reply_markup=reply) + elif message.sticker is not None: + await bot.send_sticker(user_id, message.sticker.file_id, reply_markup=reply) + elif message.dice is not None: + await bot.send_dice(user_id, message.dice.emoji, reply_markup=reply) + elif message.location is not None: + await bot.send_location(user_id, latitude=message.location.latitude, longitude=message.location.longitude, + reply_markup=reply) + else: + await bot.send_message(user_id, get_text) + + +######################################## ПРОЧЕЕ ######################################## +# Очистка HTML тэгов +def clear_html(get_text: str): + if get_text is not None: + if "<" in get_text: get_text = get_text.replace("<", "*") + if ">" in get_text: get_text = get_text.replace(">", "*") + + return get_text + + +# Очистка пробелов в списке +def clear_list(get_list: list): + while "" in get_list: get_list.remove("") + while " " in get_list: get_list.remove(" ") + while "," in get_list: get_list.remove(",") + while "\r" in get_list: get_list.remove("\r") + + return get_list + + +# Разбив списка на несколько частей +def split_messages(get_list: list, count: int): + return [get_list[i:i + count] for i in range(0, len(get_list), count)] + + +# Получение даты +def get_date(): + this_date = datetime.today().replace(microsecond=0) + this_date = this_date.strftime("%d.%m.%Y %H:%M:%S") + + return this_date + + +# Получение юникс даты +def get_unix(): + return int(time.time()) + + +# Генерация пароля +def generate_password(len_password: int): + passwd = list("1234567890abcdefghigklmnopqrstuvyxwzABCDEFGHIGKLMNOPQRSTUVYXWZ") + random.shuffle(passwd) + random_chars = "".join([random.choice(passwd) for x in range(len_password)]) + + return random_chars + + +# Корректный вывод времени +def convert_times(get_time, get_type="day"): + get_time = int(get_time) + + if get_type == "second": + get_list = ["секунда", "секунды", "секунд"] + elif get_type == "minute": + get_list = ["минута", "минуты", "минут"] + elif get_type == "hour": + get_list = ["час", "часа", "часов"] + elif get_type == "day": + get_list = ["день", "дня", "дней"] + elif get_type == "month": + get_list = ["месяц", "месяца", "месяцев"] + else: + get_list = ["год", "года", "лет"] + + 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} {get_list[count]}" + + +######################################## ЧИСЛА ######################################## +# Конвертация числа в вещественное +def to_float(get_number, remains=2): + if "," in str(get_number): + get_number = str(get_number).replace(",", ".") + + if "." in get_number: + get_last = get_number.split(".") + + if get_last[1].endswith("0"): + while True: + if get_number.endswith("0"): + get_number = get_number[:-1] + else: + break + + get_number = round(float(get_number), remains) + + if str(float(get_number)).split(".")[1] == "0": + get_number = int(get_number) + else: + get_number = float(get_number) + + return get_number + + +# Конвертация числа в целочисленное +def to_int(get_number): + if "," in get_number: + get_number = get_number.replace(",", ".") + + get_number = int(round(float(get_number))) + + return get_number + + +# Проверка числа на вещественное +def is_number(get_number): + if get_number.isdigit(): + return False + else: + if "," in str(get_number): get_number = str(get_number).replace(",", ".") + + try: + float(get_number) + return False + except ValueError: + return True + + +# Преобразование длинных вещественных чисел в читаемый вид +def show_floats(amount): + return f"{float(amount):.{len(str(amount).split('.')[1])}f}" + + +# Форматирование чисел в читаемый вид +def format_rate(rate, around=False): + rate = str(round(float(rate), 2)) + + if "," in rate: + rate = float(rate.replace(",", ".")) + + len_rate = str(int(float(rate))) + + if len(len_rate) == 3: + get_rate = str(len_rate) + elif len(len_rate) == 4: + get_rate = f"{len_rate[0]} {len_rate[1:]}" + elif len(len_rate) == 5: + get_rate = f"{len_rate[0:2]} {len_rate[2:]}" + elif len(len_rate) == 6: + get_rate = f"{len_rate[0:3]} {len_rate[3:]}" + elif len(len_rate) == 7: + get_rate = f"{len_rate[0:1]} {len_rate[1:4]} {len_rate[4:]}" + elif len(len_rate) == 8: + get_rate = f"{len_rate[0:2]} {len_rate[2:5]} {len_rate[5:]}" + elif len(len_rate) == 9: + get_rate = f"{len_rate[0:3]} {len_rate[3:6]} {len_rate[6:]}" + elif len(len_rate) == 10: + get_rate = f"{len_rate[0:1]} {len_rate[1:4]} {len_rate[4:7]} {len_rate[7:]}" + else: + get_rate = 0 + + if not around: + if "." in rate: + dot_amount = rate.find(".") + get_rate += f"{rate[dot_amount:]}" + + return get_rate diff --git a/tgbot/utils/misc/__init__.py b/tgbot/utils/misc/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/tgbot/utils/misc/__init__.py @@ -0,0 +1 @@ + diff --git a/tgbot/utils/misc/bot_commands.py b/tgbot/utils/misc/bot_commands.py new file mode 100644 index 0000000..239f0b8 --- /dev/null +++ b/tgbot/utils/misc/bot_commands.py @@ -0,0 +1,30 @@ +# - *- coding: utf- 8 - *- +from aiogram import Bot +from aiogram.types import BotCommand, BotCommandScopeChat, BotCommandScopeDefault + +from tgbot.config import get_admins + +# Команды для юзеров +user_commands = [ + BotCommand(command="start", description="♻ Перезапустить бота"), + BotCommand(command="inline", description="🌀 Получить Inline клавиатуру"), +] + +# Команды для админов +admin_commands = [ + BotCommand(command="start", description="♻ Перезапустить бота"), + BotCommand(command="inline", description="🌀 Получить Inline клавиатуру"), + BotCommand(command="log", description="🖨 Получить Логи"), + BotCommand(command="db", 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: + pass diff --git a/tgbot/utils/misc/bot_filters.py b/tgbot/utils/misc/bot_filters.py new file mode 100644 index 0000000..59668af --- /dev/null +++ b/tgbot/utils/misc/bot_filters.py @@ -0,0 +1,14 @@ +# - *- coding: utf- 8 - *- +from aiogram.dispatcher.filters import BaseFilter +from aiogram.types import Message + +from tgbot.config import get_admins + + +# Проверка на админа +class IsAdmin(BaseFilter): + async def __call__(self, message: Message): + if message.from_user.id in get_admins(): + return True + else: + return False diff --git a/tgbot/utils/misc/bot_logging.py b/tgbot/utils/misc/bot_logging.py new file mode 100644 index 0000000..ff27b7e --- /dev/null +++ b/tgbot/utils/misc/bot_logging.py @@ -0,0 +1,32 @@ +# - *- coding: utf- 8 - *- +import logging as bot_logger + +import colorlog + +from tgbot.config import PATH_LOGS + +# Формат логгирования +log_formatter_file = bot_logger.Formatter("%(levelname)s | %(asctime)s | %(filename)s:%(lineno)d | %(message)s") +log_formatter_console = colorlog.ColoredFormatter( + "%(purple)s%(levelname)s %(blue)s|%(purple)s %(asctime)s %(blue)s|%(purple)s %(filename)s:%(lineno)d %(blue)s|%(purple)s %(message)s%(red)s", + datefmt="%d-%m-%Y %H:%M:%S", +) + +# Логгирование в файл logs.log +file_handler = bot_logger.FileHandler(PATH_LOGS, "w", "utf-8") +file_handler.setFormatter(log_formatter_file) +file_handler.setLevel(bot_logger.INFO) + +# Логгирование в консоль +console_handler = bot_logger.StreamHandler() +console_handler.setFormatter(log_formatter_console) +console_handler.setLevel(bot_logger.CRITICAL) + +# Подключение настроек логгирования +bot_logger.basicConfig( + format="%(levelname)s | %(asctime)s | %(filename)s:%(lineno)d | %(message)s", + handlers=[ + file_handler, + console_handler + ] +) diff --git a/tgbot/utils/misc/bot_models.py b/tgbot/utils/misc/bot_models.py new file mode 100644 index 0000000..235705d --- /dev/null +++ b/tgbot/utils/misc/bot_models.py @@ -0,0 +1,10 @@ +# - *- coding: utf- 8 - *- +from textwrap import dedent + +from aiogram.dispatcher.fsm.context import FSMContext + +from tgbot.services.api_session import RequestsSession + +ded = dedent +FSM = FSMContext +RS = RequestsSession diff --git a/tgbot/utils/misc_functions.py b/tgbot/utils/misc_functions.py new file mode 100644 index 0000000..15c70f2 --- /dev/null +++ b/tgbot/utils/misc_functions.py @@ -0,0 +1,24 @@ +# - *- coding: utf- 8 - *- +from aiogram import Bot +from aiogram.types import FSInputFile + +from tgbot.config import get_admins, PATH_DATABASE +from tgbot.utils.const_functions import get_date, send_admins + + +# Выполнение функции после запуска бота (рассылка админам о запуске бота) +async def startup_notify(bot: Bot): + if len(get_admins()) >= 1: + await send_admins(bot, "✅ Бот был запущен") + + +# Автоматические бэкапы БД +async def autobackup(bot: Bot): + for admin in get_admins(): + try: + await bot.send_document(admin, + FSInputFile(PATH_DATABASE), + caption=f"📦 AUTOBACKUP\n" + f"🕰 {get_date()}") + except: + pass