aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorjantuomi <jans.tuomi@gmail.com>2016-07-19 14:20:36 +0300
committerjantuomi <jans.tuomi@gmail.com>2016-07-19 14:20:36 +0300
commit50779b5ecc55f906bcfd46e54dad70b2c74d3a98 (patch)
treec3627888448c9e49f32df8670596758f347427b4
parenteacb313e19805fd8d4bbba1b3515aa9adcd897cf (diff)
Code cleanup
-rw-r--r--basecommunicator.py10
-rw-r--r--botmanager.py127
-rw-r--r--jsonfactory.py6
-rw-r--r--mockshoutbox.py25
-rwxr-xr-xradiodiodibot.py23
-rw-r--r--shoutboxapicommunicator.py20
-rw-r--r--telegramapicommunicator.py47
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...")