From a01350a34a2a4eb4789420d042078a3affeebb76 Mon Sep 17 00:00:00 2001 From: alufers Date: Tue, 5 Dec 2023 12:17:00 +0100 Subject: [PATCH 1/3] Bump python-telegram-bot to latest version --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 65af88c..8488bd0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ decorator==4.4.2 idna==2.10 phabricator==0.8.0 pycparser==2.20 -python-telegram-bot==13.0 +python-telegram-bot==20.7 pytz==2020.4 requests==2.25.0 six==1.15.0 From 31cddd5959c60ded372b3ca801daa29636937998 Mon Sep 17 00:00:00 2001 From: alufers Date: Tue, 5 Dec 2023 12:18:48 +0100 Subject: [PATCH 2/3] Improve bot UX - If no argument provided, follow up with a force reply message - Properly parse title/description. Allow markdown in description, remove command from title - Before creating task ask user for confirmation - Handle errors while creating task --- main.py | 277 ++++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 209 insertions(+), 68 deletions(-) diff --git a/main.py b/main.py index c55e90a..3d97903 100644 --- a/main.py +++ b/main.py @@ -2,73 +2,214 @@ import logging from pprint import pprint from functools import wraps +from typing import Optional, Union from phabricator import Phabricator -from telegram import ParseMode -from telegram.ext import Updater, CommandHandler - +from telegram import ( + Message, + Update, + ForceReply, + InlineKeyboardMarkup, + InlineKeyboardButton, +) +from telegram.ext import ( + Application, + ApplicationBuilder, + Updater, + CommandHandler, + CallbackContext, + CallbackQueryHandler, + MessageHandler, + filters, +) +from telegram.constants import ParseMode + +import json import config - - -def create_task(title, description): - transactions = [ - {'type': 'title', 'value': title}, - {'type': 'description', 'value': description}, - ] - - # @TODO support exceptions/errors/blah :P - - phab = Phabricator(host=config.PHABRICATOR_URL_API, token=config.PHABRICATOR_TOKEN) - result = phab.maniphest.edit(transactions=transactions) - return result - - -def handler_add_task(update, context): - if update.message.chat and update.message.chat.title != config.TELEGRAM_CHAT_NAME: - update.message.reply_text('Niedozwolona grupa czatu!') - return - - if len(context.args) < 3: - update.message.reply_text('Podaj, proszę, tytuł jako argument') - return - - msg_text = update.message.text_markdown.lstrip('/add\\_task').strip() # need to work on this, because context.args doesn't indicate new lines - first_newline = msg_text.find('\n') - - title = '' - description = '' - - if first_newline == -1: - title = msg_text.capitalize() - else: - title = msg_text[:first_newline].capitalize() - description = msg_text[first_newline + 1:].strip() - description += '\n\n' - - description += ' *Dodane przez:* {} '.format(update.message.from_user.name) - description += '\nlink do wiadomości: {} '.format(update.message.link) - - result = create_task(title=title, description=description) - task_id = result.object['id'] - - url = '{}T{}'.format(config.PHABRICATOR_URL, task_id) - reply = '*T{}: {}* ({})'.format(task_id, title, url) - - update.message.reply_text(reply, parse_mode=ParseMode.MARKDOWN) - - -def error_callback(update, context): - pprint(context.error) - - -if __name__ == '__main__': - logging.basicConfig(level=logging.DEBUG) - - telegram_updater = Updater(config.TELEGRAM_TOKEN) - telegram_dispatcher = telegram_updater.dispatcher - - telegram_dispatcher.add_handler(CommandHandler('add_task', handler_add_task, pass_args=True)) - - telegram_dispatcher.add_error_handler(error_callback) - - telegram_updater.start_polling() - telegram_updater.idle() +import random +import string +import re + + +def create_task(title: str, description: str): + transactions = [ + {"type": "title", "value": title}, + {"type": "description", "value": description}, + ] + + phab = Phabricator(host=config.PHABRICATOR_URL_API, token=config.PHABRICATOR_TOKEN) + result = phab.maniphest.edit(transactions=transactions) + return result + + +def extract_title_and_description( + update: Union[str, "Update"] +) -> (Optional[str], Optional[str]): + msg_text = re.sub( + r"^\s*\/add_task(@\w+)?\s*", "", update.message.text, flags=re.IGNORECASE + ) + + first_newline = msg_text.find("\n") + + title = "" + description = None + + if first_newline == -1: + title = msg_text.capitalize().strip() + else: + title = msg_text[:first_newline].capitalize() + # extract the description from text_markdown separately, + # because Phabricator supports markdown in the description, but not in the title + msg_text_markdown = update.message.text_markdown + first_newline = msg_text_markdown.find("\n") + description = msg_text_markdown[first_newline:].strip() + description += "\n\n" + if not title: + return None, None + + return title, description + + +message_ids_waiting_for_reply: dict[int, Message] = {} + + +def gen_id() -> str: + return "".join(random.choice(string.ascii_lowercase) for i in range(15)) + + +tasks_awaiting_confirmation: dict[str, dict] = {} + + +async def handler_add_task(update: Union[str, "Update"], context: CallbackContext): + global message_ids_waiting_for_reply + if update.message.chat and update.message.chat.title != config.TELEGRAM_CHAT_NAME: + await update.message.reply_text("Niedozwolona grupa czatu!") + return + + title, description = extract_title_and_description(update) + print([title, description]) + if not title: + reply_msg = await update.message.reply_text( + "Proszę podaj tytuł (oraz opcjonalnie opis w nowej linii):", + reply_markup=ForceReply( + selective=True, + input_field_placeholder="Tytuł zadania i opcjonalny opis w nowej linii", + ), + ) + message_ids_waiting_for_reply[reply_msg.message_id] = reply_msg + return + if not description: + description = "" + + description += "*Dodane przez:* {} ".format(update.message.from_user.name) + description += "\nlink do wiadomości: {} ".format(update.message.link) + + confirmation_id = gen_id() + tasks_awaiting_confirmation[confirmation_id] = { + "title": title, + "description": description, + } + + await update.message.reply_markdown( + """Podgląd zadania: +*Tytuł:* {} +*Opis:* +{} + +Czy chcesz dodać zadanie?""".format( + title, description + ), + reply_markup=InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton( + "✅ Dodaj zadanie", + # callback_data=json.dumps({ + # "action": "add_task", + # "title": title, + # "description": description, + # }), + callback_data="ok " + confirmation_id, + ), + InlineKeyboardButton( + "❌ Anuluj", callback_data="cancel " + confirmation_id + ), + ] + ] + ), + ) + + +async def callback_query_handler(update: Update, context: CallbackContext): + data = update.callback_query.data + op, confirmation_id = data.split(" ", 1) + if op == "ok": + if confirmation_id not in tasks_awaiting_confirmation: + return + task = tasks_awaiting_confirmation.pop(confirmation_id) + result = None + try: + result = create_task(title=task["title"], description=task["description"]) + except Exception as e: + await update.callback_query.edit_message_text( + "Wystąpił błąd podczas dodawania zadania: {}".format(e) + ) + # print error and traceback + logging.exception("Error while creating task", exc_info=e) + return + task_id = result.object["id"] + + url = "{}T{}".format(config.PHABRICATOR_URL, task_id) + reply = "*T{}: {}* ({})".format(task_id, task["title"], url) + + await update.callback_query.edit_message_text( + reply, parse_mode=ParseMode.MARKDOWN + ) + await update.callback_query.edit_message_reply_markup( + InlineKeyboardMarkup( + [[InlineKeyboardButton("🔗 T{}".format(task_id), url=url)]] + ) + ) + elif op == "cancel": + if confirmation_id not in tasks_awaiting_confirmation: + return + tasks_awaiting_confirmation.pop(confirmation_id) + await update.callback_query.edit_message_text("Anulowano dodawanie zadania") + else: + await update.callback_query.edit_message_text( + "Nieznana operacja: {}".format(op) + ) + + +async def message_handler(update: Union[str, "Update"], context: CallbackContext): + if ( + update.message.reply_to_message + and update.message.reply_to_message.message_id in message_ids_waiting_for_reply + ): + # delete the message from the chat + await update.message.reply_to_message.delete() + message_ids_waiting_for_reply.pop(update.message.reply_to_message.message_id) + await handler_add_task(update, context) + return + + +def error_callback(update, context: CallbackContext): + pprint(context.error) + + +async def post_init(application: Application) -> None: + await application.bot.set_my_commands( + [("add_task", "Dodaj zadanie do Phabricatora")] + ) + + +if __name__ == "__main__": + # logging.basicConfig(level=logging.DEBUG) + + app = ApplicationBuilder().token(config.TELEGRAM_TOKEN).post_init(post_init).build() + + app.add_handler(CommandHandler("add_task", handler_add_task)) + app.add_handler(MessageHandler(filters.TEXT, message_handler, True)) + app.add_handler(CallbackQueryHandler(callback_query_handler)) + app.add_error_handler(error_callback) + + app.run_polling() From 95116e2a8b3a659dcc7d79a1e96e0493c4ef859b Mon Sep 17 00:00:00 2001 From: alufers Date: Tue, 5 Dec 2023 13:15:03 +0100 Subject: [PATCH 3/3] Bump container base image to bookworm --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index dcf32fe..db93149 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3-buster +FROM python:3-bookworm WORKDIR /app