From 8e6b9a604344b92e94400b2f0a2abbcc5ee6e4aa Mon Sep 17 00:00:00 2001 From: AlagappanRa <96157054+AlagappanRa@users.noreply.github.com> Date: Wed, 3 Jan 2024 21:48:46 +0800 Subject: [PATCH 1/6] New lines added Added new lines to database.py --- uspqueuebot/database.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/uspqueuebot/database.py b/uspqueuebot/database.py index fc878b6..2543388 100644 --- a/uspqueuebot/database.py +++ b/uspqueuebot/database.py @@ -81,3 +81,6 @@ def remove_user(hashid): Key = {"hashid": hashid} ) logger.info("User has been successfully removed from the database.") + + + From 8b3b7e492ee8283b65ee230217f6f2f0e395b775 Mon Sep 17 00:00:00 2001 From: nhptrangg Date: Wed, 3 Jan 2024 20:49:05 +0700 Subject: [PATCH 2/6] Testing testing --- uspqueuebot/main.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/uspqueuebot/main.py b/uspqueuebot/main.py index e9ef9c8..045afba 100644 --- a/uspqueuebot/main.py +++ b/uspqueuebot/main.py @@ -126,3 +126,7 @@ def main(bot, body): ## invalid command bot.send_message(chat_id=chat_id, text=INVALID_COMMAND_MESSAGE) return + +#hello + + From e428440e3d778e5414cbb88ee35e9840f9635b34 Mon Sep 17 00:00:00 2001 From: xinnnyeee <155549286+xinnnyeee@users.noreply.github.com> Date: Wed, 3 Jan 2024 22:52:16 +0800 Subject: [PATCH 3/6] added lines added two lines to main.py --- uspqueuebot/main.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/uspqueuebot/main.py b/uspqueuebot/main.py index e9ef9c8..72388a6 100644 --- a/uspqueuebot/main.py +++ b/uspqueuebot/main.py @@ -126,3 +126,6 @@ def main(bot, body): ## invalid command bot.send_message(chat_id=chat_id, text=INVALID_COMMAND_MESSAGE) return + + + From 654dbb43a778e33e7e5ad95ece76d19e1fa0c3a7 Mon Sep 17 00:00:00 2001 From: xinnnyeee <155549286+xinnnyeee@users.noreply.github.com> Date: Wed, 3 Jan 2024 22:53:17 +0800 Subject: [PATCH 4/6] Revert "added lines" This reverts commit e428440e3d778e5414cbb88ee35e9840f9635b34. --- uspqueuebot/main.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/uspqueuebot/main.py b/uspqueuebot/main.py index 72388a6..e9ef9c8 100644 --- a/uspqueuebot/main.py +++ b/uspqueuebot/main.py @@ -126,6 +126,3 @@ def main(bot, body): ## invalid command bot.send_message(chat_id=chat_id, text=INVALID_COMMAND_MESSAGE) return - - - From 3bedd080fc2eb07008aa74126f185832be4820c8 Mon Sep 17 00:00:00 2001 From: AlagappanRa <96157054+AlagappanRa@users.noreply.github.com> Date: Thu, 4 Jan 2024 22:42:39 +0800 Subject: [PATCH 5/6] update update --- uspqueuebot/logic.py | 1 + 1 file changed, 1 insertion(+) diff --git a/uspqueuebot/logic.py b/uspqueuebot/logic.py index a93b53e..e999206 100644 --- a/uspqueuebot/logic.py +++ b/uspqueuebot/logic.py @@ -161,3 +161,4 @@ def broadcast_command(bot, queue, chat_id, message): user_chat_id = get_first_chat_id(queue) bot.send_message(chat_id=chat_id, text=BROADCAST_SUCCESSFUL_MESSAGE) return + From 8299ea5213c4c022ad73c280f680b7058c742053 Mon Sep 17 00:00:00 2001 From: AlagappanRa <96157054+AlagappanRa@users.noreply.github.com> Date: Mon, 8 Jan 2024 21:21:41 +0800 Subject: [PATCH 6/6] Code changes for deployment - Added event management functionality using inline keyboard - Refactored code for deployment --- handler.py => api/handler.py | 60 +++---- requirements.txt | 4 + uspqueuebot/constants.py | 38 ----- uspqueuebot/database.py | 238 +++++++++++++++++++-------- uspqueuebot/logic.py | 187 +++++++++++++-------- uspqueuebot/main.py | 170 +++++++++++++------ uspqueuebot/utilities.py | 309 ++++++++++++++++++++++++++++++----- 7 files changed, 702 insertions(+), 304 deletions(-) rename handler.py => api/handler.py (53%) delete mode 100644 uspqueuebot/constants.py diff --git a/handler.py b/api/handler.py similarity index 53% rename from handler.py rename to api/handler.py index ea95857..79bab8f 100644 --- a/handler.py +++ b/api/handler.py @@ -1,8 +1,10 @@ import json import logging import os -from uspqueuebot.credentials import ADMIN_CHAT_ID import telegram +from dotenv import load_dotenv + +load_dotenv() from uspqueuebot.main import main @@ -24,57 +26,41 @@ 'body': json.dumps('Oops, something went wrong!') } -def configure_telegram(): +def webhook(event): """ - Configures the bot with a Telegram Token. - - Returns a bot instance. + Runs the Telegram webhook. + https://python-telegram-bot.readthedocs.io/en/stable/telegram.bot.html """ - - TELEGRAM_TOKEN = os.environ.get('TELEGRAM_TOKEN') + TELEGRAM_TOKEN = os.getenv('TELEGRAM_TOKEN') if not TELEGRAM_TOKEN: logger.error('The TELEGRAM_TOKEN must be set') raise NotImplementedError + + bot = telegram.Bot(TELEGRAM_TOKEN) - return telegram.Bot(TELEGRAM_TOKEN) - -def set_webhook(event, context): - """ - Sets the Telegram bot webhook. - """ - - logger.info('Event: {}'.format(event)) - bot = configure_telegram() - url = 'https://{}/{}/'.format( - event.get('headers').get('Host'), - event.get('requestContext').get('stage'), - ) - webhook = bot.set_webhook(url) - - if webhook: - return OK_RESPONSE - - return ERROR_RESPONSE - -def webhook(event, context): - """ - Runs the Telegram webhook. - https://python-telegram-bot.readthedocs.io/en/stable/telegram.bot.html - """ + # Check if the webhook is set correctly + webhook_info = bot.get_webhook_info() + desired_webhook_url = os.getenv("VERCEL_URL") + "/api/handler" - bot = configure_telegram() + # If the current webhook URL is not the desired one, update it + if webhook_info.url != desired_webhook_url: + bot.set_webhook(url=desired_webhook_url) + logger.info(f"Webhook updated to: {desired_webhook_url}") + else: + logger.info("Webhook is already set correctly.") if event.get('httpMethod') == 'POST' and event.get('body'): logger.info('Message received') - body = telegram.Update.de_json(json.loads(event.get('body')), bot).to_dict() + update = telegram.Update.de_json(json.loads(event.get('body')), bot) try: - main(bot, body) + main(bot, update) + except Exception as error: error_message = "There is an unhandled exception, please debug immediately.\n" + error.__str__() - bot.send_message(chat_id=ADMIN_CHAT_ID, text=error_message) + bot.send_message(chat_id=os.getenv("ADMIN_CHAT_ID"), text=error_message) logger.error(error_message) - logger.error('Event: {}'.format(body)) + logger.error('Event: {}'.format(update.to_dict())) return OK_RESPONSE diff --git a/requirements.txt b/requirements.txt index 7639f07..b32abf0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,5 @@ python-telegram-bot==8.1.1 +pymongo +bson +datetime +python-dotenv \ No newline at end of file diff --git a/uspqueuebot/constants.py b/uspqueuebot/constants.py deleted file mode 100644 index ee42630..0000000 --- a/uspqueuebot/constants.py +++ /dev/null @@ -1,38 +0,0 @@ -# Admin specfic links -ADMIN_HANDLE = "@ChengYi123" - -# Bot settings -NUMBER_TO_NOTIFY = 5 -NUMBER_TO_BUMP = 5 - -# Messages sent to the user -INVALID_FORMAT_MESSAGE = "Simi? I can only read text, don't send me anything else." -NO_COMMAND_MESSAGE = "What are you saying?? Send a proper command lah please." -START_MESSAGE = "Harlo ah! Easy peasy, /join to join queue, or /help if you still blur." -HELP_MESSAGE = "Aiyo ok ok, these are what you can use:" +\ - "\n /start - Display start message" +\ - "\n /help - Display help message with available commands" +\ - "\n /join - Join the queue" +\ - "\n /leave - Leave the queue" +\ - "\n /howlong - Get your position and queue length" +\ - "\n\nStill don't know ah? Aish ok message me here: " + ADMIN_HANDLE -IN_QUEUE_MESSAGE = "Tsk! You are already in the queue lah!" -JOIN_SUCCESS_MESSAGE = "See got queue then happy happy just join right? Ok ok I put you in." -YOUR_TURN_MESSAGE = "Eh eh its your turn already, hurry up lah can or not?" -NOT_IN_QUEUE_MESSAGE = "Woi... You are not in the queue yet leh!" -LEAVE_SUCCESS_MESSAGE = "You think this one game is it? Join queue then leave... Nevermind, take you out of the queue already." -POSITION_MESSAGE = "How long more ah? Now in front of you got " -QUEUE_LENGTH_MESSAGE = " people. Then, the whole queue total got " -EMPTY_QUEUE_MESSAGE = "Nobody in the queue lah what are you doing??" -NEXT_SUCCESS_MESSAGE = "Ok done, next person is " -COME_NOW_MESSAGE = "Oi! Quick come, it's almost you liao. Number of people in front of you: " -BUMP_SUCCESS_MESSAGE = "Ok liao, that late person got bumped down already. Next person in line is " -INVALID_COMMAND_MESSAGE= "You think I graduated from Harvard is it? Don't know what you are telling me to do lah!" -USELESS_BUMP_MESSAGE = "Only 1 person in the queue, you want to bump for what?" -BUMPEE_MESSAGE = "See lah, tell you come don't want to come. Too late liao, you got bumped down the queue. Use /howlong to check your position." -UNDER_MAINTENANCE_MESSAGE = "Sorry, the bot is currently under maintenance. Do hang tight for more updates, or contact " + ADMIN_HANDLE -PURGE_MESSAGE = "You have been removed from the queue. If you think this is an error, think again. Or you can contact " + ADMIN_HANDLE -PURGE_SUCESSFUL_MESSAGE = "The queue has been successfully purged." -BROADCAST_MESSAGE = "BEEPOO BEEPOO!! Announcement from the admin:\n\n" -BROADCAST_SUCCESSFUL_MESSAGE = "Your message has been successfully broadcasted to all." -BROADCAST_MESSAGE_MISSING_MESSAGE = "What you want me to broadcast? Empty message is it? Pfft!" \ No newline at end of file diff --git a/uspqueuebot/database.py b/uspqueuebot/database.py index 2543388..0290425 100644 --- a/uspqueuebot/database.py +++ b/uspqueuebot/database.py @@ -1,86 +1,182 @@ import logging -import boto3 +from pymongo import MongoClient +from bson.objectid import ObjectId +import os +import datetime +from dotenv import load_dotenv -# Logging is cool! +load_dotenv() + +''' +The stucture of the Event object is as follows: +Queue number is implicit from array index + 1 +{ + "_id": "17826cy78ey7", + "event_name": "Concert", + "event_date": "2024-01-01", # automatically initialised using datetime.now() + "queue": [ + {"hashid": "user123", "chat_id": "chat123", "username": "user1"}, + {"hashid": "user456", "chat_id": "chat456", "username": "user2"}, + // ... More users + ] +} +''' +''' +The structure of the User object is as follows: +Queue number is implicit from array index + 1 +{ + "_id": "284873nuyu43", + "chat_id": "chat123", + "last_command": "/join", +} +''' +# Configure logging logger = logging.getLogger() if logger.handlers: for handler in logger.handlers: logger.removeHandler(handler) logging.basicConfig(level=logging.INFO) -# Setting up client with AWS -client = boto3.resource("dynamodb") -TABLE_NAME = "USPMultiQueueBotTable" -table = client.Table(TABLE_NAME) +# Setup MongoDB client +client = MongoClient(os.getenv("MONGO_URL")) +db = client.USPQUEUEBOT # Change to your database name +events_collection = db.Events # Using the Events collection +users_collection = db.Users # Using the Users collection -def create_table(): - """ - Creates a DynamoDB table - """ +# Function to create a new event +def create_event(event_name): + event_document = { + "event_name": event_name, + "event_date": datetime.now(), + "queue": [] + } + result = events_collection.insert_one(event_document) + logger.info(f"New event created with ID: {result.inserted_id}") + return result.inserted_id +def delete_event(event_id): + events_collection.delete_one({"_id": ObjectId(event_id)}) + logger.info("Event deleted.") + return + +def delete_all_events(): + ''' + Deletes all events in the database + ''' + #TODO @xinnnyeee for `purge_database_command` + pass + +def view_history(date): + ''' + Filter the events collection by date and return the events that are before and inclusive of the date + ''' + #TODO @nhptrangg for `view_history_command` + pass + +# Function to get all events, result can be displayed using an inline keyboard, with the callback data +# being the _id fields +def get_all_events(): try: - client.create_table( - TableName = TABLE_NAME, - KeySchema = [ - { - "AttributeName": 'hashid', - "KeyType": "HASH" - } - ], - AttributeDefinitions = [ - { - "AttributeName": "hashid", - "AttributeType": "S" - } - ], - ProvisionedThroughput = { - 'ReadCapacityUnits': 5, - 'WriteCapacityUnits': 5 - } - ) - logger.info("Table named " + TABLE_NAME + " was created in DynamoDB.") - except: - logger.info("Table named " + TABLE_NAME + " already exists in DynamoDB.") - return - -def get_table(): - """ - Retrieve all contents of the table - - Returns - ------- - dic - Response from scan requeston DynamoDB - """ - try: - response = table.scan() - logger.info("All entries have been retrieved and returned.") - return response - except: - create_table() - response = get_table() - return response - -def insert_user(hashid, chat_id, username, queue_number): - """ - Insert a new entry into the table - """ - table.update_item( - Key = {"hashid": hashid}, - UpdateExpression = "SET {} = :val1, {} =:val2, {} = :val3".format("chat_id", "username", "queue_number"), - ExpressionAttributeValues = {":val1": chat_id, ":val2": username, ":val3": queue_number} - ) - logger.info("New entry successfully added into DynamoDB.") - -def remove_user(hashid): - """ - Removes an entry from the table using hashid - """ - - table.delete_item( - Key = {"hashid": hashid} + events = list(events_collection.find({})) + logger.info("All events have been retrieved and returned.") + return events # Returns a list of event documents + except Exception as e: + logger.error(f"An error occurred: {e}") + return [] + +# Function to add a user to an event queue +def add_user_to_event(event_id, hashid, chat_id, username): + user_details = {"hashid": hashid, "chat_id": chat_id, "username": username} + events_collection.update_one( + {"_id": ObjectId(event_id)}, + {"$push": {"queue": user_details}} + ) + logger.info("New user added to event queue.") + return + +def remove_user_from_event(event_id, hashid): + ''' + Returns true if the user was removed from the event queue else false + ''' + result = events_collection.update_one( + {"_id": ObjectId(event_id)}, + {"$pull": {"queue": {"hashid": hashid}}} ) - logger.info("User has been successfully removed from the database.") + if result.modified_count == 1: + logger.info("User removed from event queue.") + return True + elif result.modified_count == 0: + logger.info("No user removed from event queue. User does not exist in queue.") + return False + else: + logger.exception("Multiple instances of the same user existed and have now been removed from event queue. Please check database.") + return True + +# Function to get the queue of a specific event +def get_event_queue(event_id): + ''' + Returns the queue of a specific event in array format + [ + {"hashid": "user123", "chat_id": "chat123"}, + {"hashid": "user456", "chat_id": "chat456"} + // ... More users + ] + ''' + event = events_collection.find_one({"_id": ObjectId(event_id)}) + if event: + return event.get("queue", []) + else: + logger.error("Event not found.") + return [] + +def record_last_command(chat_id, command): + ''' + Records the last command of a user given their chat_id and command + ''' + users_collection.update_one( + {"chat_id": chat_id}, + {"$set": {"last_command": command}}, + upsert=True + ) + logger.info("Last command, {} of user {} recorded.".format(command, chat_id)) + return + +def get_last_command(chat_id): + ''' + Gets the last command of a user given their chat_id \n + Returns: + Empty string if user not found or no last command \n + String of last command if user found and there is a last command + ''' + user = users_collection.find_one({"chat_id": chat_id}) + if user: + return user.get("last_command", "") + else: + logger.error("User not found.") + return "" + +# # Example usage +# if __name__ == "__main__": +# # Example of creating a new event +# event_id = create_event("Concert") + +# # Example of adding a user to the event queue +# add_user_to_event(event_id, "user123", "chat123", "user1", 1) + +# # Example of removing a user from the event queue +# remove_user_from_event(event_id, "user123") + +# # Example of retrieving the event queue +# queue = get_event_queue(event_id) +# print("Event Queue:", queue) +# # Example of recording the last command of a user +# record_last_command("chat123", "/join") +# # Example of retrieving the last command of a user +# last_command = get_last_command("chat123") +# print("Last Command:", last_command) +# # Example of deleting an event +# delete_event(event_id) +# print("Event Deleted") diff --git a/uspqueuebot/logic.py b/uspqueuebot/logic.py index e999206..134d5ed 100644 --- a/uspqueuebot/logic.py +++ b/uspqueuebot/logic.py @@ -1,21 +1,14 @@ import logging +import os +from dotenv import load_dotenv +from telegram import Bot +from datetime import datetime -from uspqueuebot.constants import (BROADCAST_MESSAGE, BROADCAST_MESSAGE_MISSING_MESSAGE, - BROADCAST_SUCCESSFUL_MESSAGE, - BUMP_SUCCESS_MESSAGE, BUMPEE_MESSAGE, - COME_NOW_MESSAGE, EMPTY_QUEUE_MESSAGE, - IN_QUEUE_MESSAGE, JOIN_SUCCESS_MESSAGE, - LEAVE_SUCCESS_MESSAGE, NEXT_SUCCESS_MESSAGE, - NOT_IN_QUEUE_MESSAGE, NUMBER_TO_NOTIFY, - POSITION_MESSAGE, PURGE_MESSAGE, - PURGE_SUCESSFUL_MESSAGE, - QUEUE_LENGTH_MESSAGE, USELESS_BUMP_MESSAGE, - YOUR_TURN_MESSAGE) -from uspqueuebot.database import insert_user, remove_user -from uspqueuebot.utilities import (get_bump_queue, get_first_chat_id, - get_first_username, get_next_queue, +from uspqueuebot.utilities import (add_user_to_event_in_database, get_bump_queue, get_first_chat_id, get_first_username, get_next_queue_number, get_position, - get_sha256_hash, is_in_queue) + get_sha256_hash, is_in_queue, remove_user_from_event_in_database, update_event_queue_in_database) + +load_dotenv() # Logging is cool! logger = logging.getLogger() @@ -24,141 +17,195 @@ logger.removeHandler(handler) logging.basicConfig(level=logging.INFO) -def join_command(bot, queue, chat_id, username): +# Note: Only use function from utilities.py. Do not use functions from database.py directly. +def create_new_event(bot : Bot, event_name : str, chat_id : int): + ''' + Creates a new event in the database. + Returns a response that displays the ongoing events to the user sorted by the event_date: + + For example, after the create command is given for event 3, the response should be: + + List of ongoing events:\n + 1. Event 1 (Created at: 11/12/2023)\n + 2. Event 2 (Created at: 12/12/2023)\n + 3. Event 3 (Created at: 15/12/2023) + + where 11/12/2023 is the event_date + ''' + #TODO @xinnnyeee + pass + +def delete_event_command(bot : Bot, event_id : int, chat_id : int): + ''' + Deletes the selected event from the database. + Returns a response that displays the (remaining) ongoing events to the user sorted by the event_date: + + For example, after the delete command is given for event 3, the response should be: + + List of ongoing events:\n + 1. Event 1 (Created at: 11/12/2023)\n + 2. Event 2 (Created at: 12/12/2023)\n + + where 11/12/2023 is the event_date + ''' + #TODO @nhptrangg + pass + +def purge_database_command(bot: Bot, valid_date: datetime, chat_id: int): + ''' + Deletes all events before and inclusive of `valid_date` from the database. + Returns a response with format: + + All events before and inclusive of 11/12/2023 have been deleted + ''' + #TODO @xinnnyeee + pass + +def view_history_command(bot: Bot, valid_date: datetime, chat_id: int): + ''' + Returns a response that displays the (remaining) ongoing events before and inclusive of `valid_date` to the user sorted by the event_date: + + For example, after the view history command is given for 11/12/2023, the response should be: + + List of ongoing events:\n + 1. Event 1 (Created at: 11/12/2023)\n + + where 11/12/2023 is the event_date + ''' + #TODO @nhptrangg + pass + +def join_command(bot, queue, chat_id, username, event_id): + ''' + The bot adds the user to the selected event's queue. + ''' if is_in_queue(queue, chat_id): - bot.send_message(chat_id=chat_id, text=IN_QUEUE_MESSAGE) + bot.send_message(chat_id=chat_id, text=os.getenv("IN_QUEUE_MESSAGE")) logger.info("User already in queue tried to join queue.") return queue_number = get_next_queue_number(queue) hashid = get_sha256_hash(chat_id) - insert_user(hashid, chat_id, username, queue_number) - bot.send_message(chat_id=chat_id, text=JOIN_SUCCESS_MESSAGE) + add_user_to_event_in_database(event_id, hashid, chat_id, username, queue_number) + bot.send_message(chat_id=chat_id, text=os.getenv("JOIN_SUCCESS_MESSAGE")) logger.info("New user added to the queue.") - - if len(queue) == 0: - bot.send_message(chat_id=chat_id, text=YOUR_TURN_MESSAGE) - logger.info("Newly added user is first in line.") - return -def leave_command(bot, queue, chat_id): - if not is_in_queue(queue, chat_id): - bot.send_message(chat_id=chat_id, text=NOT_IN_QUEUE_MESSAGE) - logger.info("User not in queue tried to leave queue.") - return - +def leave_command(bot, chat_id, event_id): + ''' + The bot removes the user from the selected event's queue. + ''' hashid = get_sha256_hash(chat_id) - remove_user(hashid) - bot.send_message(chat_id=chat_id, text=LEAVE_SUCCESS_MESSAGE) - logger.info("User removed from the queue.") - return + if (remove_user_from_event_in_database(event_id, hashid)): + bot.send_message(chat_id=chat_id, text=os.getenv("LEAVE_SUCCESS_MESSAGE")) + else: + bot.send_message(chat_id=chat_id, text=os.getenv("NOT_IN_QUEUE_MESSAGE")) + return def howlong_command(bot, queue, chat_id): - position = get_position(chat_id, queue) + ''' + The bot informs the user of the length of the queue and how many people are before the querying user. + ''' + position = get_position(queue, chat_id) queue_length = str(len(queue)) if position == "Not in queue": position = queue_length - message = POSITION_MESSAGE + position + QUEUE_LENGTH_MESSAGE + queue_length + "." + message = os.getenv("POSITION_MESSAGE") + position + os.getenv("QUEUE_LENGTH_MESSAGE") + queue_length + "." bot.send_message(chat_id=chat_id, text=message) logger.info("Position and queue details sent to user.") return def viewqueue_command(bot, queue, chat_id): if len(queue) == 0: - bot.send_message(chat_id=chat_id, text=EMPTY_QUEUE_MESSAGE) + bot.send_message(chat_id=chat_id, text=os.getenv("EMPTY_QUEUE_MESSAGE")) logger.info("Empty queue has been sent to admin.") return message = "Queue:" - count = 1 - for entry in queue: - username = entry[2] + for count, entry in enumerate(queue): + username = entry[1] message += "\n" - message += str(count) + message += str(count + 1) message += ". " message += username - count += 1 bot.send_message(chat_id=chat_id, text=message) logger.info("Queue details has been sent to admin.") return -def next_command(bot, queue, chat_id): +def next_command(bot, queue, chat_id, event_id): if len(queue) == 0: - bot.send_message(chat_id=chat_id, text=EMPTY_QUEUE_MESSAGE) + bot.send_message(chat_id=chat_id, text=os.getenv("EMPTY_QUEUE_MESSAGE")) logger.info("Empty queue has been sent to admin.") return - new_queue = get_next_queue(queue) + new_queue = queue[1:] next_username = get_first_username(new_queue) - bot.send_message(chat_id=chat_id, text=NEXT_SUCCESS_MESSAGE + next_username) + hash_id = get_sha256_hash(queue[0][0]) + remove_user_from_event_in_database(event_id, hash_id) + bot.send_message(chat_id=chat_id, text=os.getenv("NEXT_SUCCESS_MESSAGE") + next_username) logger.info("Queue advanced by one user.") notify(bot, new_queue) return def notify(bot, queue): count = -1 - for entry in queue[:NUMBER_TO_NOTIFY]: + for entry in queue[:os.getenv("NUMBER_TO_NOTIFY")]: count += 1 - chat_id = entry[1] + chat_id = entry[0] if count == 0: - bot.send_message(chat_id=chat_id, text=YOUR_TURN_MESSAGE) + bot.send_message(chat_id=chat_id, text=os.getenv("YOUR_TURN_MESSAGE")) continue - bot.send_message(chat_id=chat_id, text=COME_NOW_MESSAGE + str(count)) + bot.send_message(chat_id=chat_id, text=os.getenv("COME_NOW_MESSAGE") + str(count)) logger.info("First few users has been informed of their queue status.") return def bump_command(bot, queue, chat_id): if len(queue) == 0: - bot.send_message(chat_id=chat_id, text=EMPTY_QUEUE_MESSAGE) + bot.send_message(chat_id=chat_id, text=os.getenv("EMPTY_QUEUE_MESSAGE")) logger.info("Empty queue has been sent to admin.") return if len(queue) == 1: - bot.send_message(chat_id=chat_id, text=USELESS_BUMP_MESSAGE) + bot.send_message(chat_id=chat_id, text=os.getenv("USELESS_BUMP_MESSAGE")) logger.info("Admin has been informed that user is the only one in the queue.") return new_queue = get_bump_queue(queue) next_username = get_first_username(new_queue) - update_bump_queue(new_queue) + update_event_queue_in_database(new_queue) inform_bumpee(bot, queue) - bot.send_message(chat_id=chat_id, text=BUMP_SUCCESS_MESSAGE + next_username) + bot.send_message(chat_id=chat_id, text=os.getenv("BUMP_SUCCESS_MESSAGE") + next_username) logger.info("First user in the queue has been bumped down.") notify(bot, new_queue) return def inform_bumpee(bot, queue): - chat_id = get_first_chat_id(queue) - bot.send_message(chat_id=chat_id, text=BUMPEE_MESSAGE) + chat_id = queue[0][1] + bot.send_message(chat_id=chat_id, text=os.getenv("BUMPEE_MESSAGE")) logger.info("Bumpee has been informed of the bump.") return -def update_bump_queue(queue): - for (queue_number, chat_id, username) in queue: - hashid = get_sha256_hash(chat_id) - insert_user(hashid, chat_id, username, queue_number) - logger.info("New bumped list has benen updated in the database.") - return - -def purge_command(bot, queue, chat_id): +def purge_command(bot, queue, chat_id, event_id): + ''' + Removes every person in the queue but does not delete the event itself + ''' user_chat_id = get_first_chat_id(queue) while user_chat_id != "None": hashid = get_sha256_hash(user_chat_id) - remove_user(hashid) - bot.send_message(chat_id=user_chat_id, text=PURGE_MESSAGE) + remove_user_from_event_in_database(event_id, hashid) + bot.send_message(chat_id=user_chat_id, text=os.getenv("PURGE_MESSAGE")) queue = queue[1:] user_chat_id = get_first_chat_id(queue) - bot.send_message(chat_id=chat_id, text=PURGE_SUCESSFUL_MESSAGE) + bot.send_message(chat_id=chat_id, text=os.getenv("PURGE_SUCESSFUL_MESSAGE")) return def broadcast_command(bot, queue, chat_id, message): if message == "": - bot.send_message(chat_id=chat_id, text=BROADCAST_MESSAGE_MISSING_MESSAGE) + bot.send_message(chat_id=chat_id, text=os.getenv("BROADCAST_MESSAGE_MISSING_MESSAGE")) return user_chat_id = get_first_chat_id(queue) while user_chat_id != "None": - bot.send_message(chat_id=user_chat_id, text=BROADCAST_MESSAGE + message) + bot.send_message(chat_id=user_chat_id, text=os.getenv("BROADCAST_MESSAGE") + message) queue = queue[1:] user_chat_id = get_first_chat_id(queue) - bot.send_message(chat_id=chat_id, text=BROADCAST_SUCCESSFUL_MESSAGE) + bot.send_message(chat_id=chat_id, text=os.getenv("BROADCAST_SUCCESSFUL_MESSAGE")) return diff --git a/uspqueuebot/main.py b/uspqueuebot/main.py index 72388a6..ffaa3cb 100644 --- a/uspqueuebot/main.py +++ b/uspqueuebot/main.py @@ -1,13 +1,13 @@ import logging +import os +import json +from dotenv import load_dotenv +import datetime +from uspqueuebot.logic import (broadcast_command, bump_command, create_new_event, delete_event_command, howlong_command, join_command, + leave_command, next_command, purge_command, purge_database_command, view_history_command, viewqueue_command) +from uspqueuebot.utilities import (extract_user_details, get_event_queue_from_database, get_last_command_from_database, get_message_type, record_last_command_in_database, send_event_selection) -from uspqueuebot.constants import (HELP_MESSAGE, INVALID_COMMAND_MESSAGE, - INVALID_FORMAT_MESSAGE, NO_COMMAND_MESSAGE, - START_MESSAGE, UNDER_MAINTENANCE_MESSAGE) -from uspqueuebot.credentials import ADMIN_CHAT_ID, ADMINS -from uspqueuebot.logic import (broadcast_command, bump_command, howlong_command, join_command, - leave_command, next_command, purge_command, viewqueue_command) -from uspqueuebot.utilities import (extract_user_details, get_message_type, - get_queue) +load_dotenv() # Logging is cool! logger = logging.getLogger() @@ -18,19 +18,14 @@ DEBUG_MODE = False -def main(bot, body): +def main(bot, update): """ Runs the main logic of the Telegram bot """ - - # for privacy issues, this is commented out - #logger.info('Event: {}'.format(body)) + body = update.to_dict() - # manage updates (https://core.telegram.org/bots/api#getting-updates) if "update_id" in body.keys() and len(body.keys()) == 1: - logger.info("An update_id message has been sent by Telegram.") - logger.error('Event: {}'.format(body)) - return + handle_invalid_updates(body) # obtain key message details message_type = get_message_type(body) @@ -38,50 +33,59 @@ def main(bot, body): # for debugging, set DEBUG_MODE to True if DEBUG_MODE: - logger.warn("Debug mode has been activated.") - text = str(body) - bot.send_message(chat_id=ADMIN_CHAT_ID, text=text) - logger.warn("Event text has been sent to the admin.") - bot.send_message(chat_id=chat_id, text=UNDER_MAINTENANCE_MESSAGE) - logger.warn("Maintenance message has been sent to user.") - return - - # check for file types we cannot handle - if not message_type == "text": - bot.send_message(chat_id=chat_id, text=INVALID_FORMAT_MESSAGE) - logger.info("A message of invalid format has been sent.") + handle_debug_mode(bot, body, chat_id) + return + + if message_type == "text": + handle_text_message(bot, body, update, chat_id) return - # reject all non-commands - text = body["message"]["text"] - if text[0] != "/": - bot.send_message(chat_id=chat_id, text=NO_COMMAND_MESSAGE) - logger.info("No command detected.") + # When a user presses a button in an inline keyboard of a Telegram bot, the bot receives an update that is categorized as a "callback query." The callback query contains various pieces of information, including the callback_data associated with the button pressed. + if message_type == "callback_query": + handle_callback_query(bot, chat_id, username, update) return - # start command - if text == "/start": - bot.send_message(chat_id=chat_id, text=START_MESSAGE) - logger.info("Start command detected and processed.") - return + # file types we cannot handle + handle_invalid_message_type(bot, chat_id) + return - # help command - if text == "/help": - bot.send_message(chat_id=chat_id, text=HELP_MESSAGE) - logger.info("Help command detected and processed.") - return +def handle_invalid_updates(body): + logger.info("An update_id message has been sent by Telegram.") + logger.error('Event: {}'.format(body)) + return - queue = get_queue() +def handle_debug_mode(bot, body, chat_id): + logger.warn("Debug mode has been activated.") + text = str(body) + bot.send_message(chat_id=os.getenv("ADMIN_CHAT_ID"), text=text) + logger.warn("Event text has been sent to the admin.") + bot.send_message(chat_id=chat_id, text=os.getenv("UNDER_MAINTENANCE_MESSAGE")) + logger.warn("Maintenance message has been sent to user.") + return + +def handle_invalid_message_type(bot, chat_id): + bot.send_message(chat_id=chat_id, text=os.getenv("INVALID_FORMAT_MESSAGE")) + logger.info("A message of invalid format has been sent.") + return + +def handle_callback_query(bot, chat_id, username, update): + query = update.callback_query + query.answer() # It's good practice to answer the callback query + + # Extract and process the callback data + event_id = query.data + queue = get_event_queue_from_database(event_id) + text = get_last_command_from_database(chat_id) # join command if text == "/join": - join_command(bot, queue, chat_id, username) + join_command(bot, queue, chat_id, username, event_id) logger.info("Join command detected and processed.") return # leave command if text == "/leave": - leave_command(bot, queue, chat_id) + leave_command(bot, chat_id, event_id) logger.info("Leave command detected and processed.") return @@ -91,8 +95,10 @@ def main(bot, body): logger.info("Howlong command detected and processed.") return + admins_str = os.getenv('ADMINS') + admins_dict = json.loads(admins_str.replace("'", "\"")) # replacing single quotes to double quotes for valid JSON # admin commands - if chat_id in ADMINS.values(): + if chat_id in admins_dict.values(): # viewqueue command if text == "/viewqueue": viewqueue_command(bot, queue, chat_id) @@ -120,12 +126,74 @@ def main(bot, body): broadcast_command(bot, queue, chat_id, text[10:]) logger.info("Broadcast command detected and processed.") return - - # intentionally no return here - + + if text == "/delete": + delete_event_command(bot, event_id, chat_id) + logger.info("Delete command detected and processed.") + return + ## invalid command - bot.send_message(chat_id=chat_id, text=INVALID_COMMAND_MESSAGE) + bot.send_message(chat_id=chat_id, text=os.getenv("INVALID_COMMAND_MESSAGE")) return +def handle_text_message(bot, body, update, chat_id): + text = body["message"]["text"] + if text[0] != "/": + bot.send_message(chat_id=chat_id, text=os.getenv("NO_COMMAND_MESSAGE")) + logger.info("No command detected.") + return + # start command + if text == "/start": + bot.send_message(chat_id=chat_id, text=os.getenv("START_MESSAGE")) + logger.info("Start command detected and processed.") + return + # help command + if text == "/help": + bot.send_message(chat_id=chat_id, text=os.getenv("HELP_MESSAGE")) + logger.info("Help command detected and processed.") + return + + admins_str = os.getenv('ADMINS') + admins_dict = json.loads(admins_str.replace("'", "\"")) + + # admin commands + if chat_id in admins_dict.values(): + + # create new event command + if text[:7] == "/create": + create_new_event(bot, text[8:], chat_id) + logger.info("Created new event command detected and processed.") + + def validate_date(date_string): + try: + date = datetime.datetime.strptime(date_string, "%d-%m-%Y") + return date + except ValueError: + return False + + # purge database command + if text[:8] == "/purgedb": + valid_date = validate_date(text[9:]) + if valid_date: + purge_database_command(bot, valid_date, chat_id) + logger.info("Purge database command detected and processed.") + else: + logger.warning("Invalid date format.") + return + + #view history command + if text[:12] == "/viewhistory": + valid_date = validate_date(text[13:]) + if valid_date: + view_history_command(bot, valid_date, chat_id) + logger.info("View history command detected and processed.") + else: + logger.warning("Invalid date format.") + return + + else: + record_last_command_in_database(chat_id, text) + send_event_selection(update, text) + return diff --git a/uspqueuebot/utilities.py b/uspqueuebot/utilities.py index 3d09b1f..be55562 100644 --- a/uspqueuebot/utilities.py +++ b/uspqueuebot/utilities.py @@ -1,10 +1,13 @@ import hashlib +import os +from dotenv import load_dotenv +from telegram import InlineKeyboardButton, InlineKeyboardMarkup +from uspqueuebot.database import add_user_to_event, create_event, delete_all_events, delete_event, get_all_events, get_event_queue, get_last_command, record_last_command, remove_user_from_event, view_history -from uspqueuebot.constants import NUMBER_TO_BUMP -from uspqueuebot.database import get_table, remove_user +load_dotenv() - -def get_message_type(body): +#=========================FUNCTIONS FOR DATA EXTRACTION=========================# +def get_message_type(body : dict): """ Determines the Telegram message type @@ -28,6 +31,9 @@ def get_message_type(body): if "edited_message" in body.keys(): return "edited_message" + if "callback_query" in body.keys(): + return "callback_query" + return "others" def decimal_to_int(decimal): @@ -39,7 +45,7 @@ def decimal_to_int(decimal): integer = int(str(decimal)) return integer -def extract_user_details(body): +def extract_user_details(body : dict): """ Obtains the chat ID from the event body @@ -54,7 +60,10 @@ def extract_user_details(body): Tuple containing chat ID and username of user """ - if "edited_message" in body.keys(): + if "callback_query" in body.keys(): + chat_id = body["callback_query"]["message"]["chat"]["id"] + username = body["callback_query"]["message"]["chat"]["username"] + elif "edited_message" in body.keys(): chat_id = body["edited_message"]["chat"]["id"] username = body["edited_message"]["chat"]["username"] else: @@ -64,7 +73,7 @@ def extract_user_details(body): chat_id = decimal_to_int(chat_id) return (chat_id, username) -def get_sha256_hash(plaintext): +def get_sha256_hash(plaintext : str): """ Hashes an object using SHA256. Usually used to generate hash of chat ID for lookup Parameters @@ -84,30 +93,95 @@ def get_sha256_hash(plaintext): hash = hasher.hexdigest() return hash -def get_queue(): - raw_table = get_table() +#=========================FUNCTIONS FOR QUEUE MANIPULATION=========================# +def get_event_queue_from_database(event_id : str): + ''' + Pre-process queue to be in the form of a list of tuples, sorted by queue number\n + + Parameters + ---------- + event_id: str + The event ID of the event to retrieve the queue from + + Returns + ------- + queue: list + Contains tuples in the form (chat_id, username) representing each + person in the queue + ''' + raw_table = get_event_queue(event_id) queue = [] - for entry in raw_table["Items"]: - queue_number = decimal_to_int(entry["queue_number"]) + for entry in raw_table: chat_id = decimal_to_int(entry["chat_id"]) username = entry["username"] - queue.append((queue_number, chat_id, username)) + queue.append((chat_id, username)) queue.sort() return queue -def get_next_queue_number(queue): - queue_number = 0 - if len(queue) != 0: - queue_number = queue[-1][0] + 1 - return queue_number +def get_next_queue_number(queue : list): + ''' + Parameters + ---------- + queue: list + + Returns + ------- + the next (one-based) queue number or 1 if queue is empty + ''' + return len(queue) + 1 -def is_in_queue(queue, chat_id): +def is_in_queue(queue : list, chat_id : int): + ''' + Parameters + ---------- + queue: list\n + chat_id: int + + Returns + ------- + True if the user is in the queue, False otherwise + ''' for entry in queue: - if entry[1] == chat_id: + if entry[0] == chat_id: return True return False -def get_position(chat_id, queue): +def get_first_chat_id(queue: list): + ''' + Parameters + ---------- + queue: list + + Returns + ------- + the chat_id of the first person in the queue + ''' + return queue[0][0] + +def get_first_username(queue : list): + ''' + Parameters + ---------- + queue: list + + Returns + ------- + the username of the first person in the queue + ''' + return queue[0][1] + +def get_position(queue : list, chat_id : int): + ''' + Parameters + ---------- + queue: list\n + chat_id: int + + Returns + ------- + the string position of the user in the queue, + or "Not in queue" if user is not in queue + ''' ## position is equivalent to number of people ahead of user position = 0 found = False @@ -121,24 +195,185 @@ def get_position(chat_id, queue): position = "Not in queue" return str(position) -def get_next_queue(queue): - to_delete = queue[0][1] - hashid = get_sha256_hash(to_delete) - remove_user(hashid) - return queue[1:] - -def get_first_chat_id(queue): - if len(queue) == 0: - return "None" - return queue[0][1] +def get_bump_queue(queue : list): + ''' + Bumps the first person in the queue by a pre-defined number of positions in\n + `constants.py`. Updates all the queue numbers accordingly.\n -def get_first_username(queue): - if len(queue) == 0: - return "None" - return queue[0][2] + Parameters + ---------- + queue: list -def get_bump_queue(queue): - bump_queue = queue[1:NUMBER_TO_BUMP + 1] + Returns + ------- + the modified queue + ''' + bump_queue = queue[1:os.getenv("NUMBER_TO_BUMP") + 1] bump_queue.append(queue[0]) - bump_queue = [(new_index, curr_tuple[1], curr_tuple[2]) for new_index, curr_tuple in enumerate(bump_queue)] + bump_queue.append(queue[os.getenv("NUMBER_TO_BUMP") + 1:]) return bump_queue + +def create_event_in_database(event_name : str): + ''' + Creates a new event and an empty associated queue in the database\n + + Parameters + ---------- + event_name: str + + Returns + ------- + event_id: str + The event ID of the newly created event + ''' + event_id = create_event(event_name) + return event_id + +def delete_event_from_database(event_id): + ''' + Deletes an event from the database\n + + Parameters + ---------- + event_id: str + The event ID of the event to delete + ''' + delete_event(event_id) + return + +def delete_all_events_from_database(): + ''' + Deletes all events from the database + ''' + delete_all_events() + return + +def view_history_of_events_from_database(date): + ''' + Returns a view of events before and inclusive of date using the event_date field + ''' + return view_history(date) + +def get_all_events_from_database(): + ''' + Returns all events in the database\n + + Returns + ------- + events: list + List of all events in the database + + Each event is of the form: + { + "_id": "17826cy78ey7", + "event_name": "Concert", + "event_date": "2024-01-01", # automatically initialised using datetime.now() + "queue": [ + {"hashid": "user123", "chat_id": "chat123", "username": "user1"}, + {"hashid": "user456", "chat_id": "chat456", "username": "user2"}, + // ... More users + ] + } + The returned list is of the form: + [{}, {}, {}, {}] where each {} is a event. + ''' + events = get_all_events() + return events + +def add_user_to_event_in_database(event_id : str, hashid : str, chat_id : int, username : str): + ''' + Adds a user to the event queue in the database\n + + Parameters + ---------- + event_id: str\n + hashid: str\n + chat_id: int\n + username: str\n + ''' + add_user_to_event(event_id, hashid, chat_id, username) + + +def remove_user_from_event_in_database(event_id : str, hashid : str): + ''' + Removes a user from the event queue in the database\n + + Parameters + ---------- + event_id: str\n + hashid: str + + Returns + ---------- + True if user was removed, False otherwise + ''' + return remove_user_from_event(event_id, hashid) + + +def record_last_command_in_database(chat_id : int, command : str): + ''' + Records the last command of a user given their chat_id and command\n + + Parameters + ---------- + chat_id: int\n + command: str + ''' + record_last_command(chat_id, command) + +def get_last_command_from_database(chat_id : int): + ''' + Gets the last command of a user given their chat_id\n + + Parameters + ---------- + chat_id: int\n + + Returns + ------- + last_command: str + The last command of the user + ''' + last_command = get_last_command(chat_id) + return last_command + +def update_event_queue_in_database(new_queue : list, event_id): + ''' + Updates the database with a new queue\n + + Parameters + ---------- + new_queue: list + The new queue to update the database with + ''' + for (chat_id, username) in new_queue: + hashid = get_sha256_hash(chat_id) + add_user_to_event_in_database(event_id, hashid, chat_id, username) + return + +#=========================FUNCTIONS FOR EVENT SELECTION=========================# +def send_event_selection(update, last_instr : str): + ''' + Generates a keyboard of events using an inline keyboard. + + Parameters: + ------------ + bot: The bot object from Telegram API. + update: The update object from Telegram API. + last_instr: The last instruction executed by the user from database. + ''' + # Retrieve all events from the database + events = get_all_events_from_database() + + # Populates the keyboard with a bunch of buttons + keyboard = [[InlineKeyboardButton(event['event_name'], callback_data=str(event['_id']))] for event in events] + + # This line wraps the keyboard in the InlineKeyboardMarkup, which is the format + # expected by Telegram API for inline keyboards. The reply_markup object will be passed to + # the bot's send_message function to display the inline keyboard to the user. + reply_markup = InlineKeyboardMarkup(keyboard) + + # This sends a message to the user with the text "Please choose an event to join:". + # The reply_markup parameter is used to attach the inline keyboard to the message, allowing the + # user to make a selection from the available events. + update.message.reply_text('Please choose an event to execute' + " " + last_instr + " " + 'on :', reply_markup=reply_markup)