diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..57e109f Binary files /dev/null and b/.DS_Store differ diff --git a/.env.template b/.env.template new file mode 100644 index 0000000..1ea3fcb --- /dev/null +++ b/.env.template @@ -0,0 +1,2 @@ +BOT_TOKEN="Ваш токен для бота, полученный от @BotFather" +ADMIN_IDS='["user_id первого админа", "user_id второго админа", "user_id третьего админа"]' \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..913d692 --- /dev/null +++ b/.gitignore @@ -0,0 +1,131 @@ +# 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 + +# 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/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# 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__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.venv +*/.env +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ +.idea/* +.env \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ac7d706 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +# создаем образ +FROM python:3.9-alpine3.16 + +#копируем все файлы в директорию /app докер-контейнера +COPY . /app + +# устанавливаем все необходимые зависимости для работы приложения +RUN pip install -r /app/requirements.txt + +# запускаем приложение +CMD [ "python", "/app/main.py" ] diff --git a/README.md b/README.md index 1164b0e..848b41a 100644 --- a/README.md +++ b/README.md @@ -1 +1,179 @@ # tg-bot-users 🤖 + +Телеграм-бот отслеживает юбилейных пользователей групп. По умолчанию, юбилейными считаются участники, кратные 500. Эти настройки можно изменить при конфигурации бота. +При вступлении в комьюнити-группу нового, юбилейного участника бот присылает в группу модераторов уведомление с именем, ником, id пользователя, юбилейным номером и датой и временем вступления, а также кнопкой "Поздравить" для автоматического поздравления юбилейного участника. +Помимо юбилейных, бот сохраняет двух последующих участников, так как юбилейным может вступить не пользователь, а бот или модератор. + +## Особенности + +* Бот проверяет, является ли новый юбилейный пользователь ботом, и если да, то не сохраняет информацию о нем и не считает его юбилейным. +* Бот проверяет, был ли уже новый юбилейный пользователь победителем, и если да, то не сохраняет информацию о нем и не считает его юбилейным. +* Настройки администрирования доступны только пользователям с правами администраторов. +* Команды боту доступны только в чатах модераторов. +* Поздравление юбилейных пользователей автоматизировано. + +## Requirements + +* Python 3.9+ +* [pyTelegramBotAPI](https://github.com/python-telegram-bot/python-telegram-bot) – Python Telegram Bot API +* [dotenv](https://pypi.org/project/python-dotenv/) - виртуальное окружение dotenv + +Если вы планируете запускать приложение локально, то можете установить все зависимости, выполнив следующую команду: `pip install -r requirements.txt`. + +## Запуск приложения + +### 1. Подготовка к запуску +1. Если у вас еще нет telegram-бота, создайте его с помощью @BotFather бота и сохраните token от вашего бота. +2. Добавьте вашего бота во все telegram-чаты, где вы хотите, чтобы он отслеживал юбилейных участников. +3. Определите пользователей, которые будут обладать правами администратора, и узнайте их user_id с помощью @username_to_id_bot бота. +4. По умолчанию, юбилейным номером считается число 500. Вы можете изменить эти настройки в файле `config_data/config.py` поменяв значение `HAPPY_NUMBER`. + +Приложение можно запустить локально либо через контейнер docker. + +### 2а. Запустить приложение локально + +1. Создать файл .env и наполнить его согласно инструкции из файла /.env.template. +2. Скачать приложение tg-bot-users в нужную директорию. +3. Сохранить файл .env в директорию приложения tg-bot-users. +4. В терминале перейти в директорию tg-bot-users. +5. Установить зависимости, выполнив в терминале следующую команду: `pip install -r requirements.txt`. +6. Запустить файл main.py + +Бот запущен! Переходите к настройкам админки. + +### 2б. Запустить приложение через контейнер docker: + +#### Вариант 1: через файл .env + +1. Создать файл .env и наполнить его согласно инструкции из файла /.env.template. +2. Скачать приложение tg-bot-users в нужную директорию. +3. Сохранить файл .env в директории приложения tg-bot-users. +4. Установить и запустить docker (например, Docker Desktop). +5. В терминале перейти в директорию tg-bot-users. +6. Создать контейнер docker, введя в терминале команду: + + docker build -t telegram-bot:latest . + +где `telegram-bot` - пример названия контейнера. Можно дать другое название контейнеру. + +7. Запустить контейнер, введя в терминале команду: + + docker run --name telegram-bot --volume c:\Users\tg-bot-users\database:/app/database -d telegram-bot:latest + +где `c:\Users\tg-bot-users\database` - пример абсолютного пути до директории database внутри директории tg-bot-users. +При работе на операционной системе Windows в пути стоит писать обратный слэш, как в примере. При работе с другими операционными системами в пути стоит писать прямой слэш `/`. +Эта команда позволит не терять данные из базы данных при перезапуске приложения и контейнера. + +Бот запущен! Переходите к настройкам админки. + +#### Вариант 2: через передачу параметров при запуске docker контейнера + +1. Скачать приложение tg-bot-users в нужную директорию. +2. Установить и запустить docker (например, Docker Desktop). +3. В терминале перейти в директорию tg-bot-users. +4. Создать контейнер docker, введя в терминале команду: + + docker build -t telegram-bot:latest . + +где `telegram-bot` - пример названия контейнера. Можно дать другое название контейнеру. +5. Запустить контейнер, введя в терминале команду: + + docker run --name telegram-bot --volume c:\Users\tg-bot-users\database:/app/database -e BOT_TOKEN=type_your_token -e ADMIN_IDS='["user_id_of_first_admin", "user_id_of_second_admin"]' telegram-bot:latest + +где `type_your_token` - token от вашего бота; +`user_id_of_first_admin` и `user_id_of_second_admin` - user_id тех пользователей, которым вы хотите дать права администрирования (их не обязательно должно быть двое, может быть один, может быть больше); +`c:\Users\tg-bot-users\database` - пример абсолютного пути до директории database внутри директории tg-bot-users. +При работе на операционной системе Windows в пути стоит писать обратный слэш как в примере. При работе с другими операционными системами в пути стоит писать прямой слэш `/`. +Эта команда позволит не терять данные из базы данных при перезапуске приложения и контейнера. + +Бот запущен! Переходите к настройкам админки. + +### 3. Настройка админки + +По умолчанию команды боту работают только в чатах модераторов. Поэтому сразу после запуска бота администратор должен вызвать команду `/adminsetup` и настроить группы модераторов, в которые будут приходить уведомления из указанных групп пользователей. Для этой операции нужно узнать chat_id групп с помощью @username_to_id_bot бота, отправив ему invite-ссылку на вступление в чат, chat_id которого вам нужно узнать. +Сразу после этой настройки все остальные команды будут доступны всем пользователям в группах модераторов. + +## Команды бота + +По умолчанию все команды боту работают только в чатах модераторов (за исключением `/adminsetup`). + +* `/start` - запуск бота +* `/help` - список команд и их описание +* `/adminsetup` - установка настроек админки (доступно только администраторам) +* `/adminshow` - просмотр настроек админки +* `/luckylist` - по запросу названия группы присылать список всех зафиксированных пользователей за время работы +* `/unceleb` - запрос последних непоздравленных участников + +### Команда /adminsetup + +Доступ к команде есть только у пользователей, user_id которых был перечислен в файле .env в переменной ADMIN_IDS при запуске бота. + +1. Вам нужно узнать chat_id групп модераторов и групп пользователей, которые вы хотите добавить в настройки. Это можно сделать с помощью @username_to_id_bot бота, отправив ему invite-ссылку на вступление в чат, chat_id которого вам нужно узнать. +2. При запуске команды бот запрашивает подтверждение того, что вы хотите изменить настройки админки. Если вы нажмете "да", все текущие настройки сбросятся и их будет нельзя восстановить. Если перед этой командой вы хотите узнать текущие настройки, введите команду `/adminshow`. +3. Если вы нажали "да", то бот запросит данные по id группам модераторов (id_ГМ) и id группам пользователей (id_ГП) в формате: id_ГМ id_ГП_1 id_ГП_2 id_ГП_3 +4. Для одной группы модераторов укажите ее id_ГМ и через пробелы укажите все id_ГП тех чатов, которые подведомственны этой группе модераторов. Обязательно вводите реальные и корректные chat_id, чтобы бот работал корректно и без ошибок. Помните, что chat_id - это отрицательное число. +5. Бот спросит, хотите ли вы добавить еще данные по группам модераторов и пользователей в админку. Если да, то проделайте п.4 столько раз, сколько вам требуется. +6. После завершения настроек админки все остальные команды станут доступны всем пользователям в указанных группах модераторов. + +### Команда /adminshow + +Команда выводит список всех настроенных id Групп Модераторов, куда приходят уведомления из указанных id Групп Пользователей, с группировкой по id Группам Модераторов. + +### Команда /luckylist + +Команда выводит список всех зафиксированных юбилейный пользователей за время работы из всех групп пользователей, которые подведомственны той группе модераторов, из которой была введена команда. +Все участники, которые уже были поздравлены, отмечены символами короны. +Обратите внимание, что если юбилейным пользователем вступил бот или участник, который уже был победителем, он не будет зафиксирован как юбилейный пользователь и не будет выводится этой командой. + +### Команда /unceleb + +Команда выводит список последних непоздравленных пользователей из всех групп пользователей, которые подведомственны той группе модераторов, из которой была введена команда. Если последних непоздравленных пользователей нет, то ничего не выведется. +Эту команду уместно использовать после ошибочного нажатия кнопок "Не поздравлять" для пользователя, которого требовалось поздравить. + +## Для разработчиков + +Структура приложения: +* `config_data/`: + * `config.py` - загрузка переменных окружения и команд боту +* `handlers/`: + * `admin.py` - хендлер для обработки команд `/adminshow` и `/adminsetup` + * `lucky_list.py` - хендлер для обработки команды `/luckylist` + * `register_new_user.py` - логика по обработке новых юбилейных пользователей с автоматизацией поздравления + * `unceleb.py` - хендлер для обработки команды `/unceleb` + * `default_handlers/` - хендлеры для обработки команд `/start` и `/help` +* `keyboards/inline` - inline-клавиатуры для соответствующих команд боту +* `utils/set_bot_commands.py` - установка команд бота +* `.env.template` - шаблон оформления файла .env +* `database/`: + * `commands.py` - все функции для обращения к SQLite-базе данных + * `database.py` - SQLite база данных: + * таблица 'users' (хранение информации о всех зафиксированных юбилейный пользователях): + * id (INTEGER) - первичный ключ + * nickname (STRING) - ник пользователя в telegram + * user_name (STRING) - имя пользователя в telegram + * user_id (INTEGER) - уникальный id пользователя в telegram + * dtime_connetion (DATETIME) - дата и время вступления пользователя в группу пользователей + * congr_number (INTEGER) - юбилейный порядковый номер вступления пользователя в группу пользователей + * chat_id (INTEGER) - уникальный id группы пользователей + * is_winer (INTEGER) - является ли пользователь победителем (был поздравлен): 1 да, 0 нет + * таблица 'groups_relation' (хранение настроек админки): + * relation_id (INTEGER) - первичный ключ + * moderator_id (INTEGER) - уникальный id группы модераторов + * group_id (INTEGER) - уникальный id группы пользователей + * таблица 'temp_storage' (хранение информации о последних непоздравленных пользователях): + * storage_id (INTEGER) - первичный ключ + * chat_id (INTEGER) - уникальный id группы пользователей, в которую вступил юбилейный пользователь + * record_id (INTEGER) - id записи о данном юбилейном пользователе из таблицы 'users' + * bot_message_id (INTEGER) - уникальный id сообщения бота о юбилейном пользователе с кнопкой "Поздравить" + * таблица 'temp_unceleb' (хранение информации о последних непоздравленных пользователях): + * unceleb_id (INTEGER) - первичный ключ + * chat_id (INTEGER) - уникальный id группы пользователей, в которую вступил юбилейный пользователь + * record_id (INTEGER) - id записи о данном юбилейном пользователе из таблицы 'users' + * bot_message_id (INTEGER) - уникальный id сообщения бота о юбилейном пользователе с кнопкой "Поздравить" + * связь таблиц 'users' и 'groups_relation': + * 'users'.chat_id = 'groups_relation'.group_id + * связь таблиц 'users' и 'temp_storage'/'temp_unceleb': + * 'users'.id = 'temp_storage'.record_id/'temp_unceleb'.record_id + + + diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config_data/__init__.py b/config_data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config_data/config.py b/config_data/config.py new file mode 100644 index 0000000..6220f0a --- /dev/null +++ b/config_data/config.py @@ -0,0 +1,21 @@ +import os +from dotenv import load_dotenv, find_dotenv + +if not find_dotenv(): + exit('Переменные окружения не загружены т.к отсутствует файл .env') +else: + load_dotenv() + +BOT_TOKEN = os.getenv('BOT_TOKEN') +ADMIN_IDS = os.getenv('ADMIN_IDS') +HAPPY_NUMBER = 500 + +DEFAULT_COMMANDS = ( + ('start', "Запустить бота"), + ('help', "Вывести справку"), + ('adminsetup', "Установить настройки админки"), + ('adminshow', "Вывести текущие настройки админки"), + ('luckylist', "Запросить юбилейный список"), + ('unceleb', "Вывести последних не поздравленных"), + +) diff --git a/database/.DS_Store b/database/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/database/.DS_Store differ diff --git a/database/__init__.py b/database/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/database/commands.py b/database/commands.py new file mode 100644 index 0000000..fc6a14e --- /dev/null +++ b/database/commands.py @@ -0,0 +1,479 @@ +import sqlite3 +import os +from typing import List +from config_data.config import HAPPY_NUMBER + + +ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) +DB = os.path.join(ROOT_DIR, 'database.db') + + +""""Блок функций для регистрации и обработки нового юбилейного пользователя""" + + +def get_all_group_id() -> List[int]: + """ + Функция, которая возвращает список всех id групп пользователей из таблицы groups_relation. + :return List[int]: список id групп пользователей + """ + with sqlite3.connect((DB)) as conn: + cursor = conn.cursor() + cursor.execute(f"SELECT DISTINCT group_id FROM 'groups_relation'") + group_id_list = cursor.fetchall() + result = [] + for group_id in group_id_list: + result.append(group_id[0]) + return result + + +def get_moderator_id(group_id: int) -> List[int]: + """ + Функция, которая возвращает список id групп модераторов из таблицы groups_relation по id группы пользователя. + :param int group_id: уникальный id группы пользователя + :return List[int]: список id групп модераторов + """ + with sqlite3.connect((DB)) as conn: + cursor = conn.cursor() + cursor.execute(f"SELECT DISTINCT moderator_id FROM 'groups_relation' WHERE group_id={group_id}") + moderator_id_list = cursor.fetchall() + result = [] + for moderator_id in moderator_id_list: + result.append(moderator_id[0]) + return result + + +def insert_to_users(nickname: str, user_name: str, chat_id: int, user_id: int, chat_name: str, + congr_number: int, is_winner: int, dtime_connection) -> None: + """ + Функция, которая записывает nickname, user_name, chat_id, user_id, chat_name, congr_number, is_winner, dtime в + таблицу users при вступлении нового юбилейного пользователя. + :param str nickname: ник пользователя в telegram + :param str user_name: имя пользователя в telegram + :param int chat_id: уникальный id группы, куда вступил новый пользователь + :param int user_id: уникальный id пользователя + :param str chat_name: имя чата, куда вступил новый пользователь + :param int congr_number: юбилейный номер вступления пользователя + :param int is_winner: является ли пользователь победителем (1 да, 0 нет) + :param dtime_connection: дата и время вступления пользователя + :return: None + """ + with sqlite3.connect((DB)) as conn: + cursor = conn.cursor() + cursor.execute(""" + INSERT INTO 'users' (nickname, user_name, chat_name, + congr_number, chat_id, user_id, + dtime_connetion, is_winner) + VALUES (?, ?, ?, ?,?,?,?,?); + """, (nickname, user_name, chat_name, congr_number, chat_id, user_id, dtime_connection, is_winner)) + + +def winner_check(user_id: int) -> list: + """" + Функция которая проверяет, является ли пользователь из таблицы users c конкретным значением user_id победителем. + :param int user_id: уникальный id пользователя + :return list: информация о пользователе-победителе в виде списка + """ + with sqlite3.connect(( DB )) as conn: + cursor = conn.cursor() + cursor.execute(f"SELECT * FROM users WHERE user_id={user_id} AND is_winner=1") + result = cursor.fetchall() + return result + + +def other_lucky_check(count_users: int, chat_id: int) -> list: + """" + Функция которая проверяет, есть ли в таблице users для данного chat_id группы и данного юбилейного номера, + победители. + :param int count_users: юбилейный номер пользователя + :param int chat_id: уникальный id группы + :return list: информация о победителях + """ + lucky_number = count_users - count_users % HAPPY_NUMBER + with sqlite3.connect((DB)) as conn: + cursor = conn.cursor() + cursor.execute(f'''SELECT * FROM users WHERE chat_id={chat_id} AND + (congr_number BETWEEN {lucky_number} AND {lucky_number + 2}) AND is_winner=1;''') + winners_list = cursor.fetchall() + result = [] + for winner in winners_list: + result.append(winner) + return result + + +def select_id_from_users(user_id: int) -> int: + """" + Функция которая по id пользователя возвращает id последней записи о нем из таблицы 'users'. + :param int user_id: уникальный id пользователя + :return int: id записи о пользователе + """ + with sqlite3.connect((DB)) as conn: + cursor = conn.cursor() + cursor.execute(f"SELECT id FROM 'users' WHERE user_id={user_id}") + record_id = cursor.fetchall() + if record_id: + return record_id[-1][0] + return 0 + + +def temp_save(chat_id: int, record_id: int, bot_message_id: int) -> None: + """" + Функция которая в таблицу 'temp_storage' записывает chat_id, record_id, bot_message_id. + :param int chat_id: уникальный id группы + :param int record_id: уникальный id записи о пользователе в таблицу users + :param int bot_message_id: уникальный id сообщения бота + :return: None + """ + with sqlite3.connect((DB)) as conn: + cursor = conn.cursor() + cursor.execute(""" + INSERT INTO 'temp_storage' (chat_id, record_id, bot_message_id) VALUES (?, ?, ?); + """, (chat_id, record_id, bot_message_id)) + + +def is_winner_id_select(bot_message_id: int) -> int: + """" + Функция которая по bot_message_id сообщенияб бота возвращает id записи о пользователе из таблицы 'temp_storage'. + :param int bot_message_id: уникальный id сообщения бота + :return int: id записи о пользователе + """ + with sqlite3.connect((DB)) as conn: + cursor = conn.cursor() + ids = cursor.execute(f'''SELECT users.id FROM users Join temp_storage ON users.id=temp_storage.record_id + WHERE temp_storage.bot_message_id={bot_message_id};''') + result = [] + for id in ids: + result.append(id) + return result[0][0] + + +def data_finder(bot_message_id: int) -> list: + """" + Функция которая по bot_message_id сообщения бота возвращает user_name, congr_number, chat_id из таблицы 'users'. + :param int bot_message_id: уникальный id сообщения бота + :return list: информация о победителе + """ + with sqlite3.connect((DB)) as conn: + cursor = conn.cursor() + result = cursor.execute(f'''SELECT users.user_name, users.congr_number, users.chat_id FROM users + Join temp_storage ON users.id=temp_storage.record_id + WHERE temp_storage.bot_message_id={bot_message_id};''') + data = [] + for i in result: + data.append(i) + return data + + +def is_winner_record(winner_id: int) -> None: + """" + Функция которая по winner_id id записи о пользователе в таблице 'users' обновляет поле is_winner с 0 на 1. + :param int winner_id: уникальный id записи о пользователе в таблице 'users' + :return: None + """ + with sqlite3.connect((DB)) as conn: + cursor = conn.cursor() + cursor.execute(f'''UPDATE users set is_winner = '1' + WHERE id={winner_id}''') + + +def buttons_remover(chat_id: int) -> List[int]: + """" + Функция которая по chat_id группы пользователей из таблицы 'temp_storage' возвращает все bot_message_id. + :param int chat_id: уникальный id группы пользователей + :return List[int]: список id bot_message_id + """ + with sqlite3.connect((DB)) as conn: + cursor = conn.cursor() + cursor.execute(f"SELECT bot_message_id FROM 'temp_storage' WHERE chat_id={chat_id}") + message_id_list = cursor.fetchall() + result = [] + for message_id in message_id_list: + result.append(message_id[0]) + return result + + +def storage_cleaner(chat_id: int) -> None: + """" + Функция которая по chat_id группы пользователей из таблицы 'temp_storage' удаляет все записи. + :param int chat_id: уникальный id записи о пользователе в таблице 'users' + :return: None + """ + with sqlite3.connect((DB)) as conn: + cursor = conn.cursor() + cursor.execute(f'''DELETE FROM 'temp_storage' WHERE chat_id={chat_id};''') + + +def storage_cleaner_lite(bot_message_id: int) -> None: + """" + Функция которая по bot_message_id сообщения бота из таблицы 'temp_storage' удаляет запись. + :param int bot_message_id: уникальный id сообщения бота + :return: None + """ + with sqlite3.connect((DB)) as conn: + cursor = conn.cursor() + cursor.execute(f'''DELETE FROM 'temp_storage' WHERE bot_message_id={bot_message_id};''') + + +""""Блок функций для настроек и просмотра админки""" + + +def insert_to_groups(moderator_id: int, group_id: int) -> None: + """ + Функция, которая записывает id группы модераторов и групп пользователей в таблицу groups_relation (по связи многие + ко многим; одной записи соответствует одна пара). + :param moderator_id: id группы модераторов + :param group_id: id группы пользователей + :return: None + """ + with sqlite3.connect((DB)) as conn: + cursor = conn.cursor() + cursor.execute( + """INSERT INTO 'groups_relation' (moderator_id, group_id) + VALUES (?, ?);""", + (moderator_id, group_id) + ) + + +def select_from_groups() -> List[tuple]: + """ + Генератор, который из таблицы groups_relation возвращает данные о записях id групп модераторов и пользователей, + сгруппированные по id групп модераторов. + :return List[tuple]: id групп модераторов и пользователей + """ + with sqlite3.connect((DB)) as conn: + cursor = conn.cursor() + cursor.execute("SELECT DISTINCT moderator_id " + "FROM 'groups_relation'") + moderator_id_list = cursor.fetchall() + for moderator_id in moderator_id_list: + cursor.execute(f"SELECT moderator_id, group_id " + f"FROM 'groups_relation'" + f"WHERE moderator_id={moderator_id[0]}") + result = cursor.fetchall() + yield result + + +def delete_from_groups() -> None: + """ + Функция, которая полностью очищает таблицу groups_relation. + :return: None + """ + with sqlite3.connect((DB)) as conn: + cursor = conn.cursor() + cursor.execute("DELETE FROM 'groups_relation'") + + +def get_all_moderator_id() -> List[int]: + """ + Функция, которая возвращает список всех id групп модераторов из таблицы groups_relation. + :return List[int]: список id групп модераторов + """ + with sqlite3.connect((DB)) as conn: + cursor = conn.cursor() + cursor.execute("SELECT DISTINCT moderator_id FROM 'groups_relation'") + moderator_id_list = cursor.fetchall() + result = [] + for moderator_id in moderator_id_list: + result.append(moderator_id[0]) + return result + + +"блок функций для вывода всех кто в юбилейном списке и для поздравления последних непоздравленных пользователей" + + +def select_lucky(moderator_id: int) -> list: + """ + Функция, которая возвращает chat_name, user_name, nickname, congr_number, dtime_connetion, is_winner, chat_id + из таблицы users по id группы модератора. + :param int moderator_id: уникальный id группы модератора + :return list: список информации обо всех зафиксированных пользователях + """ + with sqlite3.connect((DB)) as conn: + cursor = conn.cursor() + cursor.execute(f"""SELECT chat_name, user_name, nickname, congr_number, dtime_connetion, is_winner, chat_id + FROM 'users' JOIN 'groups_relation' + ON chat_id = group_id AND moderator_id ={moderator_id};""") + result = cursor.fetchall() + return result + + +def get_group_id(moderator_id: int) -> List[int]: + """ + Функция, которая возвращает список id групп пользователей из таблицы groups_relation по id группы модератора. + :param int moderator_id: уникальный id группы модератора + :return List[int]: список id групп пользователей + """ + with sqlite3.connect((DB)) as conn: + cursor = conn.cursor() + cursor.execute(f"SELECT DISTINCT group_id FROM 'groups_relation' WHERE moderator_id={moderator_id}") + group_id_list = cursor.fetchall() + result = [] + for group_id in group_id_list: + result.append(group_id[0]) + return result + + +def select_last_congr_number_from_users(chat_id: int) -> int: + """" + Функция которая по chat_id группы пользователя возвращает congr_number последней записи из таблицы 'users'. + :param int chat_id: уникальный id пользователя + :return int: congr_number последней записи + """ + with sqlite3.connect((DB)) as conn: + cursor = conn.cursor() + cursor.execute(f"SELECT congr_number FROM 'users' WHERE chat_id={chat_id}") + record_id = cursor.fetchall() + if record_id: + return record_id[-1][0] + return 0 + + +def data_finder_unceleb(bot_message_id: int) -> list: + """" + Функция которая по bot_message_id сообщения бота возвращает user_name, congr_number, chat_id из таблицы 'users'. + :param int bot_message_id: уникальный id сообщения бота + :return list: информация о победителе + """ + with sqlite3.connect((DB)) as conn: + cursor = conn.cursor() + result = cursor.execute(f'''SELECT users.user_name, users.congr_number, users.chat_id FROM users + Join temp_unceleb ON users.id=temp_unceleb.record_id + WHERE temp_unceleb.bot_message_id={bot_message_id};''') + data = [] + for i in result: + data.append(i) + return data + + +def is_uncongr(congr_number: int, chat_id: int) -> bool: + """" + Функция которая проверяет, есть ли в таблице users для данного chat_id группы и данного юбилейного номера, + непоздравленные пользователи. + :param int congr_number: юбилейный номер пользователя + :param int chat_id: уникальный id группы + :return bool: True если последние пользователи не поздравлены, False если последние пользователи поздравлены + """ + lucky_number = congr_number - congr_number % HAPPY_NUMBER + with sqlite3.connect((DB)) as conn: + cursor = conn.cursor() + cursor.execute(f'''SELECT is_winner + FROM 'users' WHERE chat_id={chat_id} AND + (congr_number BETWEEN {lucky_number} AND {lucky_number + 2})''') + winners_list = cursor.fetchall() + for winner in winners_list: + if winner[0] == 1: + return False + return True + + +def select_last_uncongratulate(congr_number: int, chat_id: int) -> list: + """" + Функция которая проверяет, есть ли в таблице users для данного chat_id группы и данного юбилейного номера, + непоздравленные пользователи. + :param int congr_number: юбилейный номер пользователя + :param int chat_id: уникальный id группы + :return list: информация о победителях + """ + lucky_number = congr_number - congr_number % HAPPY_NUMBER + with sqlite3.connect((DB)) as conn: + cursor = conn.cursor() + cursor.execute(f'''SELECT chat_id, congr_number, nickname, user_name, chat_name, dtime_connetion, id + FROM 'users' WHERE chat_id={chat_id} AND + (congr_number BETWEEN {lucky_number} AND {lucky_number + 2})''') + winners_list = cursor.fetchall() + result = [] + for winner in winners_list: + result.append(winner) + return result + + +def temp_save_unceleb(chat_id: int, record_id: int, bot_message_id: int) -> None: + """ + Функция, которая записывает chat_id, record_id, bot_message_id в таблицу 'temp_unceleb'. + :param int chat_id: id группы пользователей + :param int record_id: id записи о пользователе + :param int bot_message_id: id сообщения бота + :return: None + """ + with sqlite3.connect((DB)) as conn: + cursor = conn.cursor() + cursor.execute(""" + INSERT INTO 'temp_unceleb' (chat_id, record_id, bot_message_id) VALUES (?, ?, ?); + """, (chat_id, record_id, bot_message_id)) + + +def temp_cleaner() -> None: + """ + Функция, которая очищает таблицу 'temp_unceleb'. + :return: None + """ + with sqlite3.connect((DB)) as conn: + cursor = conn.cursor() + cursor.execute(f'''DELETE FROM 'temp_unceleb';''') + + +def get_chat_id_unceleb(bot_message_id) -> int: + """ + Функция, которая по bot_message_id сообщения бота возвращает id группы пользователя. + :param int bot_message_id: id сообщения бота + :return int: id группы пользователя + """ + with sqlite3.connect((DB)) as conn: + cursor = conn.cursor() + cursor.execute(f"SELECT chat_id FROM 'temp_unceleb' WHERE bot_message_id={bot_message_id}") + result = cursor.fetchall() + return result[0][0] + + +def is_winner_id_select_unceleb(bot_message_id: int) -> int: + """ + Функция, которая по bot_message_id сообщения бота возвращает id записи о пользователе. + :param int bot_message_id: id сообщения бота + :return int: id записи о пользователе + """ + with sqlite3.connect((DB)) as conn: + cursor = conn.cursor() + result = cursor.execute(f'''SELECT id FROM users Join temp_unceleb ON id=record_id + WHERE bot_message_id={bot_message_id};''') + id = [] + for i in result: + id.append(i) + return id[0][0] + + +def buttons_remover_unceleb(chat_id: int) -> List[int]: + """ + Функция, которая по chat_id группы пользователя возвращает список bot_message_id сообщений бота из + таблицы 'temp_unceleb'. + :param int chat_id: id группы пользователя + :return List[int]: список bot_message_id сообщений бота + """ + with sqlite3.connect((DB)) as conn: + cursor = conn.cursor() + cursor.execute(f"SELECT bot_message_id FROM 'temp_unceleb' WHERE chat_id={chat_id}") + result = cursor.fetchall() + delete_list = [] + for i in result: + delete_list.append(i) + return delete_list + + +def storage_cleaner_unceleb(chat_id: int) -> None: + """ + Функция, которая по chat_id группы пользователя очищает таблицу 'temp_unceleb'. + :param int chat_id: id группы пользователя + :return: None + """ + with sqlite3.connect((DB)) as conn: + cursor = conn.cursor() + cursor.execute(f'''DELETE FROM 'temp_unceleb' WHERE chat_id={chat_id};''') + + +def record_cleaner_unceleb(bot_message_id: int) -> None: + """ + Функция, которая по bot_message_id сообщения бота очищает запись в таблице 'temp_unceleb'. + :param int bot_message_id: id сообщения бота + :return: None + """ + with sqlite3.connect((DB)) as conn: + cursor = conn.cursor() + cursor.execute(f'''DELETE FROM 'temp_unceleb' WHERE bot_message_id={bot_message_id};''') diff --git a/database/database.db b/database/database.db new file mode 100644 index 0000000..1881473 Binary files /dev/null and b/database/database.db differ diff --git a/handlers/__init__.py b/handlers/__init__.py new file mode 100644 index 0000000..63a7946 --- /dev/null +++ b/handlers/__init__.py @@ -0,0 +1,5 @@ +from . import default_heandlers +from . import register_new_user +from . import lucky_list +from . import admin +from . import last_uncongratulate diff --git a/handlers/admin.py b/handlers/admin.py new file mode 100644 index 0000000..d680325 --- /dev/null +++ b/handlers/admin.py @@ -0,0 +1,132 @@ +from telebot.types import Message, CallbackQuery +from loader import bot +from database.commands import get_all_moderator_id, select_from_groups, insert_to_groups, delete_from_groups +from keyboards.inline import admin_keyboard +from config_data.config import ADMIN_IDS + + +@bot.message_handler(commands=['adminshow']) +def bot_admin_show(message: Message) -> None: + """ + Функция, которая выводит текущие настройки админки с группировкой по id группы модератора. Команда доступна только + в чатах модераторов, которые настроены командой /adminsetup и пользователям с правами администратора. + :param Message message: /adminshow + :return: None + """ + moderator_ids = get_all_moderator_id() + + if (message.chat.id in moderator_ids) or (str(message.from_user.id) in ADMIN_IDS): + admin_info = select_from_groups() + + if admin_info: + bot.send_message(chat_id=message.chat.id, text="Текущие настройки админки в формате\n" + "id Группа модераторов: id Группа пользователей") + for i_info in admin_info: + bot_text = f"{i_info[0][0]}: " + for j_info in i_info: + bot_text = f"{bot_text}{j_info[1]} " + bot.send_message(chat_id=message.chat.id, text=bot_text) + else: + bot.send_message(chat_id=message.chat.id, + text="Настройки еще не выставлены.") + + +@bot.message_handler(commands=['adminsetup']) +def bot_admin_setup(message: Message) -> None: + """ + Функция, которая запрашивает подтверждение смены текущих настроек админки. Доступна только пользователям с правами + администрирования. + :param Message message: /adminsetup + :return: None + """ + moderator_ids = get_all_moderator_id() + + if str(message.from_user.id) in ADMIN_IDS: + bot.send_message(chat_id=message.chat.id, + text="Пожалуйста, подтвердите, что вы действительно хотите поменять настройки админки. " + "Это приведет к полному удалению текущих настроек и отменить это действие будет " + "невозможно!", + reply_markup=admin_keyboard.yes_no_proceed_keyboard()) + elif message.chat.id in moderator_ids: + bot.send_message(chat_id=message.chat.id, + text=f"Ошибка доступа: эта команда доступна только пользователям с правами " + f"администрирования. Доступ есть у пользователей с такими user_id:\n{ADMIN_IDS}") + + +def process_admin_input(message: Message) -> None: + """ + Функция, которая сохраняет введенные администратором id групп модераторов и пользователей. + :param Message message: id групп модератора и его пользователей через пробелы + :return: None + """ + groups = message.text.split(" ") + + for i_group in groups: + if not (i_group.startswith("-") and i_group[1:].isdigit()): + msg = bot.send_message(chat_id=message.chat.id, + text="Ошибка: id_группы - это отрицательное число. Попробуйте еще раз. Формат" + " ввода:\n id_ГМ id_ГП_1 id_ГП_2 id_ГП_3") + bot.register_next_step_handler(message=msg, callback=process_admin_input) + return + + for i in range(len(groups) - 1): + insert_to_groups(moderator_id=int(groups[0]), group_id=int(groups[i + 1])) + + bot.send_message(chat_id=message.chat.id, + text="Сделано! Хотите добавить еще группы?", + reply_markup=admin_keyboard.yes_no_add_keyboard()) + + +@bot.callback_query_handler(func=lambda call: call.data == "yes_admin_proceed") +def callback_query(call: CallbackQuery) -> None: + """ + Колбек, обрабатывайщий подтверждение, что администратор действитель хочет поменять настройки админки. Сбрасывает + текущие настройки и перенаправляет на обработчик process_admin_input. + :param CallbackQuery call: yes_admin_proceed + :return: None + """ + delete_from_groups() + msg = bot.edit_message_text(chat_id=call.message.chat.id, + message_id=call.message.message_id, + text="Добро пожаловать в меню админки. Добавьте группы пользователей (ГП), из которых " + "будут приходить уведомления в выбранную группу модераторов (ГМ). Формат ввода:\n" + "id_ГМ id_ГП_1 id_ГП_2 id_ГП_3") + bot.register_next_step_handler(msg, process_admin_input) + + +@bot.callback_query_handler(func=lambda call: call.data == "no_admin_proceed") +def callback_query(call: CallbackQuery) -> None: + """ + Колбек, обрабатывайщий отказ администратора поменять настройки админки. + :param CallbackQuery call: no_admin_proceed + :return: None + """ + bot.edit_message_text(chat_id=call.message.chat.id, + message_id=call.message.message_id, + text="Вы отказались менять настройки админки.") + + +@bot.callback_query_handler(func=lambda call: call.data == "yes_admin_add") +def callback_query(call: CallbackQuery) -> None: + """ + Колбек, обрабатывайщий необходимость ввести еще группы в меню админки. Перенаправляет на обработчик + process_admin_input. + :param CallbackQuery call: yes_admin_add + :return: None + """ + msg = bot.edit_message_text(chat_id=call.message.chat.id, + message_id=call.message.message_id, + text="Формат ввода:\n id_ГМ id_ГП_1 id_ГП_2 id_ГП_3") + bot.register_next_step_handler(msg, process_admin_input) + + +@bot.callback_query_handler(func=lambda call: call.data == "no_admin_add") +def callback_query(call: CallbackQuery) -> None: + """ + Колбек, обрабатывайщий окончание ввода групп в меню админки. + :param CallbackQuery call: no_admin_add + :return: None + """ + bot.edit_message_text(chat_id=call.message.chat.id, + message_id=call.message.message_id, + text="Сделано! Чтобы посмотреть все текущие настройки админки введите команду /adminshow") diff --git a/handlers/default_heandlers/__init__.py b/handlers/default_heandlers/__init__.py new file mode 100644 index 0000000..91e8acb --- /dev/null +++ b/handlers/default_heandlers/__init__.py @@ -0,0 +1,2 @@ +from . import start +from . import help diff --git a/handlers/default_heandlers/help.py b/handlers/default_heandlers/help.py new file mode 100644 index 0000000..8b2cdd0 --- /dev/null +++ b/handlers/default_heandlers/help.py @@ -0,0 +1,20 @@ +from telebot.types import Message +from config_data.config import DEFAULT_COMMANDS +from loader import bot +from database.commands import get_all_moderator_id + + +@bot.message_handler(commands=['help']) +def bot_help(message: Message): + """ + Хендлер, который выводит все доступные боту команды при команде /help. Команда доступна только в чатах модераторов, + которые настроены командой /adminsetup. + :param Message message: /help + :return: None + """ + moderator_ids = get_all_moderator_id() + + if message.chat.id in moderator_ids: + text = [f'/{command} - {desk}' for command, desk in DEFAULT_COMMANDS] + bot.send_message(chat_id=message.chat.id, + text='\n'.join(text)) diff --git a/handlers/default_heandlers/start.py b/handlers/default_heandlers/start.py new file mode 100644 index 0000000..3a7d9a2 --- /dev/null +++ b/handlers/default_heandlers/start.py @@ -0,0 +1,23 @@ +from telebot.types import Message +from loader import bot +from config_data.config import ADMIN_IDS +from database.commands import get_all_moderator_id + + +@bot.message_handler(commands=['start']) +def bot_start(message: Message): + """ + Хендлер, который приветствует пользователя и подсказывает, что делать при команде /start. Команда доступна только + в чатах модераторов, которые настроены командой /adminsetup и пользователям с правами администратора. + :param Message message: /start + :return: None + """ + moderator_ids = get_all_moderator_id() + + if (message.chat.id in moderator_ids) or (str(message.from_user.id) in ADMIN_IDS): + bot.send_message(chat_id=message.chat.id, + text=f"Привет, {message.from_user.full_name}!\n" + f"Чтобы узнать, что я могу, введи команду /help\n\n" + f"При первом запуске администратору следует выставить настройки админки по " + f"команде /adminsetup в личном чате со мной (доступ есть у пользователей с " + f"такими user_id:\n{ADMIN_IDS})") diff --git a/handlers/last_uncongratulate.py b/handlers/last_uncongratulate.py new file mode 100644 index 0000000..e598f34 --- /dev/null +++ b/handlers/last_uncongratulate.py @@ -0,0 +1,107 @@ +from telebot.types import Message, CallbackQuery +from loader import bot +from database.commands import temp_cleaner, select_last_uncongratulate, get_all_moderator_id, \ + temp_save_unceleb, get_chat_id_unceleb, is_winner_id_select_unceleb, is_winner_record, buttons_remover_unceleb, \ + storage_cleaner_unceleb, record_cleaner_unceleb, data_finder_unceleb, get_group_id, \ + select_last_congr_number_from_users, is_uncongr +import datetime +from keyboards.inline import unceleb_keyboard +from config_data.config import HAPPY_NUMBER + + +@bot.message_handler(commands=['unceleb']) +def bot_uncongratulate(message: Message) -> None: + """" + Хендлер, который обрабатывает команду /unceleb по выводу последних непоздравленных пользователей. Команда доступна + только в чатах модераторов, которые настроены командой /adminsetup. + :param Message message: /unceleb + :return: None + """ + moderator_ids = get_all_moderator_id() + + if message.chat.id in moderator_ids: + # чистим таблицу temp_unceleb + temp_cleaner() + + # получаем id всех групп пользователей для этой группы модераторов + group_id_list = get_group_id(moderator_id=message.chat.id) + + # получаем список всех последних непоздравленных пользователей + uncelebs_list = [] + + # для каждой группы пользователей находим номер последнего зарегистрированного пользователя + for group_id in group_id_list: + last_congr_number = select_last_congr_number_from_users(chat_id=group_id) + + # если в группе зарегистрированы пользователи, проверяем есть ли победитель для последнего юбилейного номера + if last_congr_number: + if is_uncongr(congr_number=last_congr_number, chat_id=group_id): + + # если победителя нет, то мы добавляем всех последних непоздравленных пользователей для этой + # группы в общий список последних непоздравленных пользователей + last_uncong_users = select_last_uncongratulate(congr_number=last_congr_number, chat_id=group_id) + for user in last_uncong_users: + uncelebs_list.append(user) + + # выводим информацию о последних непоздравленных пользователях, предлагаем их поздравить и + # сохраняем данные в таблицу temp_unceleb + if uncelebs_list: + for unceleb in uncelebs_list: + dtime = datetime.datetime.strptime(unceleb[5], '%Y-%m-%d %H:%M:%S.%f').strftime('%d.%m.%y %H:%M') + + bot_message = bot.send_message(chat_id=message.chat.id, + text=f'\U0001F389 "{unceleb[4]}" \U0001F464 {unceleb[3]} ' + f'(@{unceleb[2]})\n\U0001F522 {unceleb[1]} \U0001F550 {dtime}', + reply_markup=unceleb_keyboard.congratulate_keyboard()) + + temp_save_unceleb(chat_id=unceleb[0], + record_id=unceleb[6], + bot_message_id=bot_message.id) + + else: + bot.send_message(chat_id=message.chat.id, text=f'В списке последних непоздравленных никого нет!') + + +@bot.callback_query_handler(func=lambda call: call.data == "congr" or call.data == "uncongr") +def callback(call: CallbackQuery) -> None: + """ + Колбек, обрабатывайщий нажатие кнопки Поздравить или Отклонить в команде /unceleb. + :param CallbackQuery call: congr or uncongr + :return: None + """ + user_data = data_finder_unceleb(bot_message_id=call.message.message_id)[0] + name = user_data[0] + congr_number = user_data[1] + users_chat = user_data[2] + moders_chat = call.message.chat.id + + if call.data == 'congr': + winner_chat_id = get_chat_id_unceleb(bot_message_id=call.message.message_id) + winner_id = is_winner_id_select_unceleb(bot_message_id=call.message.message_id) + + # записываем, что пользователь выиграл + is_winner_record(winner_id=winner_id) + + # удаляем все остальные сообщения бота и чистим информацию о сообщениях в базе данных таблицы temp_storage + remove_list = buttons_remover_unceleb(chat_id=winner_chat_id) + for message in remove_list: + bot.delete_message(chat_id=call.message.chat.id, message_id=message) + storage_cleaner_unceleb(chat_id=call.message.chat.id) + + # поздравляем победителя в группе пользователей + bot.send_message(chat_id=users_chat, + text=f'\U0001F389 Поздравляю, {name}, как же удачно попали в нужное время!\n' + f'Вы участник {congr_number - congr_number % HAPPY_NUMBER} ' + f'коммьюнити.\nВас ждут плюшки и печенюшки! \U0001F389') + + # уведомляем группу модераторов о поздравлении победителя + bot.send_message(chat_id=moders_chat, + text=f'Участник {name} поздравлен! \U0001F389') + + else: + # удаляем сообщение бота и чистим информацию о сообщении в базе данных таблицы temp_unceleb + bot.delete_message(chat_id=call.message.chat.id, message_id=call.message.message_id) + record_cleaner_unceleb(bot_message_id=call.message.message_id) + + # уведомляем группу модераторов о том, что участника не поздравили + bot.send_message(chat_id=moders_chat, text=f'Участника {name} не поздравили.') diff --git a/handlers/lucky_list.py b/handlers/lucky_list.py new file mode 100644 index 0000000..69f25b6 --- /dev/null +++ b/handlers/lucky_list.py @@ -0,0 +1,33 @@ +from telebot.types import Message +from loader import bot +from database.commands import get_all_moderator_id, select_lucky +import datetime + + +@bot.message_handler(commands=['luckylist']) +def bot_lucky_list(message: Message) -> None: + """" + Хендлер, который обрабатывает команду /luckylist по выводу всех зарегитрированных юбилейных пользователей. + Команда доступна только в чатах модераторов, которые настроены командой /adminsetup. + :param Message message: /luckylist + :return: None + """ + moderator_ids = get_all_moderator_id() + + if message.chat.id in moderator_ids: + winners = select_lucky(moderator_id=message.chat.id) + + if winners: + for winner in winners: + dtime = datetime.datetime.strptime(winner[4], '%Y-%m-%d %H:%M:%S.%f').strftime('%d.%m.%y %H:%M') + if winner[5] == 1: + bot.send_message(chat_id=message.chat.id, + text=f'\U0001F451\U0001F451\U0001F451 "{winner[0]}" \U0001F464 {winner[1]} ' + f'(@{winner[2]})\n\U0001F522 {winner[3]} \U0001F550 {dtime}') + else: + bot.send_message(chat_id=message.chat.id, + text=f'\U0001F389 "{winner[0]}" \U0001F464 {winner[1]} ' + f'(@{winner[2]})\n\U0001F522 {winner[3]} \U0001F550 {dtime}') + + else: + bot.send_message(chat_id=message.chat.id, text=f'В списке юбилейных пользователей никого нет!') diff --git a/handlers/register_new_user.py b/handlers/register_new_user.py new file mode 100644 index 0000000..5f9af90 --- /dev/null +++ b/handlers/register_new_user.py @@ -0,0 +1,98 @@ +from loader import bot +from database.commands import insert_to_users +import datetime +from database.commands import winner_check, select_id_from_users,\ + temp_save, buttons_remover, storage_cleaner, storage_cleaner_lite, is_winner_id_select, \ + is_winner_record, other_lucky_check, data_finder, get_moderator_id, get_all_group_id +from telebot.types import Message, CallbackQuery +from keyboards.inline import new_user_keyboard +from config_data.config import HAPPY_NUMBER + + +@bot.message_handler(content_types=['new_chat_members']) +def handler_new_member(message: Message) -> None: + """" + Хендлер, который обрабатывает вступление в группу пользователей нового пользователя. + :param Message message: сообщение о вступлении в группу нового пользователя + :return: None + """ + group_ids = get_all_group_id() + + if message.chat.id in group_ids: + count = bot.get_chat_members_count(message.chat.id) + user_id = message.from_user.id + chat_name = message.chat.title + nickname = message.from_user.username + user_name = message.from_user.first_name + dtime = datetime.datetime.now() + + # проверка на то, что новый пользователь не является ботом, еще не был победителем, + # а также имеет юбилейный номер вступления + if (not message.from_user.is_bot) and (not winner_check(user_id=user_id)) and \ + (count % HAPPY_NUMBER == 0 or count % HAPPY_NUMBER == 1 or count % HAPPY_NUMBER == 2): + + # если все условия выполнены, записываем пользователя в базу в таблицу users + insert_to_users(nickname=nickname, user_name=user_name, congr_number=count, chat_name=chat_name, + user_id=user_id, dtime_connection=dtime, chat_id=message.chat.id, is_winner=0) + + # проверка, что для юбилейного номера нового пользователя в этом чате еще никого не поздравили + if not other_lucky_check(count_users=count, chat_id=message.chat.id): + + # если условие выполнено, предлагаем модераторам поздравить пользователя и + # сохраняем данные в таблицу temp_storage + moderator_ids = get_moderator_id(group_id=message.chat.id) + + for moderator_id in moderator_ids: + dtime_formatted = dtime.strftime('%d.%m.%y %H:%M') + bot_message = bot.send_message(chat_id=moderator_id, + text=f'В {chat_name} вступил юбилейный пользователь {nickname} ' + f'{user_name}\nПорядковый номер вступления: {count}, время ' + f'вступления: {dtime_formatted}', + reply_markup=new_user_keyboard.congratulate_keyboard()) + + temp_save(chat_id=message.chat.id, + record_id=select_id_from_users(user_id=message.from_user.id), + bot_message_id=bot_message.id) + + +@bot.callback_query_handler(func=lambda call: call.data == "grac" or call.data == "decline") +def callback(call: CallbackQuery) -> None: + """ + Колбек, обрабатывайщий нажатие кнопки Поздравить или Отклонить в группе модераторов. + :param CallbackQuery call: grac or decline + :return: None + """ + winner_id = is_winner_id_select(bot_message_id=call.message.message_id) + user_data = data_finder(bot_message_id=call.message.message_id)[0] + name = user_data[0] + congr_number = user_data[1] + users_chat = user_data[2] + moders_chat = call.message.chat.id + + if call.data == 'grac': + # записываем, что пользователь выиграл + is_winner_record(winner_id=winner_id) + + # удаляем все остальные сообщения бота и чистим информацию о сообщениях в базе данных таблицы temp_storage + remove_list = buttons_remover(chat_id=users_chat) + for message in remove_list: + bot.delete_message(chat_id=moders_chat, message_id=message) + storage_cleaner(chat_id=users_chat) + + # поздравляем победителя в группе пользователей + bot.send_message(chat_id=users_chat, + text=f'\U0001F389 Поздравляю, {name}, как же удачно попали в нужное время!\n' + f'Вы участник {congr_number - congr_number % HAPPY_NUMBER} ' + f'коммьюнити.\nВас ждут плюшки и печенюшки! \U0001F389') + + # уведомляем группу модераторов о поздравлении победителя + bot.send_message(chat_id=moders_chat, + text=f'Участник {name} поздравлен! \U0001F389') + + else: + # удаляем сообщение бота и чистим информацию о сообщении в базе данных таблицы temp_storage + bot.delete_message(chat_id=moders_chat, message_id=call.message.message_id) + storage_cleaner_lite(bot_message_id=call.message.message_id) + + # уведомляем группу модераторов о том, что участника не поздравили + bot.send_message(moders_chat, f'Участника {name} не поздравили.') diff --git a/keyboards/__init__.py b/keyboards/__init__.py new file mode 100644 index 0000000..3c54f1c --- /dev/null +++ b/keyboards/__init__.py @@ -0,0 +1,2 @@ + +from . import inline diff --git a/keyboards/inline/__init__.py b/keyboards/inline/__init__.py new file mode 100644 index 0000000..2cdb5ee --- /dev/null +++ b/keyboards/inline/__init__.py @@ -0,0 +1,3 @@ +from . import admin_keyboard +from . import new_user_keyboard +from . import unceleb_keyboard diff --git a/keyboards/inline/admin_keyboard.py b/keyboards/inline/admin_keyboard.py new file mode 100644 index 0000000..62da4bc --- /dev/null +++ b/keyboards/inline/admin_keyboard.py @@ -0,0 +1,25 @@ +from telebot.types import InlineKeyboardMarkup, InlineKeyboardButton + + +def yes_no_add_keyboard() -> InlineKeyboardMarkup: + """ + Функция, которая генерирует инлайн-клавиатуру "да-нет" для меню админки. + :return InlineKeyboardMarkup: + """ + + keyboard = InlineKeyboardMarkup() + keyboard.add(InlineKeyboardButton(text="да", callback_data="yes_admin_add"), + InlineKeyboardButton(text="нет", callback_data="no_admin_add")) + return keyboard + + +def yes_no_proceed_keyboard() -> InlineKeyboardMarkup: + """ + Функция, которая запрашивает подтверждение, что администратор действитель хочет поменять настройки админки. + :return InlineKeyboardMarkup: + """ + + keyboard = InlineKeyboardMarkup() + keyboard.add(InlineKeyboardButton(text="да", callback_data="yes_admin_proceed"), + InlineKeyboardButton(text="нет", callback_data="no_admin_proceed")) + return keyboard diff --git a/keyboards/inline/new_user_keyboard.py b/keyboards/inline/new_user_keyboard.py new file mode 100644 index 0000000..e39f15f --- /dev/null +++ b/keyboards/inline/new_user_keyboard.py @@ -0,0 +1,14 @@ +from telebot.types import InlineKeyboardMarkup, InlineKeyboardButton + + +def congratulate_keyboard() -> InlineKeyboardMarkup: + """ + Функция, которая генерирует инлайн-клавиатуру "Поздравить/Отклонить" для поздравления юбилейного пользователя. + :return InlineKeyboardMarkup: + """ + + keyboard = InlineKeyboardMarkup(row_width=1) + congratulations = InlineKeyboardButton(text='Поздравить', callback_data='grac') + shame = InlineKeyboardButton(text='Отклонить', callback_data='decline') + keyboard.add(congratulations, shame) + return keyboard diff --git a/keyboards/inline/unceleb_keyboard.py b/keyboards/inline/unceleb_keyboard.py new file mode 100644 index 0000000..81137a1 --- /dev/null +++ b/keyboards/inline/unceleb_keyboard.py @@ -0,0 +1,15 @@ +from telebot.types import InlineKeyboardMarkup, InlineKeyboardButton + + +def congratulate_keyboard() -> InlineKeyboardMarkup: + """ + Функция, которая генерирует инлайн-клавиатуру "Поздравить/Отклонить" для поздравления пользователей из + команды /unceleb. + :return InlineKeyboardMarkup: + """ + + keyboard = InlineKeyboardMarkup(row_width=1) + congr = InlineKeyboardButton(text='Поздравить', callback_data='congr') + uncongr = InlineKeyboardButton(text='Отклонить', callback_data='uncongr') + keyboard.add(congr, uncongr) + return keyboard diff --git a/keyboards/reply/__init__.py b/keyboards/reply/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/loader.py b/loader.py new file mode 100644 index 0000000..9825b10 --- /dev/null +++ b/loader.py @@ -0,0 +1,7 @@ +from telebot import TeleBot +from telebot.storage import StateMemoryStorage +from config_data import config + +storage = StateMemoryStorage() +bot = TeleBot(token=config.BOT_TOKEN, state_storage=storage) + diff --git a/main.py b/main.py new file mode 100644 index 0000000..dfcda83 --- /dev/null +++ b/main.py @@ -0,0 +1,8 @@ +from loader import bot +import handlers +from utils.set_bot_commands import set_default_commands + + +if __name__ == '__main__': + set_default_commands(bot) + bot.infinity_polling() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..03cac72 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +pyTelegramBotAPI==4.4.0 +python-dotenv==0.19.2 +emoji==1.7.0 \ No newline at end of file diff --git a/states/README.md b/states/README.md new file mode 100644 index 0000000..c99fc8d --- /dev/null +++ b/states/README.md @@ -0,0 +1,5 @@ +# Состояния пользователя внутри сценария +Чтобы не писать собственные классы для хранения полученной информации, +куда проще использовать реализацию состояний пользователя внутри сценария. +> Пример можно найти в репозитории [ссылка](https://github.com/eternnoir/pyTelegramBotAPI/blob/master/examples/custom_states.py) + diff --git a/states/__init__.py b/states/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..ab665e1 --- /dev/null +++ b/utils/__init__.py @@ -0,0 +1 @@ +from . import misc diff --git a/utils/misc/__init__.py b/utils/misc/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/utils/misc/__init__.py @@ -0,0 +1 @@ + diff --git a/utils/set_bot_commands.py b/utils/set_bot_commands.py new file mode 100644 index 0000000..6494889 --- /dev/null +++ b/utils/set_bot_commands.py @@ -0,0 +1,8 @@ +from telebot.types import BotCommand +from config_data.config import DEFAULT_COMMANDS + + +def set_default_commands(bot): + bot.set_my_commands( + [BotCommand(*i) for i in DEFAULT_COMMANDS] + )