From 9d94a01b631676b54f680c2571ae4dbba9ccd037 Mon Sep 17 00:00:00 2001 From: jantuomi Date: Tue, 19 Jul 2016 14:50:22 +0300 Subject: Restructure project --- project/__init__.py | 0 project/basecommunicator.py | 10 +++ project/botmanager.py | 135 +++++++++++++++++++++++++++++++++++++ project/jsonfactory.py | 27 ++++++++ project/mockshoutbox.py | 79 ++++++++++++++++++++++ project/shoutboxapicommunicator.py | 41 +++++++++++ project/telegramapicommunicator.py | 47 +++++++++++++ 7 files changed, 339 insertions(+) create mode 100644 project/__init__.py create mode 100644 project/basecommunicator.py create mode 100644 project/botmanager.py create mode 100644 project/jsonfactory.py create mode 100644 project/mockshoutbox.py create mode 100644 project/shoutboxapicommunicator.py create mode 100644 project/telegramapicommunicator.py (limited to 'project') diff --git a/project/__init__.py b/project/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/project/basecommunicator.py b/project/basecommunicator.py new file mode 100644 index 0000000..f4f8337 --- /dev/null +++ b/project/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/project/botmanager.py b/project/botmanager.py new file mode 100644 index 0000000..805f20c --- /dev/null +++ b/project/botmanager.py @@ -0,0 +1,135 @@ +# -*- coding: utf-8 -*- + +import logging +import random +import sys +import time +import traceback + +import telepot + +from project.jsonfactory import JSONFactory +from project.shoutboxapicommunicator import ShoutboxCommunicator +from project.telegramapicommunicator import TelegramCommunicator + + +class BotManager(object): + """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): + """ + 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)) + TelegramCommunicator.spawn_bot(token) + logging.info("Bot succesfully created.") + except: + traceback.print_exc() + logging.error("Error creating bot listener. src will now exit...") + 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 + + def start(self): + """Try fetching bot information from Telegram to check connection""" + TelegramCommunicator.start_listening(self.handle) + + while True: + # Forward all new messages to the Telegram chat + # and add them to the message dict + self.forward_to_telegram(ShoutboxCommunicator.fetch()) + + # Wait for the update interval + time.sleep(ShoutboxCommunicator.interval) + + # Remove obsolete messages from the dict to prevent it + # from bloating + self.clean_up_message_dict() + + 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: + + # 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: + TelegramCommunicator.send(message) + self.last_message_timestamps[message["id"]] = message["timestamp"] + + except: + logging.warning("Failed to send message to Telegram!") + + songs = ["Ace of Spades", "Mökkitie", "Alpha Russian XXL Night Mixtape", "teekkarihymni"] + + def action_now_playing(self, msg): + """Placeholder action for testing commands""" + TelegramCommunicator.send_raw("Radiossa soi {}!".format(random.choice(BotManager.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 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.action_now_playing(msg) + elif "/start" in t: + self.action_not_supported(msg) + elif "/stop" in t: + 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)) + + 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': + self.parse_message(msg) diff --git a/project/jsonfactory.py b/project/jsonfactory.py new file mode 100644 index 0000000..c57b430 --- /dev/null +++ b/project/jsonfactory.py @@ -0,0 +1,27 @@ +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, + "user": user, + "timestamp": timestamp, + "ip": ip + }) + JSONFactory.running_id += 1 + return message + + @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/project/mockshoutbox.py b/project/mockshoutbox.py new file mode 100644 index 0000000..4a64406 --- /dev/null +++ b/project/mockshoutbox.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python + +import sys +import http.server +import traceback +from http.server import SimpleHTTPRequestHandler +import json + +import time + + +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 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": 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"])) + self.send_response(200) + + except: + traceback.print_exc() + self.send_response(400) + + self.end_headers() + + return + +HandlerClass = MyHandler +ServerClass = http.server.HTTPServer +Protocol = "HTTP/1.0" + +if sys.argv[1:]: + port = int(sys.argv[1]) +else: + port = 8000 +server_address = ('127.0.0.1', port) + +HandlerClass.protocol_version = Protocol +httpd = ServerClass(server_address, HandlerClass) + +sa = httpd.socket.getsockname() +print("Serving HTTP on", sa[0], "port", sa[1], "...") +httpd.serve_forever() diff --git a/project/shoutboxapicommunicator.py b/project/shoutboxapicommunicator.py new file mode 100644 index 0000000..7194c8f --- /dev/null +++ b/project/shoutboxapicommunicator.py @@ -0,0 +1,41 @@ +import json +import logging + +import requests + +from project.basecommunicator import BaseCommunicator + + +class ShoutboxCommunicator(BaseCommunicator): + """Class for communication with the shoutbox API""" + + url = "http://localhost:8000" + interval = 10 + + @staticmethod + def fetch(): + """Get and return a list of new messages from the API""" + try: + r = requests.get(ShoutboxCommunicator.url) + logging.info("Response from API OK.") + + except: + logging.warning("Failed to get response from API!") + return + + content = json.loads(r.text) + logging.info("Messages:") + for msg in content: + logging.info("{}: {}".format(msg["user"], msg["text"])) + + return content + + @staticmethod + def send(data): + """Send a JSON message to the API""" + try: + 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!") diff --git a/project/telegramapicommunicator.py b/project/telegramapicommunicator.py new file mode 100644 index 0000000..7fdb29c --- /dev/null +++ b/project/telegramapicommunicator.py @@ -0,0 +1,47 @@ +import logging +import sys + +import telepot + +from project.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...") -- cgit v1.3