From 0d7d2c7050fa37c1b8347f4eedeb64f6886ae5e4 Mon Sep 17 00:00:00 2001
From: Al Matty <al@almatty.com>
Date: Mon, 28 Nov 2022 13:42:37 -0800
Subject: [PATCH] Implemented a button menu for data entry (closes #8 )

---
 telegram_bot.py | 100 ++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 97 insertions(+), 3 deletions(-)

diff --git a/telegram_bot.py b/telegram_bot.py
index 1c2c915..6a2813a 100644
--- a/telegram_bot.py
+++ b/telegram_bot.py
@@ -6,7 +6,9 @@
 '''
 
 import os, logging, random
+from telegram import InlineKeyboardButton, InlineKeyboardMarkup
 from telegram.ext import CommandHandler, Filters, MessageHandler, Updater
+from telegram.ext import CallbackQueryHandler
 from urllib.error import HTTPError
 from dotenv import load_dotenv
 from inspect import cleandoc
@@ -48,6 +50,14 @@ def __init__(self):
             '/donate': self.show_donate,
             }
 
+        # Logic that ties button names to self.users dictionary keys for data entry
+        self.button_map = {
+            'enter Discord username': 'discord_username'
+            }
+
+        # To be set to a self.users key if user data is expected in next message
+        self.save_to_users = False
+
         # Discord bot instance (set from outside this scope)
         self.discord_bot = None
 
@@ -74,8 +84,10 @@ def run_bot(self):
         # Declares and adds handlers for commands that shows help info
         start_handler = CommandHandler('start', self.start_dialogue)
         help_handler = CommandHandler('help', self.show_menu)
+        callback_handler = CallbackQueryHandler(self.button_logic)
         self.dispatcher.add_handler(start_handler)
         self.dispatcher.add_handler(help_handler)
+        self.dispatcher.add_handler(callback_handler)
 
         # Declares and adds a handler for text messages that will reply with
         # a response if the message includes a trigger word
@@ -91,6 +103,60 @@ def set_discord_instance(self, bot):
         """
         self.discord_bot = bot
 
+    def button_logic(self, update, context):
+        """
+        Central point for all logic for pressed buttons.
+        Parses the CallbackQuery and updates the message text.
+        """
+        query = update.callback_query
+        choice = query.data
+
+        # CallbackQueries need to be answered, even if no notification to the user is needed
+        # Some clients may have trouble otherwise. See https://core.telegram.org/bots/api#callbackquery
+        query.answer()
+        if query.data != 'back':
+            query.edit_message_text(text=f"Please {query.data}:")
+
+        # Possibility: Button 'back' pressed -> lead to menu at all times
+        if choice == 'back':
+            self.show_menu(update, context)
+            self.save_to_users = False
+
+        # Ignore any command handled by handle_texte_massages() already
+        elif choice in self.command_map:
+            self.save_to_users = False
+            return
+
+        # Possibility: Other button -> set self.users key as self.save_to_users flag
+        else:
+            # retrieve correct database key for option chosen by user
+            self.save_to_users = self.button_map[choice]
+
+
+
+    def button_choice(self, msg, choices, update, context):
+        """
+        Takes a message & list of button names (buttons).
+        Adds option 'back' if not included in list.
+        Sends a message to the user with inline buttons attached.
+        """
+        def keyb_from_choices(button_names):
+            """Returns InlineKeyboardMarkup object from list of choices."""
+            keyboard = []
+            if 'back' not in button_names: button_names.append('back')
+
+            for button_name in button_names:
+                b = InlineKeyboardButton(button_name, callback_data=button_name)
+                keyboard.append(b)
+
+            return InlineKeyboardMarkup([keyboard])
+
+        reply_markup = keyb_from_choices(choices)
+        msg = self.parse_msg(msg)
+        update.message.reply_text(msg, reply_markup=reply_markup, parse_mode='MarkdownV2')
+
+        return
+
     def parse_msg(self, msg):
         """
         Helper function to create telegram compatible text output
@@ -149,9 +215,16 @@ def send_msg(self, msg, update, context):
         """
         Sends a text message.
         """
+
+        # Prevent AttributeError for callback_query type updates
+        if update.callback_query:
+            chat_id = update.callback_query.message.chat.id
+        else:
+            chat_id = update.message.chat_id
+
         parsed_msg = self.parse_msg(msg)
         context.bot.send_message(
-            chat_id=update.message.chat_id,
+            chat_id=chat_id,
             text=parsed_msg,
             disable_web_page_preview=True,
             parse_mode='MarkdownV2'
@@ -223,9 +296,14 @@ def edit_discord_handle(self, update, context):
 
         add_handle_msg = f"""
         Please enter your Discord username (i.e. {rand_user}).
-        You can find it by tapping your avatar or in _*settings*_ -> _*my account*_ -> _*username*_.
+        You can find it by tapping your avatar or in _*settings -> my account -> username*_.
         """
-        self.send_msg(add_handle_msg, update, context)
+
+        # create button menu & setup text handler to store user information
+        self.save_to_users = 'discord_username'
+        buttons = ['enter Discord username']
+        self.button_choice(add_handle_msg, buttons, update, context)
+
         return
 
     def add_user(self, update, context):
@@ -326,10 +404,12 @@ def delete_user(self, update, context):
             msg = 'All data wiped!'
             self.send_msg(msg, update, context)
             self.refresh_discord_bot()
+            self.start_dialogue(update, context)
 
         else:
             msg = 'User data has already been deleted previously.'
             self.send_msg(msg, update, context)
+
         return
 
     def show_donate(self, update, context):
@@ -364,6 +444,19 @@ def handle_text_messages(self, update, context):
             chat_user_client = update.message.chat_id
             logging.info(f'{chat_user_client} interacted with the bot.')
 
+        # Check if user data from prompt is expected
+        if (self.save_to_users != False) and (hasattr(update, 'callback_query')):
+            # Add user data to appropriate key in self.users
+            user_id = update.message.chat_id
+            k = self.save_to_users
+            v = update.message.text
+            self.users[user_id][k] = v
+            self.save_to_users = False
+            write_to_json(self.users, self.users_path)
+            self.refresh_discord_bot()
+            success_msg = f'Success! Discord bot updated with data: {v}\n Show /menu'
+            self.send_msg(success_msg, update, context)
+
         # Possibility: received command from menu_trigger
         for Trigger in self.menu_trigger:
             for word in words:
@@ -378,6 +471,7 @@ def handle_text_messages(self, update, context):
             if word in self.command_map:
                 self.command_map[word](update, context)
 
+
 def main():
     """
     Entry point of the script. If run directly, instantiates the