From d8489d48d05b01625e4f4e530276ce2429825382 Mon Sep 17 00:00:00 2001 From: Edoardo Morassutto Date: Fri, 13 Nov 2020 15:20:58 +0100 Subject: [PATCH] Telegram Bot for notifying new questions During a contest it might be useful to be notified on the smartphone when a new question arrives. This is especially true for long contests where keeping the admin page open is not really feasible. This patch sends a Telegram message to a chat when a new question arrives and it is approved (i.e. not too long). It just uses "requests" for interacting with the Telgram API, avoiding additional dependencies and it's disabled by default. The actual notification is sent in a separate greenlet avoiding to slow down the request handler if the Internet connection or the Telegram servers are slow. To enable the bot you have to set the Telegram token and the chat id in cms.conf (`telegram_bot_token` and `telegram_bot_chat_id`). --- cms/conf.py | 9 +++++ cms/server/contest/communication.py | 52 +++++++++++++++++++++++++++++ config/cms.conf.sample | 7 ++++ 3 files changed, 68 insertions(+) diff --git a/cms/conf.py b/cms/conf.py index 9ccc2b8e74..1b1e5f5ee5 100644 --- a/cms/conf.py +++ b/cms/conf.py @@ -142,6 +142,8 @@ def __init__(self): # necessary to change it. # [1] http://freedesktop.org/wiki/Software/shared-mime-info self.shared_mime_info_prefix = "/usr" + self.telegram_bot_token = None + self.telegram_bot_chat_id = None # AdminWebServer. self.admin_listen_address = "" @@ -202,6 +204,13 @@ def __init__(self): # Attempt to load a config file. self._load(paths) + if bool(self.telegram_bot_token) ^ bool(self.telegram_bot_chat_id): + raise ConfigError("Both telegram_bot_token and telegram_bot_chat_id " + "should be set or left null") + if self.telegram_bot_chat_id: + if type(self.telegram_bot_chat_id) != int: + raise ConfigError("telegram_bot_chat_id should be an integer") + # If the configuration says to print detailed log on stdout, # change the log configuration. set_detailed_logs(self.stream_log_detailed) diff --git a/cms/server/contest/communication.py b/cms/server/contest/communication.py index aee44fe7c4..93a46aea68 100644 --- a/cms/server/contest/communication.py +++ b/cms/server/contest/communication.py @@ -29,6 +29,10 @@ import logging +import gevent +import requests + +from cms import config from cms.db import Question, Announcement, Message from cmscommon.datetime import make_timestamp @@ -57,6 +61,49 @@ def __init__(self, subject, text, text_params=None): self.text_params = text_params +def send_telegram_message(content): + """Send a message as the Telegram bot with the specified content. + """ + api_url = "https://api.telegram.org/bot{}/sendMessage" \ + .format(config.telegram_bot_token) + body = {"chat_id": config.telegram_bot_chat_id, + "text": content, + "parse_mode": "HTML"} + res = requests.post(api_url, data=body) + if res.status_code != 200: + logger.warn("Failed to send Telegram notification: %s", res.json()) + else: + logger.debug("Telegram notification sent for the question") + + +def send_telegram_question_notification(question): + """Craft and send a Telegram message with the question content. + + This is a no-op if the bot is not configured. + + This also takes care of formatting the message to be sent, escaping + it and making sure it doesn't exceed the bot message size limit + (4096 bytes). + + The message is sent in a different greenlet, so that it doesn't + block the caller if the network is slow. + """ + if not config.telegram_bot_token or not config.telegram_bot_chat_id: + return + trim = lambda s, n: s[:n] + "…" if len(s) > n else s + escape = lambda t: t.replace("<", "<").replace(">", ">") + message = """New question +From: {username} +Subject: {subject} + +{text} +""".format( + username=trim(escape(question.participation.user.username), 50), + subject=trim(escape(question.subject), 100), + text=trim(escape(question.text), 3000)) + gevent.spawn(send_telegram_message, message) + + def accept_question(sql_session, participation, timestamp, subject, text): """Add a contestant-submitted question to the database. @@ -100,6 +147,11 @@ def accept_question(sql_session, participation, timestamp, subject, text): logger.info("Question submitted by user %s.", participation.user.username) + try: + send_telegram_question_notification(question) + except Exception as e: + logger.exception("Error occurred while sending Telegram notification") + return question diff --git a/config/cms.conf.sample b/config/cms.conf.sample index 8bb0d730b8..b03a3612b7 100644 --- a/config/cms.conf.sample +++ b/config/cms.conf.sample @@ -133,6 +133,13 @@ "_help": "STL documentation path in the system (exposed in CWS).", "stl_path": "/usr/share/cppreference/doc/html/", + "_help": "Telegram bot configuration for posting notifications for the", + "_help": "questions. The token is a string obtained from @BotFather and", + "_help": "the chat_id (an int) is the chat id of a conversation where the", + "_help": "bot has access to: a chat where the user started the bot or a", + "_help": "channel where the bot has been authorized.", + "telegram_bot_token": null, + "telegram_bot_chat_id": null, "_section": "AdminWebServer",