diff options
| -rw-r--r-- | basecommunicator.py | 10 | ||||
| -rw-r--r-- | botmanager.py | 127 | ||||
| -rw-r--r-- | jsonfactory.py | 6 | ||||
| -rw-r--r-- | mockshoutbox.py | 25 | ||||
| -rwxr-xr-x | radiodiodibot.py | 23 | ||||
| -rw-r--r-- | shoutboxapicommunicator.py | 20 | ||||
| -rw-r--r-- | telegramapicommunicator.py | 47 |
7 files changed, 183 insertions, 75 deletions
diff --git a/basecommunicator.py b/basecommunicator.py new file mode 100644 index 0000000..f4f8337 --- /dev/null +++ b/basecommunicator.py @@ -0,0 +1,10 @@ +class BaseCommunicator: + """Parent class for Telegram and shoutbox API communicators""" + + @staticmethod + def fetch(): + ... + + @staticmethod + def send(data): + ... diff --git a/botmanager.py b/botmanager.py index e9b65a2..8cfe949 100644 --- a/botmanager.py +++ b/botmanager.py @@ -8,104 +8,125 @@ import time import logging from jsonfactory import JSONFactory -from shoutboxapicommunicator import Communicator - -class ObsoleteMessageException(Exception): - pass +from shoutboxapicommunicator import ShoutboxCommunicator +from telegramapicommunicator import TelegramCommunicator class BotManager(object): - shoutbox_api_url = "http://localhost:8000" - telegram_chat_id = "default_id" - api_call_interval = 10 + """Manager class for the process""" + # Make a dict of ( message id : timestamp ) pairs to + # keep track of sent messages. + # Messages older than 2 * update interval will be forgotten. last_message_timestamps = {} def __init__(self, token): - self.token = token + """ + Attempt to create a bot with telepot - # Attempt to create a bot with telepot + If not possible, crash gracefully + """ + TelegramCommunicator.token = token try: logging.info("Creating bot listener with token {}...".format(token)) - self.bot = telepot.Bot(token) + TelegramCommunicator.spawn_bot(token) logging.info("Bot succesfully created.") except: traceback.print_exc() logging.error("Error creating bot listener. radiodiodibot will now exit...") sys.exit(1) - def start(self): - # Try fetching bot information from Telegram to check connection - try: - logging.info("Bot info: {}".format(self.bot.getMe())) - except: - logging.error("Could not fetch bot info. Check your token and connectivity to Telegram!") - sys.exit(1) + def set_parameters(self, shoutbox_api_url, telegram_chat_id, api_call_interval): + """Store parameters in the manager instance""" + ShoutboxCommunicator.url = shoutbox_api_url + TelegramCommunicator.chat_id = telegram_chat_id + ShoutboxCommunicator.interval = api_call_interval - self.bot.message_loop(self.handle) + def start(self): + """Try fetching bot information from Telegram to check connection""" + TelegramCommunicator.start_listening(self.handle) - logging.info("Listening for messages...") while True: - self.forward_to_telegram(Communicator.fetch(self.shoutbox_api_url)) - time.sleep(self.api_call_interval) + # Forward all new messages to the Telegram chat + # and add them to the message dict + self.forward_to_telegram(ShoutboxCommunicator.fetch()) - new_message_dict = {} + # Wait for the update interval + time.sleep(ShoutboxCommunicator.interval) - for msg_id in self.last_message_timestamps: + # Remove obsolete messages from the dict to prevent it + # from bloating + self.clean_up_message_dict() - # if the message is older than 2 * call interval, - # pop it from the buffer because there is no way - # it can be a duplicate anymore - timestamp = round(float(self.last_message_timestamps[msg_id])) - if not round(time.time()) - timestamp < 2 * self.api_call_interval: - new_message_dict[msg_id] = self.last_message_timestamps[msg_id] - else: - logging.info("Popped message from buffer.") + def clean_up_message_dict(self): + """Purge all obsolete messages from the message dict""" + new_message_dict = {} + for msg_id in self.last_message_timestamps: - self.last_message_timestamps = new_message_dict + # if the message is older than 2 * call interval, + # pop it from the buffer because there is no way + # it can be a duplicate anymore + timestamp = round(float(self.last_message_timestamps[msg_id])) + if not round(time.time()) - timestamp < 2 * ShoutboxCommunicator.interval: + new_message_dict[msg_id] = self.last_message_timestamps[msg_id] + else: + logging.info("Popped message from buffer.") + self.last_message_timestamps = new_message_dict def forward_to_telegram(self, messages): + """Send all messages in the messages list to the Telegram chat""" try: for message in messages: + # Do not send messages that have already been sent + # This check is in place because of possible artifacts in API calls if message["id"] not in self.last_message_timestamps: - self.bot.sendMessage(self.telegram_chat_id, - "{}: {}".format(message["user"], message["text"])) + TelegramCommunicator.send(message) self.last_message_timestamps[message["id"]] = message["timestamp"] except: logging.warning("Failed to send message to Telegram!") - def default_action(self, chat_id): - self.bot.sendMessage(chat_id, "Radio palaa keväällä 2017!") + songs = ["Ace of Spades", "Mökkitie", "Alpha Russian XXL Night Mixtape", "teekkarihymni"] - # Placeholder action for testing commands - def now_playing(self, chat_id): + def action_now_playing(self, msg): + """Placeholder action for testing commands""" + TelegramCommunicator.send_raw("Radiossa soi {}!".format(random.choice(BotManager.songs))) - songs = ["Ace of Spades", "Mökkitie", "Alpha Russian XXL Night Mixtape", "teekkarihymni"] - self.bot.sendMessage(chat_id, "Radiossa soi {}!".format(random.choice(songs))) + def action_not_supported(self, msg): + """Notify the user that the given command is not supported""" + TelegramCommunicator.send_raw("Tätä toimintoa ei ole tuettu.") - def not_supported(self, chat_id): - self.bot.sendMessage(chat_id, "Tätä toimintoa ei ole tuettu.") - - # Determine which action to take def parse_message(self, msg): + """Determine which action to take for an incoming Telegram text message""" t = msg["text"] content_type, chat_type, chat_id = telepot.glance(msg) if "/nowplaying" in t: - self.now_playing(chat_id) + self.action_now_playing(msg) elif "/start" in t: - self.not_supported(chat_id) + self.action_not_supported(msg) elif "/stop" in t: - self.not_supported(chat_id) - elif str(chat_id) == self.telegram_chat_id.strip(): - user_name = msg["from"]["first_name"] - data = JSONFactory.make_object(t, user_name, msg["date"], "null") - logging.info("Created JSON packet:\n{}".format(data)) - Communicator.send(self.shoutbox_api_url, data) + self.action_not_supported(msg) + elif str(chat_id) == TelegramCommunicator.chat_id.strip(): + self.action_send_to_shoutbox(msg) else: logging.info("Got non-forwarded message from chat_id: {}".format(chat_id)) - # Start parsing the incoming message if it is a text message + def action_send_to_shoutbox(self, msg): + """Forward a Telegram message to shoutbox""" + try: + user_name = msg["from"]["first_name"] + text = msg["text"] + date = msg["date"] + except KeyError: + logging.warning("Malformed message from Telegram! Details:\n{}".format(msg)) + logging.warning("Skipped sending message to shoutbox.") + return + + data = JSONFactory.make_object(text, user_name, date, "null") + logging.info("Created JSON packet:\n{}".format(data)) + ShoutboxCommunicator.send(data) + def handle(self, msg): + """Start parsing the incoming message if it is a text message""" content_type, chat_type, chat_id = telepot.glance(msg) if content_type == 'text': diff --git a/jsonfactory.py b/jsonfactory.py index 6703ef2..c57b430 100644 --- a/jsonfactory.py +++ b/jsonfactory.py @@ -2,10 +2,15 @@ import json class JSONFactory(object): + """Factory class to create JSON messages to be sent to the shoutbox API""" running_id = 1 @staticmethod def make_object(text, user, timestamp, ip): + """ + Create a JSON object from the passed parameters and + add a running id + """ message = json.dumps({ "id": JSONFactory.running_id, "text": text, @@ -18,4 +23,5 @@ class JSONFactory(object): @staticmethod def make_array(object_list): + """Create a JSON array from a list of JSON objects""" return "[\n" + ",\n".join(object_list) + "\n]" diff --git a/mockshoutbox.py b/mockshoutbox.py index da8e48a..4a64406 100644 --- a/mockshoutbox.py +++ b/mockshoutbox.py @@ -8,40 +8,49 @@ import json import time -''' -Mock HTTP server to test -shoutbox api requests. -''' class MyHandler(SimpleHTTPRequestHandler): + """ + Mock shoutbox HTTP server to test + shoutbox API requests. + """ def do_HEAD(self): self.send_response(200) self.send_header("Content-type", "application/json") self.end_headers() def do_GET(self): - """Respond to a GET request.""" + """ + Respond to a GET with a JSON array containing one object + that contains the following: + + test user: test message + """ + self.send_response(200) self.send_header("Content-type", "application/json") self.end_headers() + timestamp = str(round(time.time())) body = json.dumps([{ "user": "test user", "text": "test message", - "timestamp": str(time.time()), - "id": time.time(), + "timestamp": timestamp, + "id": timestamp, "ip": "1.2.3.4" }]) self.wfile.write(body.encode()) def do_POST(self): + """ + Respond to a POST by printing the message contents to console + """ content_len = int(self.headers.get('Content-Length')) post_data = self.rfile.read(content_len) try: message = json.loads(post_data.decode('utf-8')) print("{}: {}".format(message["user"], message["text"])) - # Begin the response self.send_response(200) except: diff --git a/radiodiodibot.py b/radiodiodibot.py index 8533aa9..8499e90 100755 --- a/radiodiodibot.py +++ b/radiodiodibot.py @@ -4,6 +4,15 @@ import sys import argparse import botmanager import logging +import signal + + +def sigint_handler(signal, frame): + """Exit gracefully when receiving an interrupt signal""" + print("Exiting radiodiodibot...") + sys.exit(0) + +signal.signal(signal.SIGINT, sigint_handler) try: # Python 2.7+ from logging import NullHandler @@ -14,7 +23,8 @@ except ImportError: logging.getLogger(__name__).addHandler(NullHandler()) -# Read configs +# Read configs, unsuccessful reads are silently +# ignored CONFIG_FILE = "bot.config" config = configparser.ConfigParser() config.read(CONFIG_FILE) @@ -28,6 +38,7 @@ parser.add_argument("-v", "--verbose", help="Verbose output", action="store_true parser.add_argument("-i", "--interval", help="Delay between API update calls in seconds", type=int, default=10) args = parser.parse_args() +# Show verbose output with the -v option if args.verbose: logging_level = logging.INFO else: @@ -71,13 +82,11 @@ def main(): # Store the final values in a manager instance manager = botmanager.BotManager(telegram_bot_token) - manager.shoutbox_api_url = shoutbox_api_url - manager.telegram_chat_id = telegram_chat_id - manager.api_call_interval = api_call_interval + manager.set_parameters(shoutbox_api_url, telegram_chat_id, api_call_interval) - logging.info("Using Shoutbox API URL: {}".format(manager.shoutbox_api_url)) - logging.info("Using Telegram Chat ID: {}".format(manager.telegram_chat_id)) - logging.info("Using API call interval of {} seconds.".format(manager.api_call_interval)) + logging.info("Using Shoutbox API URL: {}".format(shoutbox_api_url)) + logging.info("Using Telegram Chat ID: {}".format(telegram_chat_id)) + logging.info("Using API call interval of {} seconds.".format(api_call_interval)) # Start listening manager.start() diff --git a/shoutboxapicommunicator.py b/shoutboxapicommunicator.py index 28a5b74..7138aa9 100644 --- a/shoutboxapicommunicator.py +++ b/shoutboxapicommunicator.py @@ -1,15 +1,20 @@ import json - import requests import logging +from basecommunicator import BaseCommunicator + +class ShoutboxCommunicator(BaseCommunicator): + """Class for communication with the shoutbox API""" -class Communicator(object): + url = "http://localhost:8000" + interval = 10 @staticmethod - def fetch(url): + def fetch(): + """Get and return a list of new messages from the API""" try: - r = requests.get(url) + r = requests.get(ShoutboxCommunicator.url) logging.info("Response from API OK.") except: @@ -24,10 +29,11 @@ class Communicator(object): return content @staticmethod - def send(url, data): + def send(data): + """Send a JSON message to the API""" try: - requests.post(url, data) + requests.post(ShoutboxCommunicator.url, data) user = json.loads(data)["user"] logging.info("Sent message from {} to shoutbox.".format(user)) except: - logging.warning("Failed to send message to shoutbox API!")
\ No newline at end of file + logging.warning("Failed to send message to shoutbox API!") diff --git a/telegramapicommunicator.py b/telegramapicommunicator.py new file mode 100644 index 0000000..169a1ce --- /dev/null +++ b/telegramapicommunicator.py @@ -0,0 +1,47 @@ +import logging + +import sys +import telepot + +from basecommunicator import BaseCommunicator + + +class TelegramCommunicator(BaseCommunicator): + """Class for communicating with the Telegram API""" + + chat_id = "default_id" + bot = None + token = "DEFAULT_TOKEN_123" + + @staticmethod + def spawn_bot(token): + TelegramCommunicator.bot = telepot.Bot(token) + + @staticmethod + def fetch(): + raise Exception("""Attempted to fetch messages from Telegram manually. + Please use telepot bot functionality for listening to messages.""") + + @staticmethod + def send(data): + TelegramCommunicator.send_raw("{}: {}".format(data["user"], data["text"])) + + @staticmethod + def send_raw(message): + try: + TelegramCommunicator.bot.sendMessage(TelegramCommunicator.chat_id, + message) + except: + logging.warning("Could not send message to Telegram chat id {}!".format(TelegramCommunicator.chat_id)) + + @staticmethod + def start_listening(handle): + try: + logging.info("Bot info: {}".format(TelegramCommunicator.bot.getMe())) + except: + logging.error("Could not fetch bot info. Check your token and connectivity to Telegram!") + sys.exit(1) + + TelegramCommunicator.bot.message_loop(handle) + + logging.info("Listening for messages...") |
