diff --git a/.env.dist b/.env.dist new file mode 100644 index 0000000..22c3f52 --- /dev/null +++ b/.env.dist @@ -0,0 +1,10 @@ +BOT_CONTAINER_NAME=bot_container_name +BOT_IMAGE_NAME=botimage_name +BOT_NAME=mybotname +BOT_TOKEN=123456:Your-TokEn_ExaMple +ADMINS=123456,654321 +USE_REDIS=False + +DB_NAME=Granted.db + +GRANT_NUMBER=500 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9fe4e99 --- /dev/null +++ b/.gitignore @@ -0,0 +1,66 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Translations +*.mo +*.pot + + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Environments +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Pyre type checker +.pyre/ +.idea/* +.env + +# Database +*.db \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a92a6aa --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ +FROM python:3.9-buster +ENV BOT_NAME=$BOT_NAME + +WORKDIR /usr/src/app/"${BOT_NAME:-tg_bot}" + +COPY requirements.txt /usr/src/app/"${BOT_NAME:-tg_bot}" +RUN pip install -r /usr/src/app/"${BOT_NAME:-tg_bot}"/requirements.txt +COPY . /usr/src/app/"${BOT_NAME:-tg_bot}" diff --git a/README.md b/README.md index 1164b0e..2544183 100644 --- a/README.md +++ b/README.md @@ -1 +1,85 @@ -# tg-bot-users 🤖 +# Anniversary Users Bot 🤖 + +Бот для автоматического отслеживания и поздравления юбилейных пользователей. При +вступлении в группу юбилейного участника бот присылает в группу +администраторов уведомление с именем, ником, id пользователя, юбилейным номером и датой и временем вступления, +а также кнопкой "Поздравить" и "Отменить" для автоматического поздравления юбилейного участника или отмены, в случае +вступления в группу бота/модератора. В файле .env в переменной GRANT_NUMBER (пример в .env.dist), необходимо установить +число, кратно которому будут определятся пользователи (при 500 - 500, 1000 и т.д.), это глобальная настройка для всех +групп, если необходимо для какой-то группы выставить индивидуальные значения, то необходимо через приватный канал бота, +для этой группы настроить соответствующую таблицу. Для доступа к конфигурированию бота через приватный канал, в файле +.env (пример в .env.dist) необходимо прописать в переменной ADMINS, прописать ID пользователей Телеграмм, которые будут +иметь соответствующие права. + +## Особенности + +* Бот, в автоматическом режиме проверяет вступившего пользователя на "юбилейные" места в группе, и посылает оповещения в + группу администраторов. +* Все новые вступившие пользователи проверяются по базе данных, что бы исключить повторное поздравление. +* Управление ботов осуществляется в группе администраторов. +* Возможность "тонкой" настройки юбилейных мест. К примеру, каждый: 50, 100, 200 ... и так далее. +* Сохранение данных о двух последующих пользователях. +* Возможность настройки групп администраторов. +* База данных на всех поздравленных пользователей +* Автоматический перезапуск бота в случае возникновения ошибки (Docker) + +## Доступные команды + +### В группе модераторов + +* /проверка - команда проверяет, есть ли в очереди на поздравление пользователи из модерируемых групп. +* /восстановить - команда предназначена для восстановления кнопок для поздравления, на случай если уведомление с + кнопками поздравлениями удалили, или нажали кнопку отмены. +* /списокЮбилейный - выводит историю о юбилейных пользователях + +### В приватном чате бота(админка) + +Настройка бота производится в приватном чате, команды доступные в меню: + +*/start - Позволяет проверить запушен ли бот и есть ли у пользователя права на конфигурирование +*/configure - Вызывает главное меню + +#### Главное меню: + +В главном меню есть доступ для перехода к конфигурированию 2 таблиц: + +1. Настройка таблицы групп: + +* Показать таблицу - выводит таблицу с данными из БД +* Добавить строку - позволяет добавить новую в БД, на одну группу модераторов может быть добавлено несколько групп + пользователей, при этом, если ввести id группы модераторов, запись с которой уже существует, то бот просто добавит + к старой записи новые группы пользователей, при этом бот уберет дубликаты id пользовательских групп, если их введут + повторно. +* Удалить строку - удаляет строку из БД по ID записи в БД +* Главное меню - возвращает в главное меню + +2. Настройка таблицы с юбилейными номерами: + +* Показать таблицу - выводит таблицу с данными из БД +* Добавить строку - позволяет добавить новую в БД, на одну группу пользователей может быть добавлено несколько номеров, + при этом, если ввести уже id группы пользователей, запись с которой уже существует, то бот просто добавит + к старой записи, новые номера, при этом бот уберет дубликаты номеров а также отсортиует их по возрастанию. +* Удалить строку - удаляет строку из БД по ID записи в БД +* Главное меню - возвращает в главное меню + +## Requirements + +* aiogram 2.21 +* aioredis 2.0 +* environs 9.0 +* aiosqlite 0.17 +* python-dotenv 0.20 + +## Подготовка и запуск + +1) Создайте своего бота с помощью бота @BotFather и сохраните свой токен. +2) Необходимо в @BotFather отключить privacy mode. +3) Добавьте бота во все чаты, в которых необходимо отслеживать пользователей(боту необходимо выдать права + администратора) +4) Создать файл .env в директории программы и заполнить его согласно .env.dist +5) Вы можете запустить бота локально, установив все зависимости командой: + pip install -r requirements.txt и запустив bot.py +6) Вы так же можете запустить бота из Docker контейнера, для этого перейдите в терминале, в папку с ботом и выполните + команду: docker-compose up +7) После чего можете переходить к настройкам админки, для того чтобы бот начал работать в группах в которые он был + добавлен, необходимо внести соответствие ID этих групп в таблицу, через приватный канал бота. \ No newline at end of file diff --git a/bot.py b/bot.py new file mode 100644 index 0000000..78ad208 --- /dev/null +++ b/bot.py @@ -0,0 +1,130 @@ +import asyncio +import logging + +from aiogram import Bot, Dispatcher +from aiogram.contrib.fsm_storage.memory import MemoryStorage +from aiogram.contrib.fsm_storage.redis import RedisStorage2 +from aiogram.contrib.middlewares.logging import LoggingMiddleware +from aiogram.types import AllowedUpdates + +from tgbot.handlers.admin.add_groups_callback import register_add_groups +from tgbot.handlers.admin.add_numbers_callback import register_add_numbers +from tgbot.handlers.admin.cancel import register_cancel_menu +from tgbot.handlers.admin.configure_groups_callback import register_configure_groups +from tgbot.handlers.admin.configure_numbers_callback import register_configure_numbers +from tgbot.handlers.admin.delete_from_groups import register_delete_from_groups +from tgbot.handlers.admin.delete_from_numbers import register_delete_numbers +from tgbot.handlers.admin.delete_groups_callback import register_delete_groups_cb +from tgbot.handlers.admin.delete_numbers_callback import register_delete_numbers_cb +from tgbot.handlers.admin.get_mod_group import register_get_mod_group +from tgbot.handlers.admin.get_numbers import register_get_grant_numbers +from tgbot.handlers.admin.get_numbers_group import register_numbers_group +from tgbot.handlers.admin.get_users_groups import register_get_users_group +from tgbot.handlers.admin.main_menu import register_main_menu +from tgbot.handlers.admin.back_to_main_menu import register_back_to_main +from tgbot.handlers.admin.show_groups_callback import register_show_groups +from tgbot.handlers.admin.show_numbers_callback import register_show_numbers +from tgbot.handlers.admin.user import register_user +from tgbot.handlers.admin.admin import register_admin + +from tgbot.handlers.groups.check import register_check_queue +from tgbot.handlers.groups.get_granted import register_get_granted +from tgbot.handlers.groups.restore import register_restore +from tgbot.handlers.groups.grant_cancel_callback import register_cancel_grant +from tgbot.handlers.groups.grant_callback import register_grant +from tgbot.handlers.groups.catch_update import register_catch +from tgbot.handlers.groups.show_granted_callback import register_show_granted_cb + +from tgbot.misc.set_commands import set_default_commands + +from tgbot.filters.moder_group import IsModerGroup +from tgbot.filters.granted import IsNotGranted +from tgbot.filters.count import IsGrantCount +from tgbot.filters.user_group import IsUserGroup +from tgbot.Utils.DBWorker import create_tables +from tgbot.config import load_config +from tgbot.filters.admin import AdminFilter +from tgbot.filters.group_join import IsGroupJoin + +logger = logging.getLogger(__name__) + + +def register_all_middlewares(dp): + dp.setup_middleware(LoggingMiddleware()) + + +def register_all_filters(dp): + dp.filters_factory.bind(AdminFilter) + dp.filters_factory.bind(IsGroupJoin) + dp.filters_factory.bind(IsUserGroup) + dp.filters_factory.bind(IsModerGroup) + dp.filters_factory.bind(IsGrantCount) + dp.filters_factory.bind(IsNotGranted) + + +def register_all_handlers(dp): + register_admin(dp) + register_user(dp) + register_catch(dp) + register_grant(dp) + register_cancel_grant(dp) + register_check_queue(dp) + register_restore(dp) + register_get_granted(dp) + register_configure_groups(dp) + register_show_groups(dp) + register_add_groups(dp) + register_get_mod_group(dp) + register_get_users_group(dp) + register_delete_groups_cb(dp) + register_delete_from_groups(dp) + register_main_menu(dp) + register_back_to_main(dp) + register_cancel_menu(dp) + register_configure_numbers(dp) + register_show_numbers(dp) + register_add_numbers(dp) + register_numbers_group(dp) + register_get_grant_numbers(dp) + register_delete_numbers_cb(dp) + register_delete_numbers(dp) + register_show_granted_cb(dp) + + +async def main(): + logging.basicConfig( + level=logging.INFO, + format=u'%(filename)s:%(lineno)d #%(levelname)-8s [%(asctime)s] - %(name)s - %(message)s', + ) + logger.info("Starting bot") + config = load_config(".env") + + storage = RedisStorage2() if config.tg_bot.use_redis else MemoryStorage() + bot = Bot(token=config.tg_bot.token, parse_mode='HTML') + dp = Dispatcher(bot, storage=storage) + + bot['config'] = config + register_all_middlewares(dp) + register_all_filters(dp) + register_all_handlers(dp) + await set_default_commands(dp) + await create_tables() + + # start + try: + await bot.delete_webhook(drop_pending_updates=True) + await dp.start_polling( + allowed_updates=AllowedUpdates.MESSAGE + AllowedUpdates.CHAT_MEMBER + AllowedUpdates.CALLBACK_QUERY) + finally: + await dp.storage.close() + await dp.storage.wait_closed() + await bot.session.close() + + +if __name__ == '__main__': + try: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + loop.run_until_complete(main()) + except (KeyboardInterrupt, SystemExit): + logger.error("Bot stopped!") diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..040a4b9 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,23 @@ +version: '3.3' + +services: + bot: + image: "${BOT_IMAGE_NAME:-tg_bot-image}" + container_name: "${BOT_CONTAINER_NAME:-tg_bot-container}" + stop_signal: SIGINT + build: + context: . + working_dir: "/usr/src/app/${BOT_NAME:-tg_bot}" + volumes: + - .:/usr/src/app/${BOT_NAME:-tg_bot} + command: python3 -m bot + restart: always + env_file: + - ".env" + networks: + - tg_bot + + +networks: + tg_bot: + driver: bridge diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a02d288 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +aiogram~=2.21 +aioredis~=2.0 +environs~=9.0 +aiosqlite~=0.17 +python-dotenv~=0.20 \ No newline at end of file diff --git a/systemd/tgbot.service b/systemd/tgbot.service new file mode 100644 index 0000000..a3fffa6 --- /dev/null +++ b/systemd/tgbot.service @@ -0,0 +1,14 @@ +[Unit] +Description=Сongratulate Bot +After=network.target + +[Service] +User=tgbot +Group=tgbot +Type=simple +WorkingDirectory=/opt/tgbot +ExecStart=/opt/tgbot/venv/bin/python bot.py +Restart=always + +[Install] +WantedBy=multi-user.target diff --git a/tgbot/Utils/DBWorker.py b/tgbot/Utils/DBWorker.py new file mode 100644 index 0000000..2140bbd --- /dev/null +++ b/tgbot/Utils/DBWorker.py @@ -0,0 +1,301 @@ +import os +from typing import List, Union + +import aiosqlite + + +async def db_execute(string: str, values: Union[List[tuple], tuple] = None, multiple: bool = False, + get: bool = False) -> Union[List[tuple], int]: + """Функция для выполнения SQL запросов""" + db_name = os.getenv('DB_NAME') + async with aiosqlite.connect(db_name) as db: + if multiple: + async with db.executemany(string, values) as cursor: + await db.commit() + return cursor.rowcount + elif get: + async with db.execute(string, values) as cursor: + return await cursor.fetchall() + elif values: + async with db.execute(string, values) as cursor: + await db.commit() + return cursor.rowcount + else: + async with db.execute(string) as cursor: + await db.commit() + return cursor.rowcount + + +async def create_tables() -> None: + """Функция для создания БД и создания таблиц""" + + string = """CREATE TABLE IF NOT EXISTS granted( + id INTEGER PRIMARY KEY autoincrement, + group_id_users INTEGER, + name_group TEXT, + user_id INTEGER, + user TEXT, + group_id_mod INTEGER, + moder_id INTEGER, + count, + datetime_update TEXT, + datetime_granted TEXT, + username TEXT); + """ + await db_execute(string) + + string = """CREATE TABLE IF NOT EXISTS groups( + id INTEGER PRIMARY KEY autoincrement, + mod_group_id INTEGER UNIQUE, + user_group_ids TEXT); + """ + await db_execute(string) + + string = """CREATE TABLE IF NOT EXISTS queue( + id INTEGER PRIMARY KEY autoincrement, + message_id INTEGER, + group_id_users INTEGER, + name_group TEXT, + group_id_mod INTEGER, + user_id INTEGER, + user TEXT, + count INTEGER, + datetime_update TEXT, + UUID TEXT, + username TEXT); + """ + await db_execute(string) + + string = """CREATE TABLE IF NOT EXISTS grant_numbers( + id INTEGER PRIMARY KEY autoincrement, + group_id INTEGER UNIQUE, + numbers TEXT); + """ + await db_execute(string) + + +async def get_users_groups(group_id: int) -> List[tuple]: + """ + Функция для получения ids модерируемых групп из таблицы groups + :param group_id: int + :return: List[tuple] + """ + return await db_execute(string='SELECT user_group_ids FROM groups WHERE mod_group_id=?', values=(group_id,), + get=True) + + +async def get_moder_groups(group_id: int) -> List[tuple]: + """ + Функция для получения ids групп модераторов из таблицы groups + :param group_id: int + :return: List[tuple] + """ + return await db_execute(string='SELECT mod_group_id FROM groups WHERE user_group_ids LIKE ?', + values=('%' + str(group_id) + '%',), + get=True) + + +async def delete_data_from_groups(ids: List[tuple]) -> int: + """ + Функция для удаления записей из таблицы groups + :param ids: List[tuple] + :return: int + """ + return await db_execute(string='DELETE FROM groups WHERE id=?', values=ids, multiple=True) + + +async def get_groups() -> List[tuple]: + """ + Функция для получения всех данных из таблицы groups + :return: List[tuple] + """ + return await db_execute(string='SELECT * FROM groups', get=True) + + +async def set_data_groups(values: tuple) -> int: + """ + Функция для вставки новой записи в таблицу groups, или замены + :param values: + :return: int + """ + return await db_execute(string='INSERT OR REPLACE INTO groups(mod_group_id, user_group_ids)' + ' VALUES(?, ?);', values=values) + + +async def set_data_queue(values: tuple) -> int: + """ + Функция для вставки новой записи в таблицу queue + :param values: tuple + :return: int + """ + return await db_execute(string= + "INSERT INTO queue(message_id, group_id_users, name_group, group_id_mod, user_id, user, " + "count, datetime_update, UUID, username) " + "VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?);", values=values) + + +async def update_data_queue(message_id: int, old_message_id: int, group_id: int) -> int: + """ + Функция для обновления записи в таблице queue + :param message_id: int + :param old_message_id: int + :param group_id: int + :return: int + """ + return await db_execute( + string='UPDATE queue SET message_id=? WHERE message_id=? AND ' + 'group_id_users LIKE ?', values=(message_id, old_message_id, '%' + str(group_id) + '%',)) + + +async def set_data_granted(values: List[tuple]) -> int: + """ + Функция для вставки новых записей в таблицу granted + :param values: List[tuple] + :return: int + """ + return await db_execute(string= + "INSERT INTO granted(group_id_users, name_group, user_id, user, group_id_mod, moder_id, " + "count, datetime_update, datetime_granted, username) " + "VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?);", + values=values, multiple=True) + + +async def get_data_granted(group_id_moder: int) -> List[tuple]: + """ + Функция для получения данных из таблицы granted с отбором по полю group_id_mod + :param group_id_moder: int + :return: List[tuple] + """ + return await db_execute(string='SELECT * FROM granted WHERE group_id_mod=? ORDER BY datetime_update', + values=(group_id_moder,), + get=True) + + +async def get_data_granted_for_kb(group_id_users: int) -> List[tuple]: + """ + Функция для получения данных из таблицы granted отсортированных по datetime_update и отборо по group_id_users + :param group_id_users: int + :return: List[tuple] + """ + return await db_execute( + string='SELECT * FROM granted WHERE group_id_users=? ORDER BY datetime_update', values=(group_id_users,), + get=True) + + +async def check_granted(user_id: int, group_id: int) -> List[tuple]: + """ + Функция для получения данных из таблицы granted c отбором по group_id_users и user_id + :param user_id: int + :param group_id: int + :return: List[tuple] + """ + return await db_execute( + string="SELECT COUNT(*) FROM granted WHERE user_id=? AND group_id_users=?", values=(user_id, group_id,), + get=True) + + +async def get_count_queue(group_id: int) -> List[tuple]: + """ + Функция для получения данных из таблицы queue c отбором по group_id_users + :param group_id: int + :return: List[tuple] + """ + return await db_execute(string="SELECT COUNT(*) FROM queue WHERE group_id_users=?", values=(group_id,), + get=True) + + +async def check_queue(user_id: int, group_id: int) -> List[tuple]: + """ + Функция для получения кол-ва существующих записей с отбором по user_id и group_id_users + :param user_id: int + :param group_id: int + :return: List[tuple] + """ + return await db_execute( + string="SELECT COUNT(*) FROM queue WHERE user_id=? AND group_id_users=?", values=(user_id, group_id,), + get=True) + + +async def get_message_in_queue(uid: str) -> List[tuple]: + """ + Функция для получения все данных из таблицы queue с отбором по UUID + :param uid: str + :return: List[tuple] + """ + return await db_execute(string="SELECT * FROM queue WHERE UUID=?", values=(uid,), get=True) + + +async def get_queue(group_id: int, message_id: int = 0) -> List[tuple]: + """ + Функция для получения всех данных из таблицы queue с отбором по group_id_users и message_id + :param group_id: int + :param message_id: int + :return: List[tuple] + """ + return await db_execute( + string="SELECT * FROM queue WHERE group_id_users=? AND message_id!=?", values=(group_id, message_id,), + get=True) + + +async def delete_from_queue(group_id: int) -> List[tuple]: + """ + Функция для получения все данных из таблицы queue с отбором по group_id_users + :param group_id: int + :return: List[tuple] + """ + return await db_execute(string="DELETE FROM queue WHERE group_id_users=?", values=(group_id,)) + + +async def count_from_queue(group_id: int) -> List[tuple]: + """ + Функция для проверки существования записи с отбором по group_id_users + :param group_id: int + :return: List[tuple] + """ + return await db_execute( + string="SELECT count FROM queue WHERE group_id_users=? ORDER BY datetime_update limit 1", values=(group_id,), + get=True) + + +async def get_data_from_grant_numbers(group_id: int) -> List[tuple]: + """ + Функция для получения поля numbers из таблицы grant_numbers с отбором по group_id + :param group_id: int + :return: List[tuple] + """ + return await db_execute(string='SELECT numbers FROM grant_numbers WHERE group_id=?', values=(group_id,), get=True) + + +async def set_data_numbers(values: tuple) -> int: + """ + Функция для вставки новой записи в grant_numbers, или замены существующей + :param values: tuple + :return: int + """ + return await db_execute(string='INSERT OR REPLACE INTO grant_numbers(group_id, numbers)' + ' VALUES(?, ?);', values=values) + + +async def delete_data_from_grant_numbers(ids: List[tuple]) -> int: + """ + Функция для удаления записей из grant_numbers с отбором по id + :param ids: List[tuple] + :return: int + """ + return await db_execute(string='DELETE FROM grant_numbers WHERE id=?', values=ids, multiple=True) + + +async def get_numbers() -> List[tuple]: + """ + Функция для получения всех данных из grant_numbers + :return: List[tuple] + """ + return await db_execute(string='SELECT * FROM grant_numbers', get=True) + + +async def vacuum() -> int: + """ + Пылесос:) + :return: int + """ + return await db_execute(string="VACUUM") diff --git a/tgbot/Utils/__init__.py b/tgbot/Utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tgbot/Utils/check_ids_records.py b/tgbot/Utils/check_ids_records.py new file mode 100644 index 0000000..9c265e9 --- /dev/null +++ b/tgbot/Utils/check_ids_records.py @@ -0,0 +1,13 @@ +from tgbot.Utils.check_number import check_number_in_message + + +async def check_ids(message: str) -> bool: + """ + Функция для проверки ids записей на корректность + :param message: str + :return: bool + """ + users_groups_ids = message.split(',') + for number in users_groups_ids: + if await check_number_in_message(number=number): + return True diff --git a/tgbot/Utils/check_message_user_groups.py b/tgbot/Utils/check_message_user_groups.py new file mode 100644 index 0000000..04cb566 --- /dev/null +++ b/tgbot/Utils/check_message_user_groups.py @@ -0,0 +1,14 @@ +from tgbot.Utils.check_number import check_number_in_message + + +async def check_users_groups(message: str) -> bool: + """ + Функция для проверки ids групп на корректность + :param message: str + :return: bool + """ + users_groups_ids = message.split(',') + for number in users_groups_ids: + if await check_number_in_message(number=number) and len(number) > 4: + return True + diff --git a/tgbot/Utils/check_number.py b/tgbot/Utils/check_number.py new file mode 100644 index 0000000..7e92e0f --- /dev/null +++ b/tgbot/Utils/check_number.py @@ -0,0 +1,11 @@ +async def check_number_in_message(number: str) -> int: + """ + Функция для проверки ids групп на целое число + :param number: str + :return: int + """ + if number[:1] == '-': + if number[1:].isdigit(): + return int(number) + elif number.isdigit(): + return int(number) diff --git a/tgbot/Utils/delete_doubles.py b/tgbot/Utils/delete_doubles.py new file mode 100644 index 0000000..5af2874 --- /dev/null +++ b/tgbot/Utils/delete_doubles.py @@ -0,0 +1,13 @@ +async def delete_doubles_ids(message: str, sort: bool = False) -> str: + """ + Функция для удаления дублей из строки с ids групп и сортировки если необходимо + :param message: str + :param sort: + :return: str + """ + without_doubles = set(message.replace(' ', '').split(',')) + if sort: + for_sorts = map(int, without_doubles) + return ','.join(map(str, sorted(for_sorts))) + else: + return ','.join(without_doubles) diff --git a/tgbot/Utils/get_ids_for_grant_numbers.py b/tgbot/Utils/get_ids_for_grant_numbers.py new file mode 100644 index 0000000..0387b57 --- /dev/null +++ b/tgbot/Utils/get_ids_for_grant_numbers.py @@ -0,0 +1,10 @@ +from typing import List + + +async def get_ids_for_multiple_record(ids: str) -> List[tuple]: + """ + Функция формирует список с кортежами с ids групп для работы с БД + :param ids: str + :return: List[tuple] + """ + return [(group_id,) for group_id in ids.split(',')] diff --git a/tgbot/Utils/get_numbers.py b/tgbot/Utils/get_numbers.py new file mode 100644 index 0000000..9e4f9c5 --- /dev/null +++ b/tgbot/Utils/get_numbers.py @@ -0,0 +1,15 @@ +from typing import List + +from tgbot.Utils.DBWorker import get_data_from_grant_numbers + + +async def get_grant_numbers(group_id: int) -> List[int]: + """ + Возвращает данные о юбилейных пользователях для фильтра + :param group_id: int + :return: List[int] + """ + numbers = await get_data_from_grant_numbers(group_id=group_id) + if not numbers[0][0]: + return [] + return [int(number) for number in numbers[0][0].split(',')] diff --git a/tgbot/Utils/get_user_link.py b/tgbot/Utils/get_user_link.py new file mode 100644 index 0000000..d5a8235 --- /dev/null +++ b/tgbot/Utils/get_user_link.py @@ -0,0 +1,22 @@ +from aiogram import md +from aiogram import types +from aiogram.utils.markdown import quote_html + + +async def get_link(user: types.User) -> str: + """ + Возвращает ссылку с пользователем для вывода сообщения + :param user: types.User + :return: str + """ + if not user.url: + return 'пользователь' + if user.first_name: + username = user.first_name + if user.last_name: + username += f' {user.last_name}' + elif user.username: + username = quote_html(user.username) + else: + username = 'пользователь' + return md.hlink(username, user.url) diff --git a/tgbot/__init__.py b/tgbot/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tgbot/config.py b/tgbot/config.py new file mode 100644 index 0000000..8bfe029 --- /dev/null +++ b/tgbot/config.py @@ -0,0 +1,47 @@ +from dataclasses import dataclass + +from environs import Env + + +@dataclass +class DbConfig: + database: str + + +@dataclass +class TgBot: + token: str + admin_ids: list[int] + use_redis: bool + + +@dataclass +class Miscellaneous: + grant_count: int + other_params: str = None + + +@dataclass +class Config: + tg_bot: TgBot + db: DbConfig + misc: Miscellaneous + + +def load_config(path: str = None): + env = Env() + env.read_env(path) + + return Config( + tg_bot=TgBot( + token=env.str("BOT_TOKEN"), + admin_ids=list(map(int, env.list("ADMINS"))), + use_redis=env.bool("USE_REDIS"), + ), + db=DbConfig( + database=env.str('DB_NAME') + ), + misc=Miscellaneous( + grant_count=env.int('GRANT_NUMBER') + ) + ) diff --git a/tgbot/filters/__init__.py b/tgbot/filters/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tgbot/filters/admin.py b/tgbot/filters/admin.py new file mode 100644 index 0000000..0c3dc9c --- /dev/null +++ b/tgbot/filters/admin.py @@ -0,0 +1,22 @@ +import typing + +from aiogram.dispatcher.filters import BoundFilter + +from tgbot.config import Config + + +class AdminFilter(BoundFilter): + """ + Класс фильтра, проверяет доступ к админке бота + """ + key = 'is_admin' + + def __init__(self, is_admin: typing.Optional[bool] = None): + self.is_admin = is_admin + + async def check(self, obj): + if self.is_admin is None: + return False + config: Config = obj.bot.get('config') + return (obj.from_user.id in config.tg_bot.admin_ids) == self.is_admin + diff --git a/tgbot/filters/count.py b/tgbot/filters/count.py new file mode 100644 index 0000000..c9a0023 --- /dev/null +++ b/tgbot/filters/count.py @@ -0,0 +1,28 @@ +from aiogram import types +from aiogram.dispatcher.filters import BoundFilter +from aiogram.dispatcher.handler import CancelHandler + +from tgbot.Utils.get_numbers import get_grant_numbers +from tgbot.Utils.DBWorker import get_count_queue + + +class IsGrantCount(BoundFilter): + key = "is_grant_count" + + def __init__(self, is_grant_count: bool): + self.is_grant_count = is_grant_count + + async def check(self, update: types.ChatMemberUpdated) -> dict: + """ + Метод фильтра, проверяет попадание нового пользователя в счастливчики, возвращает текущий номер вступившего + :param update: types.ChatMemberUpdated + :return: dict + """ + config_count = update.bot.data['config'].misc.grant_count + if not config_count: + raise CancelHandler() + count = await update.chat.get_member_count() + count_to_delete = await get_count_queue(update.chat.id) + saved_grant_numbers = await get_grant_numbers(update.chat.id) + if 0 < count_to_delete[0][0] < 3 or (count in saved_grant_numbers) or not count % config_count: + return {"count": count} diff --git a/tgbot/filters/granted.py b/tgbot/filters/granted.py new file mode 100644 index 0000000..ba1a874 --- /dev/null +++ b/tgbot/filters/granted.py @@ -0,0 +1,24 @@ +from aiogram import types +from aiogram.dispatcher.filters import BoundFilter + +from tgbot.Utils.DBWorker import check_granted, check_queue + + +class IsNotGranted(BoundFilter): + key = "is_not_granted" + + def __init__(self, is_not_granted: bool): + self.is_not_granted = is_not_granted + + async def check(self, update: types.ChatMemberUpdated) -> bool: + """ + Проверяет наличие нового пользователя в таблицах награжденных или в очереди + :param update: types.ChatMemberUpdated + :return: bool + """ + granted = await check_granted(user_id=update.new_chat_member.user.id, group_id=update.chat.id) + if granted[0][0]: + return False + in_queue = await check_queue(user_id=update.new_chat_member.user.id, group_id=update.chat.id) + if not in_queue[0][0]: + return True diff --git a/tgbot/filters/group_join.py b/tgbot/filters/group_join.py new file mode 100644 index 0000000..67e6993 --- /dev/null +++ b/tgbot/filters/group_join.py @@ -0,0 +1,17 @@ +from aiogram import types +from aiogram.dispatcher.filters import BoundFilter + + +class IsGroupJoin(BoundFilter): + key = "is_group_join" + + def __init__(self, is_group_join: bool): + self.is_group_join = is_group_join + + async def check(self, update: types.ChatMemberUpdated) -> bool: + """ + Проверяет Update на событие присоединение нового пользователя + :param update: + :return: bool + """ + return update.new_chat_member.is_chat_member() diff --git a/tgbot/filters/moder_group.py b/tgbot/filters/moder_group.py new file mode 100644 index 0000000..f5410df --- /dev/null +++ b/tgbot/filters/moder_group.py @@ -0,0 +1,21 @@ +from aiogram import types +from aiogram.dispatcher.filters import BoundFilter + +from tgbot.Utils.DBWorker import get_users_groups + + +class IsModerGroup(BoundFilter): + key = "is_moder_group" + + def __init__(self, is_moder_group: bool): + self.is_moder_group = is_moder_group + + async def check(self, message: types.Message) -> dict: + """ + Фильтр для проверки группы в списке модераторских и получения ids связанных модерируемых групп + :param message: types.Message + :return: dict + """ + ids = await get_users_groups(message.chat.id) + if ids: + return {"ids": ids} diff --git a/tgbot/filters/user_group.py b/tgbot/filters/user_group.py new file mode 100644 index 0000000..67f7b00 --- /dev/null +++ b/tgbot/filters/user_group.py @@ -0,0 +1,22 @@ +from aiogram import types +from aiogram.dispatcher.filters import BoundFilter + +from tgbot.Utils.DBWorker import get_moder_groups + + +class IsUserGroup(BoundFilter): + + key = "is_user_group" + + def __init__(self, is_user_group: bool): + self.is_user_group = is_user_group + + async def check(self, update: types.ChatMemberUpdated) -> dict: + """ + Фильтр для проверки наличия назначенных групп модераторов и получения их id + :param update: types.ChatMemberUpdated + :return: dict + """ + ids = await get_moder_groups(update.chat.id) + if ids: + return {"ids": ids} diff --git a/tgbot/handlers/admin/__init__.py b/tgbot/handlers/admin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tgbot/handlers/admin/add_groups_callback.py b/tgbot/handlers/admin/add_groups_callback.py new file mode 100644 index 0000000..55fd95c --- /dev/null +++ b/tgbot/handlers/admin/add_groups_callback.py @@ -0,0 +1,27 @@ +from contextlib import suppress +from aiogram import Dispatcher, types +from aiogram.types import ChatType +from aiogram.utils.exceptions import MessageCantBeDeleted +from tgbot.misc.states import Configure + + +async def add_groups(call: types.CallbackQuery) -> None: + """ + Функция коллбека для добавления записи в таблицу groups + :param call: types.CallbackQuery + :return: None + """ + with suppress(MessageCantBeDeleted): + await call.bot.delete_message(message_id=call.message.message_id, chat_id=call.message.chat.id) + + await call.message.answer(text='Введите id группы модераторов, целое число (/reset для сброса)') + await Configure.AddModGroups.set() + + +def register_add_groups(dp: Dispatcher): + chat_types = [ChatType.PRIVATE] + dp.register_callback_query_handler(add_groups, + chat_type=chat_types, + text='add_groups', + state="*", + is_admin=True) diff --git a/tgbot/handlers/admin/add_numbers_callback.py b/tgbot/handlers/admin/add_numbers_callback.py new file mode 100644 index 0000000..4409315 --- /dev/null +++ b/tgbot/handlers/admin/add_numbers_callback.py @@ -0,0 +1,26 @@ +from contextlib import suppress +from aiogram import Dispatcher, types +from aiogram.types import ChatType +from aiogram.utils.exceptions import MessageCantBeDeleted +from tgbot.misc.states import Configure + + +async def add_numbers(call: types.CallbackQuery) -> None: + """ + Функция коллбека для добавления записи в таблицу numbers + :param call: types.CallbackQuery + :return: None + """ + with suppress(MessageCantBeDeleted): + await call.message.delete() + + await call.message.answer(text='Введите id группы, целое число (/reset для сброса)') + await Configure.AddNumbersGroup.set() + + +def register_add_numbers(dp: Dispatcher): + chat_types = [ChatType.PRIVATE] + dp.register_callback_query_handler(add_numbers, + chat_type=chat_types, + text='add_numbers', + is_admin=True) diff --git a/tgbot/handlers/admin/admin.py b/tgbot/handlers/admin/admin.py new file mode 100644 index 0000000..66d452e --- /dev/null +++ b/tgbot/handlers/admin/admin.py @@ -0,0 +1,15 @@ +from aiogram import Dispatcher +from aiogram.types import Message + + +async def admin_start(message: Message) -> None: + """ + Если админ отправляет команду /start, бот здоровается и предлагает воспользоваться меню + :param message: Message + :return: None + """ + await message.reply("Добрый день! Конфигурация доступна из меню.") + + +def register_admin(dp: Dispatcher): + dp.register_message_handler(admin_start, commands=["start"], state="*", is_admin=True) diff --git a/tgbot/handlers/admin/back_to_main_menu.py b/tgbot/handlers/admin/back_to_main_menu.py new file mode 100644 index 0000000..0ea2ad3 --- /dev/null +++ b/tgbot/handlers/admin/back_to_main_menu.py @@ -0,0 +1,27 @@ +from contextlib import suppress +from aiogram import Dispatcher, types +from aiogram.dispatcher import FSMContext +from aiogram.types import ChatType +from aiogram.utils.exceptions import MessageCantBeEdited +from tgbot.keyboards.inline import get_main_menu_kb + + +async def back_to_main(call: types.CallbackQuery, state: FSMContext) -> None: + """ + Функция коллбэка для возрврата в главное меню и сброса состояний + :param call: types.CallbackQuery + :param state: FSMContext + :return: None + """ + with suppress(MessageCantBeEdited): + await call.message.edit_text(text='⚙ ГЛАВНОЕ МЕНЮ ⚙', reply_markup=get_main_menu_kb()) + await state.finish() + + +def register_back_to_main(dp: Dispatcher): + chat_types = [ChatType.PRIVATE] + dp.register_callback_query_handler(back_to_main, + chat_type=chat_types, + text='back_to_main', + state="*", + is_admin=True) diff --git a/tgbot/handlers/admin/cancel.py b/tgbot/handlers/admin/cancel.py new file mode 100644 index 0000000..ab50c59 --- /dev/null +++ b/tgbot/handlers/admin/cancel.py @@ -0,0 +1,22 @@ +from contextlib import suppress +from aiogram import Dispatcher, types +from aiogram.types import ChatType +from aiogram.utils.exceptions import MessageCantBeDeleted + + +async def cancel_menu(call: types.CallbackQuery) -> None: + """ + Функция колльека для закрытия главного меню + :param call: types.CallbackQuery + :return: None + """ + with suppress(MessageCantBeDeleted): + await call.message.delete() + + +def register_cancel_menu(dp: Dispatcher): + chat_types = [ChatType.PRIVATE] + dp.register_callback_query_handler(cancel_menu, + chat_type=chat_types, + text='cancel', + is_admin=True) diff --git a/tgbot/handlers/admin/configure_groups_callback.py b/tgbot/handlers/admin/configure_groups_callback.py new file mode 100644 index 0000000..946e3c3 --- /dev/null +++ b/tgbot/handlers/admin/configure_groups_callback.py @@ -0,0 +1,27 @@ +from contextlib import suppress + +from aiogram import Dispatcher, types +from aiogram.types import ChatType +from aiogram.utils.exceptions import MessageCantBeEdited + +from tgbot.keyboards.inline import get_conf_groups_kb + + +async def configure_groups(call: types.CallbackQuery) -> None: + """ + Функция коллбека для вызова меню конфигурирования таблицы соответствия групп + :param call: types.CallbackQuery + :return: None + """ + with suppress(MessageCantBeEdited): + await call.message.edit_text(text='⚙ Настройка таблицы соответствия групп ⚙', + reply_markup=get_conf_groups_kb()) + + +def register_configure_groups(dp: Dispatcher): + chat_types = [ChatType.PRIVATE] + dp.register_callback_query_handler(configure_groups, + chat_type=chat_types, + text='configure_groups', + is_admin=True + ) diff --git a/tgbot/handlers/admin/configure_numbers_callback.py b/tgbot/handlers/admin/configure_numbers_callback.py new file mode 100644 index 0000000..e8a2c62 --- /dev/null +++ b/tgbot/handlers/admin/configure_numbers_callback.py @@ -0,0 +1,27 @@ +from contextlib import suppress + +from aiogram import Dispatcher, types +from aiogram.types import ChatType +from aiogram.utils.exceptions import MessageCantBeEdited + +from tgbot.keyboards.inline import get_conf_numbers_kb + + +async def configure_numbers(call: types.CallbackQuery) -> None: + """ + Функция коллбека для вызова меню конфигурирования таблицы таблицы с поздр. номерами + :param call: types.CallbackQuery + :return: None + """ + with suppress(MessageCantBeEdited): + await call.message.edit_text(text='⚙ Настройка таблицы с поздр. номерами ⚙', + reply_markup=get_conf_numbers_kb()) + + +def register_configure_numbers(dp: Dispatcher): + chat_types = [ChatType.PRIVATE] + dp.register_callback_query_handler(configure_numbers, + chat_type=chat_types, + text='configure_numbers', + is_admin=True + ) diff --git a/tgbot/handlers/admin/delete_from_groups.py b/tgbot/handlers/admin/delete_from_groups.py new file mode 100644 index 0000000..cdb3f9c --- /dev/null +++ b/tgbot/handlers/admin/delete_from_groups.py @@ -0,0 +1,45 @@ +from aiogram import Dispatcher, types +from aiogram.dispatcher import FSMContext +from aiogram.types import ChatType + +from tgbot.Utils.DBWorker import delete_data_from_groups, vacuum +from tgbot.Utils.check_ids_records import check_ids +from tgbot.Utils.get_ids_for_grant_numbers import get_ids_for_multiple_record +from tgbot.keyboards.inline import get_conf_groups_kb + +from tgbot.misc.states import Configure + + +async def delete_from_groups(message: types.Message, state: FSMContext) -> None: + """ + Функция для удаления записей из таблицы групп + :param message: types.Message + :param state: FSMContext + :return: None + """ + if message.text == '/reset': + await state.finish() + return + record_id = await check_ids(message.text) + if not record_id: + await message.answer('Введите IDs строк для удаления записей из базы, целые числа, ' + 'если нужно удалить несколько, вводите через запятую (/reset для сброса)') + return + ids = await get_ids_for_multiple_record(message.text) + deleted_records = await delete_data_from_groups(ids) + if deleted_records: + await message.answer(f'Удалил {deleted_records} записи(-ей)') + await vacuum() + else: + await message.answer(f'Таких строк нет в таблице') + await state.finish() + await message.answer(text='⚙ Настройка таблицы соответствия групп ⚙', + reply_markup=get_conf_groups_kb()) + + +def register_delete_from_groups(dp: Dispatcher): + chat_types = [ChatType.PRIVATE] + dp.register_message_handler(delete_from_groups, + chat_type=chat_types, + state=Configure.DeleteUserGroups, + is_admin=True) diff --git a/tgbot/handlers/admin/delete_from_numbers.py b/tgbot/handlers/admin/delete_from_numbers.py new file mode 100644 index 0000000..14c714a --- /dev/null +++ b/tgbot/handlers/admin/delete_from_numbers.py @@ -0,0 +1,45 @@ +from aiogram import Dispatcher, types +from aiogram.dispatcher import FSMContext +from aiogram.types import ChatType + +from tgbot.Utils.DBWorker import delete_data_from_grant_numbers, vacuum +from tgbot.Utils.check_ids_records import check_ids +from tgbot.Utils.get_ids_for_grant_numbers import get_ids_for_multiple_record +from tgbot.keyboards.inline import get_conf_numbers_kb + +from tgbot.misc.states import Configure + + +async def delete_numbers(message: types.Message, state: FSMContext) -> None: + """ + Функция для удаления записей из таблицы c поздр. номерами + :param message: types.Message + :param state: FSMContext + :return: None + """ + if message.text == '/reset': + await state.finish() + return + record_id = await check_ids(message.text) + if not record_id: + await message.answer('Введите IDs строк для удаления записей из базы, целые числа, ' + 'если нужно удалить несколько, вводите через запятую (/reset для сброса)') + return + ids = await get_ids_for_multiple_record(message.text) + deleted_records = await delete_data_from_grant_numbers(ids) + if deleted_records: + await message.answer(f'Удалил {deleted_records} записи(-ей)') + await vacuum() + else: + await message.answer(f'Таких строк нет в таблице') + await state.finish() + await message.answer(text='⚙ Настройка таблицы с поздр. номерами ⚙', + reply_markup=get_conf_numbers_kb()) + + +def register_delete_numbers(dp: Dispatcher): + chat_types = [ChatType.PRIVATE] + dp.register_message_handler(delete_numbers, + chat_type=chat_types, + state=Configure.DeleteNumbers, + is_admin=True) diff --git a/tgbot/handlers/admin/delete_groups_callback.py b/tgbot/handlers/admin/delete_groups_callback.py new file mode 100644 index 0000000..6e10af8 --- /dev/null +++ b/tgbot/handlers/admin/delete_groups_callback.py @@ -0,0 +1,30 @@ +from contextlib import suppress + +from aiogram import Dispatcher, types +from aiogram.types import ChatType +from aiogram.utils.exceptions import MessageCantBeDeleted + +from tgbot.misc.states import Configure + + +async def delete_groups(call: types.CallbackQuery) -> None: + """ + Функция коллбека для удаления записей из таблицы соотв. групп + :param call: types.CallbackQuery + :return: None + """ + with suppress(MessageCantBeDeleted): + await call.message.delete() + + await call.message.answer('Введите IDs строк для удаления записей из базы, целые числа, ' + 'если нужно удалить несколько, вводите через запятую (/reset для сброса)') + await Configure.DeleteUserGroups.set() + + +def register_delete_groups_cb(dp: Dispatcher): + chat_types = [ChatType.PRIVATE] + dp.register_callback_query_handler(delete_groups, + chat_type=chat_types, + text='delete_groups', + state="*", + is_admin=True) diff --git a/tgbot/handlers/admin/delete_numbers_callback.py b/tgbot/handlers/admin/delete_numbers_callback.py new file mode 100644 index 0000000..f481a2b --- /dev/null +++ b/tgbot/handlers/admin/delete_numbers_callback.py @@ -0,0 +1,29 @@ +from contextlib import suppress + +from aiogram import Dispatcher, types +from aiogram.types import ChatType +from aiogram.utils.exceptions import MessageCantBeDeleted + +from tgbot.misc.states import Configure + + +async def delete_numbers(call: types.CallbackQuery) -> None: + """ + Функция коллбека для удаления записей из таблицы c поздр. номерами + :param call: types.CallbackQuery + :return: None + """ + with suppress(MessageCantBeDeleted): + await call.message.delete() + + await call.message.answer('Введите IDs строк для удаления записей из базы, целые числа, ' + 'если нужно удалить несколько, вводите через запятую (/reset для сброса)') + await Configure.DeleteNumbers.set() + + +def register_delete_numbers_cb(dp: Dispatcher): + chat_types = [ChatType.PRIVATE] + dp.register_callback_query_handler(delete_numbers, + chat_type=chat_types, + text='delete_numbers', + is_admin=True) diff --git a/tgbot/handlers/admin/get_mod_group.py b/tgbot/handlers/admin/get_mod_group.py new file mode 100644 index 0000000..d01d864 --- /dev/null +++ b/tgbot/handlers/admin/get_mod_group.py @@ -0,0 +1,36 @@ +from aiogram import Dispatcher, types +from aiogram.dispatcher import FSMContext +from aiogram.types import ChatType + +from tgbot.Utils.check_number import check_number_in_message + +from tgbot.misc.states import Configure + + +async def get_mod_group(message: types.Message, state: FSMContext) -> None: + """ + Функция доя получения ID группы модераторов при добавлениий новой записи в таблицу соотв. групп + :param message: types.Message + :param state: FSMContext + :return: None + """ + if message.text == '/reset': + await state.finish() + return + group_id = await check_number_in_message(message.text) + if not group_id or len(message.text) < 5: + await message.answer('Введите id группы модераторов, целое число (/reset для сброса)') + return + + await state.update_data(id_mod=group_id) + await message.answer('Введите IDs групп пользователей, несколько групп,' + ' необходимо разделить через запятую (/reset для сброса)') + await Configure.AddUserGroups.set() + + +def register_get_mod_group(dp: Dispatcher): + chat_types = [ChatType.PRIVATE] + dp.register_message_handler(get_mod_group, + chat_type=chat_types, + state=Configure.AddModGroups, + is_admin=True) diff --git a/tgbot/handlers/admin/get_numbers.py b/tgbot/handlers/admin/get_numbers.py new file mode 100644 index 0000000..bb1bc51 --- /dev/null +++ b/tgbot/handlers/admin/get_numbers.py @@ -0,0 +1,51 @@ +from aiogram import Dispatcher, types +from aiogram.dispatcher import FSMContext +from aiogram.types import ChatType + +from tgbot.Utils.check_ids_records import check_ids +from tgbot.Utils.check_message_user_groups import check_users_groups +from tgbot.Utils.delete_doubles import delete_doubles_ids +from tgbot.keyboards.inline import get_conf_numbers_kb +from tgbot.misc.states import Configure +from tgbot.Utils.DBWorker import get_data_from_grant_numbers, set_data_numbers + + +async def get_grant_numbers(message: types.Message, state: FSMContext) -> None: + """ + Функция для ввода номеров для поздрв. для записи в таблицу БД + :param message: types.Message + :param state: FSMContext + :return: None + """ + if message.text == '/reset': + await state.finish() + return + group_ids_check = await check_ids(message.text) + + if not group_ids_check: + await message.answer('Введите номера для поздравления, несколько номеров' + ' необходимо разделить через запятую (/reset для сброса)') + return + data = await state.get_data() + id_group = data.get('id_group') + + existed = await get_data_from_grant_numbers(group_id=id_group) + numbers = message.text + if existed: + numbers += f',{existed[0][0]}' + text_message = await delete_doubles_ids(message=numbers, sort=True) + + await set_data_numbers(values=(id_group, text_message,)) + + await message.answer('Записал') + await state.finish() + await message.answer(text='⚙ Настройка таблицы с поздр. номерами ⚙', + reply_markup=get_conf_numbers_kb()) + + +def register_get_grant_numbers(dp: Dispatcher): + chat_types = [ChatType.PRIVATE] + dp.register_message_handler(get_grant_numbers, + chat_type=chat_types, + state=Configure.AddNumbers, + is_admin=True) diff --git a/tgbot/handlers/admin/get_numbers_group.py b/tgbot/handlers/admin/get_numbers_group.py new file mode 100644 index 0000000..73ef1cb --- /dev/null +++ b/tgbot/handlers/admin/get_numbers_group.py @@ -0,0 +1,36 @@ +from aiogram import Dispatcher, types +from aiogram.dispatcher import FSMContext +from aiogram.types import ChatType + +from tgbot.Utils.check_number import check_number_in_message + +from tgbot.misc.states import Configure + + +async def get_numbers_group(message: types.Message, state: FSMContext) -> None: + """ + Функция для ввода групп пользователей для записи в таблицу БД + :param message: types.Message + :param state: FSMContext + :return: None + """ + if message.text == '/reset': + await state.finish() + return + group_id = await check_number_in_message(message.text) + if not group_id or len(message.text) < 5: + await message.answer('Введите id группы, целое число (/reset для сброса)') + return + + await state.update_data(id_group=group_id) + await message.answer('Введите номера для поздравления, несколько номеров' + ' необходимо разделить через запятую (/reset для сброса)') + await Configure.AddNumbers.set() + + +def register_numbers_group(dp: Dispatcher): + chat_types = [ChatType.PRIVATE] + dp.register_message_handler(get_numbers_group, + chat_type=chat_types, + state=Configure.AddNumbersGroup, + is_admin=True) diff --git a/tgbot/handlers/admin/get_users_groups.py b/tgbot/handlers/admin/get_users_groups.py new file mode 100644 index 0000000..410d20e --- /dev/null +++ b/tgbot/handlers/admin/get_users_groups.py @@ -0,0 +1,50 @@ +from aiogram import Dispatcher, types +from aiogram.dispatcher import FSMContext +from aiogram.types import ChatType + +from tgbot.Utils.check_message_user_groups import check_users_groups +from tgbot.Utils.delete_doubles import delete_doubles_ids +from tgbot.Utils.get_ids_for_grant_numbers import get_ids_for_multiple_record +from tgbot.keyboards.inline import get_conf_groups_kb +from tgbot.misc.states import Configure +from tgbot.Utils.DBWorker import set_data_groups, get_users_groups + + +async def get_users_group(message: types.Message, state: FSMContext) -> None: + """ + Функция для получения IDs групп пользователей, для таблицы соответствия групп + :param message: types.Message + :param state: FSMContext + :return: None + """ + if message.text == '/reset': + await state.finish() + return + group_ids_check = await check_users_groups(message.text) + + if not group_ids_check: + await message.answer('Введите IDs групп пользователей, целые числа, если групп несколько, ' + 'необходимо разделять с помощь запятой (/reset для сброса)') + return + data = await state.get_data() + id_mod_group = data.get('id_mod') + + existed = await get_users_groups(group_id=id_mod_group) + ids_user_groups = message.text + if existed: + ids_user_groups += f',{existed[0][0]}' + text_message = await delete_doubles_ids(ids_user_groups) + await set_data_groups(values=(id_mod_group, text_message,)) + + await message.answer('Записал') + await state.finish() + await message.answer(text='⚙ Настройка таблицы соответствия групп ⚙', + reply_markup=get_conf_groups_kb()) + + +def register_get_users_group(dp: Dispatcher): + chat_types = [ChatType.PRIVATE] + dp.register_message_handler(get_users_group, + chat_type=chat_types, + state=Configure.AddUserGroups, + is_admin=True) diff --git a/tgbot/handlers/admin/main_menu.py b/tgbot/handlers/admin/main_menu.py new file mode 100644 index 0000000..33a4f83 --- /dev/null +++ b/tgbot/handlers/admin/main_menu.py @@ -0,0 +1,21 @@ +from aiogram import Dispatcher, types +from aiogram.types import ChatType +from tgbot.keyboards.inline import get_main_menu_kb + + +async def main_menu(message: types.Message) -> None: + """ + Функция для вызова главного меню при отправке комманды /configure + :param message: types.Message + :return: None + """ + await message.answer(text='⚙ ГЛАВНОЕ МЕНЮ ⚙', reply_markup=get_main_menu_kb()) + + +def register_main_menu(dp: Dispatcher): + chat_types = [ChatType.PRIVATE] + dp.register_message_handler(main_menu, + chat_type=chat_types, + commands=['configure'], + is_admin=True + ) diff --git a/tgbot/handlers/admin/show_groups_callback.py b/tgbot/handlers/admin/show_groups_callback.py new file mode 100644 index 0000000..9e1b5e2 --- /dev/null +++ b/tgbot/handlers/admin/show_groups_callback.py @@ -0,0 +1,37 @@ +from contextlib import suppress + +from aiogram import md +from aiogram import Dispatcher, types +from aiogram.types import ChatType +from aiogram.utils.exceptions import MessageCantBeDeleted + +from tgbot.Utils.DBWorker import get_groups + + +async def show_groups(call: types.CallbackQuery) -> None: + """ + Функция коллбека для показа данных из таблицы соответствия groups + :param call: types.CallbackQuery + :return: None + """ + groups = await get_groups() + text = md.hbold('| ID | Группа модераторов | Группы пользователей |\n\n') + + with suppress(MessageCantBeDeleted): + await call.message.delete() + + if groups: + for group in groups: + text += md.hunderline(f'| {group[0]} | {group[1]} | {group[2]} |\n') + await call.message.answer(text=text) + else: + await call.message.answer(text='Таблица еще пуста') + + +def register_show_groups(dp: Dispatcher): + chat_types = [ChatType.PRIVATE] + dp.register_callback_query_handler(show_groups, + chat_type=chat_types, + text='show_groups', + state="*", + is_admin=True) \ No newline at end of file diff --git a/tgbot/handlers/admin/show_numbers_callback.py b/tgbot/handlers/admin/show_numbers_callback.py new file mode 100644 index 0000000..819b34b --- /dev/null +++ b/tgbot/handlers/admin/show_numbers_callback.py @@ -0,0 +1,37 @@ +from contextlib import suppress + +from aiogram import md +from aiogram import Dispatcher, types +from aiogram.types import ChatType +from aiogram.utils.exceptions import MessageCantBeDeleted + +from tgbot.Utils.DBWorker import get_numbers + + +async def show_numbers(call: types.CallbackQuery) -> None: + """ + Функция коллбека для показа данных из таблицы с позд. номерами grant_numbers + :param call: types.CallbackQuery + :return: None + """ + groups = await get_numbers() + text = md.hbold('| ID | ID Группы | Номера для поздравления |\n\n') + + with suppress(MessageCantBeDeleted): + await call.message.delete() + + if groups: + for group in groups: + text += md.hunderline(f'| {group[0]} | {group[1]} | {group[2]} |\n') + await call.message.answer(text=text) + else: + await call.message.answer(text='Таблица еще пуста') + + +def register_show_numbers(dp: Dispatcher): + chat_types = [ChatType.PRIVATE] + dp.register_callback_query_handler(show_numbers, + chat_type=chat_types, + text='show_numbers', + state="*", + is_admin=True) diff --git a/tgbot/handlers/admin/user.py b/tgbot/handlers/admin/user.py new file mode 100644 index 0000000..06b6024 --- /dev/null +++ b/tgbot/handlers/admin/user.py @@ -0,0 +1,15 @@ +from aiogram import Dispatcher +from aiogram.types import Message + + +async def user_start(message: Message) -> None: + """ + Функция отправляет приветствие и сообщает, что у пользователя нет прав для конфигурирования + :param message: Message + :return: None + """ + await message.reply(f"Добрый день, у вас нет прав для конфигурирования!") + + +def register_user(dp: Dispatcher): + dp.register_message_handler(user_start, commands=["start"], state="*") diff --git a/tgbot/handlers/groups/__init__.py b/tgbot/handlers/groups/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tgbot/handlers/groups/catch_update.py b/tgbot/handlers/groups/catch_update.py new file mode 100644 index 0000000..ce40edc --- /dev/null +++ b/tgbot/handlers/groups/catch_update.py @@ -0,0 +1,44 @@ +from typing import List +from aiogram.utils.markdown import quote_html +from tgbot.keyboards.inline import get_gran_kb +from aiogram import Dispatcher, types +from aiogram.types import ChatType +import uuid +from tgbot.Utils.get_user_link import get_link +from tgbot.Utils.DBWorker import set_data_queue + + +async def new_chat(update: types.ChatMemberUpdated, ids: List[tuple[int]], count: int) -> None: + """ + Функция для отлова события присоеднинения новго пользователя к модерируемой группе + :param update: types.ChatMemberUpdated + :param ids: List[tuple[int]] + :param count: int + :return: None + """ + link = await get_link(update.new_chat_member.user) + uid = str(uuid.uuid4()) + if update.new_chat_member.user.username: + username = f'@{quote_html(update.new_chat_member.user.username)}' + else: + username = 'ника нет' + text = f'🎉 В “{update.chat.title}” группу вступил юбилейный пользователь\n' \ + f'{link} ({username}),\n' \ + f'🔢{count}. 🕐Время вступления {update.date}' + + message = await update.bot.send_message(ids[0][0], text=text, reply_markup=get_gran_kb(uid=uid)) + + await set_data_queue( + values=( + message.message_id, update.chat.id, update.chat.title, ids[0][0], update.new_chat_member.user.id, link, + count, update.date, uid, username)) + + +def register_catch(dp: Dispatcher): + chat_types = [ChatType.GROUP, ChatType.SUPERGROUP] + dp.register_chat_member_handler(new_chat, + chat_type=chat_types, + is_group_join=True, + is_not_granted=True, + is_grant_count=True, + is_user_group=True) diff --git a/tgbot/handlers/groups/check.py b/tgbot/handlers/groups/check.py new file mode 100644 index 0000000..b625b63 --- /dev/null +++ b/tgbot/handlers/groups/check.py @@ -0,0 +1,32 @@ +from typing import List +from aiogram import Dispatcher, types +from aiogram.types import ChatType +from tgbot.Utils.DBWorker import get_queue + + +async def check_queue(message: types.Message, ids: List[tuple]) -> None: + """ + Функция для проверки очереди на поздравление, комманда /проверка, доступна только в модераторских группах + :param message: types.Message + :param ids: List[tuple] + :return: None + """ + users_groups_ids = ids[0][0].split(',') + + for group_id in users_groups_ids: + messages_in_queue = await get_queue(group_id) + if messages_in_queue: + text = f'Есть не поздравленные пользователи в группе {messages_in_queue[0][3]}:\n' + for message_in_queue in messages_in_queue: + text += f'- Пользователь {message_in_queue[6]} номер вступления {message_in_queue[7]}\n' + await message.answer(text=text) + else: + await message.answer('Нет не поздравленных пользователей') + + +def register_check_queue(dp: Dispatcher): + chat_types = [ChatType.GROUP, ChatType.SUPERGROUP] + dp.register_message_handler(check_queue, + chat_type=chat_types, + commands=['проверка'], + is_moder_group=True) diff --git a/tgbot/handlers/groups/get_granted.py b/tgbot/handlers/groups/get_granted.py new file mode 100644 index 0000000..b26f514 --- /dev/null +++ b/tgbot/handlers/groups/get_granted.py @@ -0,0 +1,35 @@ +from typing import List +from aiogram import Dispatcher, types +from aiogram.dispatcher import FSMContext +from aiogram.types import ChatType + +from tgbot.keyboards.inline import get_list_granted_kb +from tgbot.misc.show_granted import send_granted_message +from tgbot.Utils.DBWorker import get_data_granted, get_users_groups + + +async def get_granted(message: types.Message, ids: List[tuple], state: FSMContext) -> None: + """ + Функция для показа поздравленных пользователей, комманда /списокЮбилейный, доступна только в модераторских группах + :param message: types.Message + :param ids: List[tuple] + :param state: FSMContext + :return: None + """ + granted_list = await get_data_granted(message.chat.id) + if granted_list: + user_groups_ids = {id_user_gr[1]: id_user_gr[2] for id_user_gr in granted_list} + if len(user_groups_ids) > 1: + await message.answer(text='Выберите группу', reply_markup=get_list_granted_kb(user_groups_ids)) + else: + await send_granted_message(granted_list, message) + else: + await message.answer('В группах для модерирования еще нет поздравленных пользователей') + + +def register_get_granted(dp: Dispatcher): + chat_types = [ChatType.GROUP, ChatType.SUPERGROUP] + dp.register_message_handler(get_granted, + chat_type=chat_types, + commands=['списокЮбилейный'], + is_moder_group=True) diff --git a/tgbot/handlers/groups/grant_callback.py b/tgbot/handlers/groups/grant_callback.py new file mode 100644 index 0000000..ea501ed --- /dev/null +++ b/tgbot/handlers/groups/grant_callback.py @@ -0,0 +1,71 @@ +from contextlib import suppress + +from aiogram import Dispatcher, types +from aiogram.dispatcher.filters import Text +from aiogram.dispatcher.handler import CancelHandler +from aiogram.types import ChatType +from aiogram.utils.exceptions import MessageCantBeEdited, MessageToEditNotFound, MessageNotModified + +from tgbot.misc.grant_text import get_great_text +from tgbot.Utils.DBWorker import get_message_in_queue, get_queue, delete_from_queue, vacuum, set_data_granted, \ + count_from_queue + + +async def grant_user(call: types.CallbackQuery) -> None: + """ + Функция коллбека для поздравления пользователя + :param call: types.CallbackQuery + :return: None + """ + uid = call.data.split('|')[1] + message_btn = await get_message_in_queue(uid) + if not message_btn: + raise CancelHandler() + message_id = message_btn[0][1] + group_id_users = message_btn[0][2] + group_id_mod = message_btn[0][4] + user_id = message_btn[0][5] + user = message_btn[0][6] + count = message_btn[0][7] + datetime_update = message_btn[0][8] + datetime_granted = call.message.date + moder_id = call.from_user.id + count_for_grant = await count_from_queue(group_id_users) + username = message_btn[0][10] + + chat_member = await call.bot.get_chat_member(group_id_users, user_id) + if not chat_member: + await call.message.delete_reply_markup() + await call.answer(text="Такого пользователя уже нет в группе", show_alert=True) + raise CancelHandler() + + text = get_great_text(user, count_for_grant[0][0]) + + grant_message = await call.bot.send_message(chat_id=group_id_users, text=text) + + await call.answer() + await call.message.answer(text=f"Пользователь {user} в группе {grant_message.chat.title} поздравлен") + + with suppress(MessageCantBeEdited, MessageToEditNotFound, MessageNotModified): + await call.message.delete_reply_markup() + + granted = [(group_id_users, grant_message.chat.title, user_id, user, group_id_mod, moder_id, count, datetime_update, + datetime_granted, username)] + + queue = await get_queue(group_id=group_id_users, message_id=message_id) + if queue: + for message in queue: + with suppress(MessageCantBeEdited, MessageToEditNotFound, MessageNotModified): + await call.bot.edit_message_reply_markup(chat_id=message[4], message_id=message[1], reply_markup=None) + granted.append((message[2], message[3], message[5], message[6], message[4], '', message[7], message[8], + '', message[10])) + + await delete_from_queue(group_id_users) + await vacuum() + + await set_data_granted(values=granted) + + +def register_grant(dp: Dispatcher): + chat_types = [ChatType.GROUP, ChatType.SUPERGROUP] + dp.register_callback_query_handler(grant_user, Text(startswith='grant'), chat_type=chat_types) diff --git a/tgbot/handlers/groups/grant_cancel_callback.py b/tgbot/handlers/groups/grant_cancel_callback.py new file mode 100644 index 0000000..386367d --- /dev/null +++ b/tgbot/handlers/groups/grant_cancel_callback.py @@ -0,0 +1,23 @@ +from aiogram import Dispatcher, types +from aiogram.dispatcher.filters import Text +from aiogram.types import ChatType + +from tgbot.Utils.DBWorker import get_message_in_queue + + +async def cancel_grant_user(call: types.CallbackQuery) -> None: + """ + Функция коллбека, убирает кнопку поздравления пользователя + :param call: types.CallbackQuery + :return: None + """ + uid = call.data.split('|')[1] + message = await get_message_in_queue(uid) + await call.answer() + if message: + await call.message.delete_reply_markup() + + +def register_cancel_grant(dp: Dispatcher): + chat_types = [ChatType.GROUP, ChatType.SUPERGROUP] + dp.register_callback_query_handler(cancel_grant_user, Text(startswith='can'), chat_type=chat_types,) diff --git a/tgbot/handlers/groups/restore.py b/tgbot/handlers/groups/restore.py new file mode 100644 index 0000000..d716496 --- /dev/null +++ b/tgbot/handlers/groups/restore.py @@ -0,0 +1,43 @@ +from contextlib import suppress +from typing import List + +from aiogram.utils.exceptions import MessageToDeleteNotFound, MessageCantBeDeleted + +from tgbot.keyboards.inline import get_gran_kb +from aiogram import Dispatcher, types +from aiogram.types import ChatType + +from tgbot.Utils.DBWorker import get_queue, update_data_queue + + +async def restore(message: types.Message, ids: List[tuple]) -> None: + """ + Функция для повторной отправки сообщений с кнопками для поздр. в группу модераторов + :param message: types.Message + :param ids: List[tuple] + :return: None + """ + users_groups_ids = ids[0][0].split(',') + + for group_id in users_groups_ids: + messages_in_queue = await get_queue(int(group_id)) + if messages_in_queue: + for message_in_queue in messages_in_queue: + with suppress(MessageToDeleteNotFound, MessageCantBeDeleted): + await message.bot.delete_message(message.chat.id, message_in_queue[1]) + text = f'🎉 “{message_in_queue[3]}” 👤 {message_in_queue[6]} ({message_in_queue[10]}),\n' \ + f'🔢 {message_in_queue[7]} 🕐 {message_in_queue[8]}' + new_message = await message.bot.send_message(message.chat.id, text=text, + reply_markup=get_gran_kb(uid=message_in_queue[9])) + await update_data_queue(message_id=new_message.message_id, old_message_id=message_in_queue[1], + group_id=message_in_queue[2]) + else: + await message.answer('Нет сообщений для восстановления') + + +def register_restore(dp: Dispatcher): + chat_types = [ChatType.GROUP, ChatType.SUPERGROUP] + dp.register_message_handler(restore, + chat_type=chat_types, + commands=['восстановить'], + is_moder_group=True) diff --git a/tgbot/handlers/groups/show_granted_callback.py b/tgbot/handlers/groups/show_granted_callback.py new file mode 100644 index 0000000..4c9959e --- /dev/null +++ b/tgbot/handlers/groups/show_granted_callback.py @@ -0,0 +1,28 @@ +from contextlib import suppress + +from aiogram import Dispatcher +from aiogram.types import CallbackQuery, ChatType +from aiogram.utils.exceptions import MessageToEditNotFound, MessageCantBeDeleted + +from tgbot.Utils.DBWorker import get_data_granted_for_kb +from tgbot.keyboards.inline import cb +from tgbot.misc.show_granted import send_granted_message + + +async def show_granted_cb(call: CallbackQuery, callback_data: dict) -> None: + """ + Функция коллбека, для фабрики коллбеков, для показа юбилейных пользователей + :param call: CallbackQuery + :param callback_data: dict + :return: None + """ + id_group = callback_data["ids"] + granted_list = await get_data_granted_for_kb(id_group) + with suppress(MessageCantBeDeleted, MessageToEditNotFound): + await call.message.delete() + await send_granted_message(granted_list, call.message) + + +def register_show_granted_cb(dp: Dispatcher): + chat_types = [ChatType.GROUP, ChatType.SUPERGROUP] + dp.register_callback_query_handler(show_granted_cb, cb.filter(), chat_type=chat_types) diff --git a/tgbot/handlers/groups/show_granted_cancel_callback.py b/tgbot/handlers/groups/show_granted_cancel_callback.py new file mode 100644 index 0000000..843b20b --- /dev/null +++ b/tgbot/handlers/groups/show_granted_cancel_callback.py @@ -0,0 +1,21 @@ +from contextlib import suppress +from aiogram import Dispatcher +from aiogram.types import CallbackQuery, ChatType +from aiogram.utils.exceptions import MessageToEditNotFound, MessageCantBeDeleted +from tgbot.keyboards.inline import cb + + +async def show_granted_cb(call: CallbackQuery, callback_data: dict) -> None: + """ + Функция коллбека для закрытия меню для /списокЮбилейный + :param call: CallbackQuery + :param callback_data: dict + :return: None + """ + with suppress(MessageCantBeDeleted, MessageToEditNotFound): + await call.message.delete() + + +def register_show_granted_cb(dp: Dispatcher): + chat_types = [ChatType.GROUP, ChatType.SUPERGROUP] + dp.register_callback_query_handler(show_granted_cb, cb.filter(ids=0), chat_type=chat_types) 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.py b/tgbot/keyboards/inline.py new file mode 100644 index 0000000..fc75524 --- /dev/null +++ b/tgbot/keyboards/inline.py @@ -0,0 +1,77 @@ +from aiogram import types +from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton +from aiogram.utils.callback_data import CallbackData + + +def get_gran_kb(uid: str) -> InlineKeyboardMarkup: + """ + Возвращает inline клав. для поздравления + :param uid: + :return: InlineKeyboardMarkup + """ + grant_btn = InlineKeyboardButton('Поздравить!', callback_data=f'grant|{uid}') + cancel_btn = InlineKeyboardButton('Отмена', callback_data=f'can|{uid}') + grant_kb = InlineKeyboardMarkup().add(grant_btn, cancel_btn) + return grant_kb + + +def get_conf_groups_kb() -> InlineKeyboardMarkup: + """ + Возвращает inline клав. для настройки таблицы соотв. групп + :return: InlineKeyboardMarkup + """ + show_btn = InlineKeyboardButton('Показать таблицу', callback_data='show_groups') + add_btn = InlineKeyboardButton('Добавить строку', callback_data='add_groups') + delete_btn = InlineKeyboardButton('Удалить строку', callback_data='delete_groups') + back_to_main_btn = InlineKeyboardButton('Главное меню', callback_data='back_to_main') + conf_groups_kb = InlineKeyboardMarkup(row_width=1).add(show_btn, add_btn, delete_btn, back_to_main_btn) + return conf_groups_kb + + +def get_main_menu_kb() -> InlineKeyboardMarkup: + """ + Возвращает inline клав. для главного меню + :return: InlineKeyboardMarkup + """ + config_groups_btn = InlineKeyboardButton('Настройка таблицы групп', callback_data='configure_groups') + config_numbers_btn = InlineKeyboardButton('Настройка таблицы номеров', callback_data='configure_numbers') + cancel_btn = InlineKeyboardButton('Отмена', callback_data='cancel') + main_kb = InlineKeyboardMarkup(row_width=1).add(config_groups_btn, config_numbers_btn, cancel_btn) + return main_kb + + +def get_conf_numbers_kb(): + """ + Возвращает inline клав. для настройки таблицы c поздр. номерами + :return: InlineKeyboardMarkup + """ + show_btn = InlineKeyboardButton('Показать таблицу', callback_data='show_numbers') + add_btn = InlineKeyboardButton('Добавить строку', callback_data='add_numbers') + delete_btn = InlineKeyboardButton('Удалить строку', callback_data='delete_numbers') + back_to_main_btn = InlineKeyboardButton('Главное меню', callback_data='back_to_main') + conf_numbers_kb = InlineKeyboardMarkup(row_width=1).add(show_btn, add_btn, delete_btn, back_to_main_btn) + return conf_numbers_kb + + +cb = CallbackData('gr', 'ids') + + +def get_list_granted_kb(user_groups: dict) -> types.InlineKeyboardMarkup: + """ + Фабрика коллбеков для вывода групп, если их больше одной, при отправке команды /списокЮбилейный + :param user_groups: dict + :return: types.InlineKeyboardMarkup + """ + markup = types.InlineKeyboardMarkup() + for ids, user_group in user_groups.items(): + markup.add( + types.InlineKeyboardButton( + user_group, + callback_data=cb.new(ids=ids)), + ) + markup.add( + types.InlineKeyboardButton( + 'Отмена', + callback_data=cb.new(ids=0)), + ) + return markup diff --git a/tgbot/middlewares/__init__.py b/tgbot/middlewares/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tgbot/misc/__init__.py b/tgbot/misc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tgbot/misc/grant_text.py b/tgbot/misc/grant_text.py new file mode 100644 index 0000000..9266e0d --- /dev/null +++ b/tgbot/misc/grant_text.py @@ -0,0 +1,11 @@ +def get_great_text(user: str, count: int) -> str: + """ + Функция возврата текста для поздравления + :param user: str + :param count: int + :return: str + """ + text = f'🎉 Поздравляю, {user}, как же удачно попали в нужное место и в нужное время!\n' \ + f'Вы {count} участник комьюнити.\n' \ + f'Вас ждут плюшки и печенюшки!🎉' + return text diff --git a/tgbot/misc/set_commands.py b/tgbot/misc/set_commands.py new file mode 100644 index 0000000..03412dd --- /dev/null +++ b/tgbot/misc/set_commands.py @@ -0,0 +1,14 @@ +from aiogram import types, Dispatcher + + +async def set_default_commands(dp: Dispatcher) -> None: + """ + Функция для установки комманд меню в приватном канале бота + :param dp: Dispatcher + :return: None + """ + + await dp.bot.set_my_commands([ + types.BotCommand("start", "Запустить бота"), + types.BotCommand("/configure", "Конфигурация"), + ]) diff --git a/tgbot/misc/show_granted.py b/tgbot/misc/show_granted.py new file mode 100644 index 0000000..7054f75 --- /dev/null +++ b/tgbot/misc/show_granted.py @@ -0,0 +1,16 @@ +from aiogram import types + + +async def send_granted_message(granted_list: list[tuple], message: types.Message) -> None: + """ + Функция для отправкий сообщений с юбилейными пользователями + :param granted_list: list[tuple] + :param message: types.Message + :return: None + """ + for granted in granted_list: + emoji = '🎉' + if granted[6]: + emoji = '👑👑👑' + await message.answer(text=f'{emoji} “{granted[2]}” 👤 {granted[4]} ({granted[10]}),\n' + f'🔢 {granted[7]} 🕐 {granted[8]}') diff --git a/tgbot/misc/states.py b/tgbot/misc/states.py new file mode 100644 index 0000000..381a89e --- /dev/null +++ b/tgbot/misc/states.py @@ -0,0 +1,10 @@ +from aiogram.dispatcher.filters.state import StatesGroup, State + + +class Configure(StatesGroup): + AddNumbersGroup = State() + AddNumbers = State() + DeleteNumbers = State() + AddModGroups = State() + AddUserGroups = State() + DeleteUserGroups = State() diff --git a/tgbot/models/__init__.py b/tgbot/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tgbot/services/__init__.py b/tgbot/services/__init__.py new file mode 100644 index 0000000..e69de29