diff --git a/.gitignore b/.gitignore index db090aa..f21f1b3 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,9 @@ !app/* !/app/plugin -!app/plugin/* \ No newline at end of file +!app/plugin/* + +app/bot.py + +__pycache__/ +.env \ No newline at end of file diff --git a/app/config.py b/app/config.py index 4958e15..29e5ced 100644 --- a/app/config.py +++ b/app/config.py @@ -7,8 +7,11 @@ load_dotenv() class config: - DATE_START = datetime.strptime(os.environ.get("DATE_START"), "%Y-%m-%d").date() - BOT_TOKEN = os.environ.get("API_TOKEN") + DATE_START = datetime.strptime( + f'{os.environ.get("DATE_START")}-{os.environ.get("TIME_START")}', + "%Y-%m-%d-%H:%M", + ) + API_TOKEN = os.environ.get("API_TOKEN") ADMIN_ID = os.environ.get("ADMIN_ID") ENV = os.getenv("ENV", "PROD") @@ -30,7 +33,11 @@ class Messages: WELCOME_LIST = [ "Привет! Это бот Quick dates (Random Coffee). Чтобы зарегистрироваться, пожалуйста, ответь на следующие вопросы." # несколько сообщений подряд ] - HELP = "Помощь" + HELPS = [ + "Доступные команды:" "\n/start - начать регистрацию" "n/help - помощь", + "Начало мероприятия: {} \nЭто через целых {} минут!", + ] + ERROR = "Произошла ошибка" ENTER_NAME = "Как тебя зовут?" ENTER_AGE = "Сколько тебе лет?" @@ -47,3 +54,6 @@ class Messages: \nГруппа: {}\ \n\nВсё верно?" ALREADY_REGISTERED = "Ты уже зарегистрирован. Хочешь изменить данные?" + MATCHING_START = ( + "Мы начали подбор пар. Пожалуйста, подтверди, что ты находишься в аудитории" + ) diff --git a/app/db.py b/app/db.py index 970effd..9ef5c3e 100644 --- a/app/db.py +++ b/app/db.py @@ -15,6 +15,9 @@ class DB: def find(self, query): return self.collection.find(query) + def find_one(self, query): + return self.collection.find_one(query) + def update(self, query, data): self.collection.update_one(query, {"$set": data}) diff --git a/app/main.py b/app/main.py index 8181703..07d256d 100644 --- a/app/main.py +++ b/app/main.py @@ -8,6 +8,7 @@ from plugin.user import User from plugin.admin import Admin, AdminMessages from plugin.bot_instance import bot, BotCommands from plugin.register import start_reg_name +from datetime import datetime, timedelta import time @@ -61,14 +62,11 @@ def start_message(message: types.Message): @bot.message_handler(commands=BotCommands.Help) def help_message(message: types.Message): - bot.send_message( - message.chat.id, - ( - Messages.HELP - if not Admin.is_admin(message.chat.id) - else AdminMessages.ADMIN_HELP - ), - ) + if not Admin.is_admin(message.chat.id): + for msg in Messages.HELPS: + bot.send_message(message.chat.id, msg) + else: + bot.send_message(message.chat.id, AdminMessages.ADMIN_HELP, parse_mode="HTML") return @@ -76,7 +74,23 @@ def help_message(message: types.Message): if __name__ == "__main__": print("/t--- Bot started ---") + time_for_notif = config.DATE_START + while True: + if config.DATE_START - datetime.now() < timedelta(days=1): + users = User.get_all() + for user in users: + bot.send_message( + user["user_id"], + "Напоминание, что наше мероприятие пройдёт уже завтра! Следите за новостями)", + ) + + admins = Admin.get_all_admins() + for admin in admins: + bot.send_message( + admin["user_id"], + "Отправлено напоминание о скором начале мероприятия.", + ) time.sleep(10) exit() diff --git a/app/plugin/admin.py b/app/plugin/admin.py index 819a429..ebca9a6 100644 --- a/app/plugin/admin.py +++ b/app/plugin/admin.py @@ -1,7 +1,10 @@ from db import DB import config +import os +import threading +import time from plugin.user import User -from plugin.shuffle import Shuffle +from plugin.shuffle import Shuffle, already_matched from plugin.bot_instance import bot @@ -29,9 +32,13 @@ class Admin: return + @staticmethod + def get_all_admins(): + return Admin._db.find({}) + @staticmethod def is_admin(user_id): - return Admin._db.find_one({"user_id": user_id}) is not None + return Admin._db.find({"user_id": user_id}) is not None @staticmethod def add_admin(new_user_id, name, unique_key) -> int: @@ -206,8 +213,6 @@ class Admin: with open("admin_stats.txt", "rb") as f: bot.send_document(message.chat.id, f) - import os - os.remove("stats.txt") os.remove("admin_stats.txt") @@ -242,7 +247,7 @@ class Admin: def clear_all_command(message): bot.send_message( message.chat.id, - "Отправьте delete all для подтверждения", + "Отправьте delete all для подтверждения. \n\nВнимание, вы вряд ли хотите это делать.", parse_mode="HTML", ) @@ -263,8 +268,76 @@ class Admin: @bot.message_handler(commands=["random"]) @admin_only def random_command(message): - users = User.get_all() - Shuffle(users) + already_matched.clear() + + for admin in Admin.get_all_admins(): + bot.send_message( + admin["user_id"], + "Отправлено приглашение пользователем. В данный момент подтвердило участие 0 человек. Запускайте команду /end_random для завершения регистрации", + ) + + User.start_shuffle_reg() + + def monitor_participation(): + while True: + participants = len(already_matched) + bot.send_message( + message.chat.id, + f"В данный момент подтвердило участие {participants} человек", + ) + time.sleep(5) + + thread = threading.Thread( + target=monitor_participation, name="monitor_participation" + ) + thread.start() + + return + + @bot.message_handler(commands=["end_random", "random_again"]) + @admin_only + def end_random_command(message): + for thread in threading.enumerate(): + if thread.name == "monitor_participation": + thread.kill() + + bot.send_message(message.chat.id, "Регистрация завершена. Ожидайте") + + pairs = Shuffle() + for pair in pairs: + if pair[1]: + bot.send_message( + pair[0].user_id, + f"Ваша пара: {pair[1].name}. Парта находится под номером: {pairs.index(pair) + 1})", + ) + bot.send_message( + pair[1].user_id, + f"Ваша пара: {pair[0].name}. Парта находится под номером: {pairs.index(pair) + 1})", + ) + else: + bot.send_message( + pair[0].user_id, + "Подойди к организаторам мероприятия, они тебе подберут идеальную пару!", + ) + + with open("pairs.txt", "w") as f: + str_pair = "" + for pair in pairs: + str_pair += ( + f"Парта {pairs.index(pair) + 1}: {pair[0].name} | {pair[1].name}\n" + ) + + f.write(str_pair) + + with open("pairs.txt", "rb") as f: + for admin in Admin.get_all_admins(): + bot.send_document( + admin["user_id"], + f, + caption="Распредение по парам.", + ) + + os.remove("pairs.txt") return @@ -273,10 +346,11 @@ class AdminMessages: ADMIN_HELP = ( "Вы администратор! Вот что вы можете сделать:\n" "/remove_admin - удалить админа\n" - "/generate_key - сгенерировать уникальный ключ" - "/delete_user - удалить пользователя" - "/stats - статистика" - "/send_message - отправить всем сообщение" - "/random - зарандомить людей для 1 тура" - "clear_all - удаляет ВСЕХ пользователей" + "/generate_key - сгенерировать уникальный ключ\n" + "/delete_user - удалить пользователя\n" + "/stats - статистика\n" + "/send_message - отправить всем сообщение\n" + "/random - зарандомить людей для 1 тура\n" + "/random_again - зарандомить людей без повторной регистрации\n" + "clear_all - удаляет ВСЕХ пользователей для их повторной регистрации\n" ) diff --git a/app/plugin/bot_instance.py b/app/plugin/bot_instance.py index 6f77a77..5e12998 100644 --- a/app/plugin/bot_instance.py +++ b/app/plugin/bot_instance.py @@ -1,5 +1,5 @@ import telebot -import config +from config import config import threading @@ -9,7 +9,7 @@ class BotCommands: bot = telebot.TeleBot( - config.BOT_TOKEN, + config.API_TOKEN, colorful_logs=True, ) diff --git a/app/plugin/register.py b/app/plugin/register.py index 55d7426..404c1d3 100644 --- a/app/plugin/register.py +++ b/app/plugin/register.py @@ -4,6 +4,8 @@ from config import Messages from plugin.test import TestMessages from plugin.bot_instance import bot +import os +import re def start_reg_name(message: types.Message): @@ -159,11 +161,24 @@ def show_result(message: types.Message): TestMessages.TEST_RESULTS[user.type][0], TestMessages.TEST_RESULTS[user.type][1], ) - bot.send_photo( - message.chat.id, - open(f"pics/{user.type}.jpg", "rb"), - caption=text, - ) + + directory = "pics/" + pattern = re.compile(f"{user.type}.*\.(jpg|jpeg|png|gif)$", re.IGNORECASE) + + image_path = None + for filename in os.listdir(directory): + if pattern.match(filename): + image_path = os.path.join(directory, filename) + break + + if image_path: + bot.send_photo( + message.chat.id, + open(image_path, "rb"), + caption=text, + ) + else: + print("Image not found") bot.send_message(message.chat.id, TestMessages.TEST_FINISH) diff --git a/app/plugin/shuffle.py b/app/plugin/shuffle.py index 514b7fc..ced1092 100644 --- a/app/plugin/shuffle.py +++ b/app/plugin/shuffle.py @@ -1,6 +1,155 @@ -from plugin.user import User +from plugin.user import User, users_reg +from typing import List, Tuple +import random + +already_matched: List[Tuple[User, User]] = [] -def Shuffle(users: list[User]): +def Shuffle() -> List[Tuple[User, User]]: + users = users_reg.copy() + random.shuffle(users) + pairs = [] - return + if len(users) % 2 != 0: + pairs.append((users.pop(),)) + + # Step 1: Match men with women by compatibility and age + for user in users: + for potential_match in users: + if user == potential_match: + continue + if ( + user.gender != potential_match.gender + and potential_match.type in compatibility[user.type] + and abs(user.age - potential_match.age) <= 5 + and (user, potential_match) not in already_matched + and (potential_match, user) not in already_matched + ): + pairs.append((user, potential_match)) + users.remove(user) + users.remove(potential_match) + break + + # Step 2: Match men with women with compatibility + for user in users: + for potential_match in users: + if user == potential_match: + continue + if ( + user.gender != potential_match.gender + and potential_match.type in compatibility[user.type] + and (user, potential_match) not in already_matched + and (potential_match, user) not in already_matched + ): + pairs.append((user, potential_match)) + users.remove(user) + users.remove(potential_match) + break + + # Step 3: Match men with women + for user in users: + for potential_match in users: + if user == potential_match: + continue + if ( + user.gender != potential_match.gender + and (user, potential_match) not in already_matched + and (potential_match, user) not in already_matched + ): + pairs.append((user, potential_match)) + users.remove(user) + users.remove(potential_match) + break + + # Step 4: Match people of the same age + for user in users: + for potential_match in users: + if user == potential_match: + continue + if ( + user.age == potential_match.age + and (user, potential_match) not in already_matched + and (potential_match, user) not in already_matched + ): + pairs.append((user, potential_match)) + users.remove(user) + users.remove(potential_match) + break + + # Step 5: Pair all remaining users + while len(users) > 1: + pairs.append((users.pop(), users.pop())) + + already_matched.extend(pairs) + + return pairs + + +compatibility = { + "INTJ": ( + "ENFP", + "ENTP", + ), + "INTP": ( + "ENTJ", + "ESTJ", + ), + "INFJ": ( + "ENFP", + "ENTP", + ), + "INFP": ( + "ENFJ", + "ENTJ", + ), + "ISTJ": ( + "ESFP", + "ESTP", + ), + "ISTP": ( + "ESFJ", + "ESTJ", + ), + "ISFJ": ( + "ESFP", + "ESTP", + ), + "ISFP": ( + "ENFJ", + "ESFJ", + "ESTJ", + ), + "ENTJ": ( + "INFP", + "INTP", + ), + "ENTP": ( + "INTJ", + "ENTP", + ), + "ENFJ": ( + "INFP", + "ISFP", + "ENFJ", + ), + "ENFP": ( + "INFJ", + "INTJ", + ), + "ESTJ": ( + "ISFP", + "ISTP", + ), + "ESTP": ( + "ISFJ", + "ISTJ", + ), + "ESFJ": ( + "ISFP", + "ISTP", + ), + "ESFP": ( + "ISTJ", + "ISFJ", + ), +} diff --git a/app/plugin/user.py b/app/plugin/user.py index 110faae..e126433 100644 --- a/app/plugin/user.py +++ b/app/plugin/user.py @@ -1,4 +1,10 @@ from db import DB +from plugin.bot_instance import bot +from config import Messages +from telebot import types +from typing import List, Tuple + +users_reg: List["User"] = [] class User: @@ -119,12 +125,42 @@ class User: data = self._db.find_one({"_id": self.user_id}) self._name = data["name"] + self._age = data["age"] self._gender = data["gender"] self._faculty = data["faculty"] self._group = data["group"] self._type = data["type"] + @staticmethod + def start_shuffle_reg(): + users_reg.clear() + users = [user for user in User.get_all() if user.type] + + markup = types.InlineKeyboardMarkup() + markup.add( + types.InlineKeyboardButton("Я тут!", callback_data="shuffle_agree"), + ) + + for user in users: + bot.send_message(user.user_id, Messages.MATCHING_START, reply_markup=markup) + + @bot.callback_query_handler(func=lambda call: call.data == "shuffle_agree") + def add_user_to_reg(call: types.CallbackQuery): + user = User(call.message.chat.id) + users_reg.append(user) + + bot.edit_message_text( + call.message.chat.id, + call.message.message_id, + "Вы добавлены в список участников! Пожалуйста, подождите, пока все зарегистрируются", + reply_markup=None, + ) + + return + + return + def __str__(self): text = [f"{var}: {vars(self)[var]}" for var in vars(self) if var] text = "\n".join(text) diff --git a/requirements.txt b/app/requirements.txt similarity index 100% rename from requirements.txt rename to app/requirements.txt diff --git a/docker-compose.yaml b/docker-compose.yaml index 9db6ff3..6afeefa 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -6,12 +6,12 @@ services: container_name: python_bot working_dir: /usr/src/app volumes: - - .:/usr/src/app + - ./app:/usr/src/app networks: - rc_network depends_on: - mongodb - command: sh -c "pip install -r requirements.txt && python bot.py" + command: sh -c "pip install -r requirements.txt && python main.py" mongodb: image: mongo:latest