diff --git a/.gitignore b/.gitignore index e4ae184..b0c2ce1 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,5 @@ nosetests.xml conf/*.conf nohup.out + +conf/bot_.py diff --git a/pancake.py b/pancake.py index 37eca4f..426d607 100755 --- a/pancake.py +++ b/pancake.py @@ -19,12 +19,7 @@ conf_name = args.config conf = library.config.Settings(conf_name) - bot = library.Bot( - conf.general['api_token'], - name=conf.general['bot_name'], - aws=conf.aws, - gecko=conf.geckoboard - ) - - bot.joinRooms(conf.general['rooms']) + bot = library.Bot(conf) + + bot.join_rooms(conf.general['rooms']) bot.start() diff --git a/plugins/arnold/__init__.py b/plugins/arnold/__init__.py new file mode 100644 index 0000000..8b8f36a --- /dev/null +++ b/plugins/arnold/__init__.py @@ -0,0 +1,23 @@ +import random + +from arnold_phrases import ARNOLD_PHRASES + +class ArnoldPlugin(): + + help = "Arnold Schwarzenegger's phrase (/! name)" + command = "!" + + @staticmethod + def response(mentioned_user, random_user): + + phrase = random.choice(ARNOLD_PHRASES) + + if "{}" not in phrase: + phrase = "{}, " + phrase + + username = mentioned_user + + if not username: + username = random_user + + return phrase.format(username) \ No newline at end of file diff --git a/src/_arnold_phrases.py b/plugins/arnold/arnold_phrases.py similarity index 100% rename from src/_arnold_phrases.py rename to plugins/arnold/arnold_phrases.py diff --git a/plugins/cats.py b/plugins/cats.py new file mode 100644 index 0000000..08f56c1 --- /dev/null +++ b/plugins/cats.py @@ -0,0 +1,25 @@ +import re +import requests + +class CatGIFPlugin(): + + help = "Post a random cat gif" + command = "cat" + + def __init__(self): + self.RE_URL = re.compile("([^<]*)", re.MULTILINE + re.IGNORECASE) + + def response(self): + message = "Can't connect to cat API =(" + params = {'format': 'xml', 'type': 'gif'} + + r = requests.get('http://thecatapi.com/api/images/get', params=params) + + if r.status_code == 200: + response = r.text + + res = re.search(self.RE_URL, response) + if res: + message = res.groups()[0] + + return message \ No newline at end of file diff --git a/plugins/giphy.py b/plugins/giphy.py new file mode 100644 index 0000000..577ec70 --- /dev/null +++ b/plugins/giphy.py @@ -0,0 +1,22 @@ +import requests + +class GiphyPlugin(): + + help = "Get a random gif, with a optional search term (/gif keyboard cat)" + command = "gif" + + @staticmethod + def response(message): + search_values = message.split('/gif', 1) + tags = '' + if len(search_values) == 2: + tags = '+'.join(search_values[1].split()) + + r = requests.get('http://api.giphy.com/v1/gifs/random?api_key=dc6zaTOxFJmzC&tag=' + tags) + + if r.status_code == 200: + response = r.json() + return response['data']['image_url'] + + else: + return "Can't connect to giphy API =(" diff --git a/plugins/lego/__init__.py b/plugins/lego/__init__.py new file mode 100644 index 0000000..0547697 --- /dev/null +++ b/plugins/lego/__init__.py @@ -0,0 +1,23 @@ +import random + +from lego_quotes import LEGO_QUOTES + +class LegoPlugin(): + + help = "LEGO Movie quote's (/lego name)" + command = "lego" + + @staticmethod + def response(mentioned_user, random_user): + + phrase = random.choice(LEGO_QUOTES) + + if "{}" not in phrase: + phrase = "{}, " + phrase + + username = mentioned_user + + if not username: + username = random_user + + return phrase.format(username) \ No newline at end of file diff --git a/src/_lego_quotes.py b/plugins/lego/lego_quotes.py similarity index 100% rename from src/_lego_quotes.py rename to plugins/lego/lego_quotes.py diff --git a/src/aws_basic.py b/src/aws_basic.py deleted file mode 100644 index 8baf555..0000000 --- a/src/aws_basic.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: UTF-8 -*- - -class AWSBasic(): - - REGION = 'ap-southeast-2' - - ZONE = 'ap-southeast-2a' - RESERVED_ZONE = 'ap-southeast-2b' - - conn = False - - def __init__(self, aws_access_key_id, aws_secret_access_key, region=False): - if region: - self.REGION = region - - self.connect(aws_access_key_id, aws_secret_access_key, region) - - def connect(self, aws_access_key_id, aws_secret_access_key, region=False): - pass \ No newline at end of file diff --git a/src/bot.py b/src/bot.py index 1cf4548..9b64176 100644 --- a/src/bot.py +++ b/src/bot.py @@ -1,29 +1,20 @@ -import requests import re import random import inspect -import json -from time import time,sleep -from ec2_helper import EC2Helper -from simple_hipchat import HipChat +from time import time, sleep from urllib2 import HTTPError -# For Arnold -from _arnold_phrases import ARNOLD_PHRASES -from _lego_quotes import LEGO_QUOTES - +from plugin_loader import PluginLoader +from simple_hipchat import HipChat class Bot(): - RE_URL = re.compile("([^<]*)", re.MULTILINE + re.IGNORECASE) - EC2_DOWN_CODE = 80 - - ec2 = None - geckoboard = None rooms = None joined_rooms = {} actions = {} + plugins = [] + # hipchat simple interaction object hipster = None name = 'Pancake' @@ -33,7 +24,10 @@ class Bot(): rooms_users = {} get_users_timeout = 5 * 60 # 5 min - def __init__(self, api_token, name=None, aws=False, gecko=False): + def __init__(self, conf): + + api_token = conf.general['api_token'] + name = conf.general['bot_name'] self.hipster = HipChat(token=api_token) @@ -43,95 +37,54 @@ def __init__(self, api_token, name=None, aws=False, gecko=False): print('Error! API token not specified or invalid') exit() - if aws: - self.__awsInit(aws) - - self.gecko = gecko - if name: self.name = name - self.__setActions() + pl = PluginLoader(conf, '/plugins') + + self.plugins = pl.get_plugins() + + self.__set_actions() # Private # ================================================================== - def __setActions(self): + @staticmethod + def __get_latest_date(messages): + latest_date = '' + for message in messages: + if message['date'] > latest_date: + latest_date = message['date'] + + return latest_date + + @staticmethod + def __mention_user(username): + return '@' + username.replace(' ', '') + + def __set_actions(self): self.actions = { '/help': { - 'action': self.__cmdHelp, + 'action': self.__cmd_help, 'help': 'Show this help' }, - '/cat': { - 'action': self.__cmdGetRandomCatGIF, - 'help': 'Post random cat gif' - }, - '/staging': { - 'action': self.__cmdGetStagingStatus, - 'help': 'Get staging servers status' - }, - '/chuck': { - 'action': self.__cmdGetRandomChuckPhrase, - 'help': 'Post random Chuck\'s phrase' - }, - '/blame': { - 'action': self.__cmdBlameSomebody, - 'help': 'Blame somebody' - }, - '/pony': { - 'action': self.__cmdGetPony, - 'help': 'Post pony image' - }, - '/rps': { - 'action': self.__cmdRPS, - 'help': 'Rock - Paper - Scissors - Lizard - Spock (type /rps help)' - }, - '/!': { - 'action': self.__cmdArnold, - 'help': 'Arnold Schwarzenegger\'s phrase (/! name)' - }, - '/lego': { - 'action': self.__cmdLego, - 'help': 'LEGO Movie quote\'s (/lego name)' - }, - '/xkcd': { - 'action': self.__cmdXKCD, - 'help': 'Get random xkcd comics' - }, - '/gif': { - 'action': self.__cmdRandomGIF, - 'help': 'Get a random gif, with a optional search term (/gif keyboard cat)' - }, - '/roll': { - 'action': self.__cmdRoll, - 'help': 'Roll a random number 0 - 100' - }, - '/tell': { - 'action': self.__cmdTell, - 'help': 'Tell someone something (/tell name message or /tell name message from name)' - }, '/limit': { - 'action': self.__cmdGetLimit, + 'action': self.__cmd_get_limit, 'help': 'Get current HipChat API limit status' - }, - '/board': { - 'action': self.__cmdPostToBoard, - 'help': 'Post message to geckoboard' - }, - '/?': { - 'action': self.__cmdAsk, - 'help': 'Ask me a question' } } - def __awsInit(self, credentials): - if not ('secret_key' in credentials and 'access_key' in credentials): - return False + # load plugins + for plugin in self.plugins: + self.actions.update({ + '/' + plugin.command: { + 'action': plugin.response, + 'help': plugin.help + } + }) - self.ec2 = EC2Helper(credentials['access_key'], credentials['secret_key']) - - def __getMessages(self, room_name, last_date): + def __get_messages(self, room_name, last_date): msgs = self.hipster.method( 'rooms/history', method='GET', @@ -145,15 +98,7 @@ def __getMessages(self, room_name, last_date): return new_messages - def __getLatestDate(self, messages): - latest_date = '' - for message in messages: - if message['date'] > latest_date: - latest_date = message['date'] - - return latest_date - - def __getLatestDates(self): + def __get_latest_dates(self): last_dates = {} for room_name in self.joined_rooms: msgs = self.hipster.method( @@ -162,11 +107,11 @@ def __getLatestDates(self): parameters={'room_id': self.joined_rooms[room_name], 'date': 'recent'} )['messages'] - last_dates.update({room_name: self.__getLatestDate(msgs)}) + last_dates.update({room_name: self.__get_latest_date(msgs)}) return last_dates - def __getUsers(self, room_name): + def __get_users(self, room_name): if not room_name in self.rooms_users or self.rooms_users[room_name]['time'] + self.get_users_timeout < time(): users = self.hipster.method( 'rooms/show', @@ -181,18 +126,15 @@ def __getUsers(self, room_name): return self.rooms_users[room_name]['users'] - def __mentionUser(self, username): - return '@' + username.replace(' ', '') - - def __getRandomUser(self, room_name): + def __get_random_user(self, room_name): username = '@here' - user = random.choice(self.__getUsers(room_name)) + user = random.choice(self.__get_users(room_name)) if user: - username = self.__mentionUser(user['name']) + username = self.__mention_user(user['name']) return username - def __getMentionedUser(self, room_name, message): + def __get_mentioned_user(self, room_name, message): message_parts = message.split() username = None @@ -202,30 +144,17 @@ def __getMentionedUser(self, room_name, message): search = re.compile(search, re.IGNORECASE) - users = self.__getUsers(room_name) + users = self.__get_users(room_name) usernames = [user['name'] for user in users if re.search(search, user['name'])] if usernames: - username = self.__mentionUser(random.choice(usernames)) + username = self.__mention_user(random.choice(usernames)) return username - def __doQuoteAtUser(self, room_name, quoteList, message): - phrase = random.choice(quoteList) - - if "{}" not in phrase: - phrase = "{}, " + phrase - - username = self.__getMentionedUser(room_name, message) - # Default user to random if not found. - if not username: - username = self.__getRandomUser(room_name) - - self.postMessage(room_name, phrase.format(username)) - # Commands # ================================================================== - def __cmdHelp(self, room_name): + def __cmd_help(self): message = 'Pancake bot, here are available commands:\n' for action_name in self.actions.keys(): message += action_name @@ -233,220 +162,67 @@ def __cmdHelp(self, room_name): message += ' - ' + self.actions[action_name]['help'] message += '\n' - self.postMessage(room_name, message) - - def __cmdTell(self, room_name, message): - message_parts = message.split() - - recipient = self.__getMentionedUser(room_name, message_parts[1]) - if not recipient: # Default to just text. - recipient = message_parts[1] - - # Check for "from someone" at the end. - if message_parts[-2].lower() == 'from': - sender = self.__getMentionedUser(room_name, message_parts[-1]) - if not sender: # Default to just text. - sender = message_parts[-1] - - # Send message as from someone. - message_parts = message_parts - message = ' '.join(message_parts[2:-2]) - self.postMessage(room_name, "Hey {}, {} said {}".format(recipient, sender, message)) - else: - # Send message as from bot. - message = ' '.join(message_parts[2:]) - self.postMessage(room_name, "Hey {}, {}".format(recipient, message)) - - def __cmdPostToBoard(self, room_name, message): - message_parts = message.split(' ', 1) - - if not self.gecko: - return self.postMessage(room_name, 'Have no credentials for geckoboard') - - params = { - "api_key": self.gecko["api"], - "data": { "item":[{"text": message_parts[1],"type":0}] } - } - headers = {'Content-type': 'application/json'} - r = requests.post('https://push.geckoboard.com/v1/send/' + self.gecko["widget"], data=json.dumps(params), headers=headers) - - if r.status_code == 200: - response = r.json() - - if response['success']: - return self.postMessage(room_name, 'Message was posted to Geckoboard') - - return self.postMessage(room_name, 'Something went wrong') - - def __cmdGetRandomChuckPhrase(self, room_name): - message = "Can't connect to Chuck API =(" - params = {'limitTo': '[nerdy]'} - - r = requests.get('http://api.icndb.com/jokes/random', params=params) - - if r.status_code == 200: - response = r.json() - - if response['type'] == 'success': - message = response['value']['joke'] - - self.postMessage(room_name, message) - - def __cmdGetRandomCatGIF(self, room_name): - message = "Can't connect to cat API =(" - params = {'format': 'xml', 'type': 'gif'} - - r = requests.get('http://thecatapi.com/api/images/get', params=params) - - if r.status_code == 200: - response = r.text - - res = re.search(self.RE_URL, response) - if res: - message = res.groups()[0] - - self.postMessage(room_name, message) - - def __cmdGetStagingStatus(self, room_name): - - if not self.ec2: - self.postMessage(room_name, 'Can\'t connect to AWS API =(') - return False - - is_up = False - message = '' - short_status = 'Staging servers status: ' - - instances = self.ec2.getInstanceStatuses() - for instance in instances: - is_up = is_up or instance['state_code'] != self.EC2_DOWN_CODE - message += instance['name'] + ': ' + instance['state'] + '\n' - - if is_up: - short_status += 'RUNNING' - else: - short_status += 'STOPPED' - - self.postMessage(room_name, short_status) - self.postMessage(room_name, message) - - def __cmdBlameSomebody(self, room_name): - message = '{}, this is your fault!' - username = self.__getRandomUser(room_name) - - self.postMessage(room_name, message.format(username)) + return message - def __cmdGetPony(self, room_name): - max = 160 - message = "http://ponyfac.es/{}/full.jpg".format(random.randint(1, max)) - self.postMessage(room_name, message) - - def __cmdRPS(self, room_name, username, message): - - if 'help' in message: - self.postMessage(room_name, 'http://a.tgcdn.net/images/products/additional/large/db2e_lizard_spock.jpg') - return False - - options = ['Rock', 'Paper', 'Scissors', 'Lizard', 'Spock'] - response = '{0} - {1}'.format(self.__mentionUser(username), random.choice(options)) - self.postMessage(room_name, response) - - def __cmdAsk(self, room_name, username): - options = ['yes', 'no', 'no way!', 'yep!'] - response = '{0}, {1}'.format(self.__mentionUser(username), random.choice(options)) - self.postMessage(room_name, response) - - def __cmdArnold(self, room_name, message): - self.__doQuoteAtUser(room_name, ARNOLD_PHRASES, message) - - def __cmdLego(self, room_name, message): - self.__doQuoteAtUser(room_name, LEGO_QUOTES, message) - - def __cmdXKCD(self, room_name): - max_value = 1335 - value = random.randint(1, max_value) - - r = requests.get('http://xkcd.com/{}/info.0.json'.format(value)) - - if r.status_code == 200: - response = r.json() - self.postMessage(room_name, response['img']) - self.postMessage(room_name, response['alt']) - - else: - self.postMessage(room_name, "Can't connect to xkcd API =(") - - def __cmdRandomGIF(self, room_name, message): - search_values = message.split('/gif', 1) - tags = '' - if len(search_values) == 2: - tags = '+'.join(search_values[1].split()) - r = requests.get('http://api.giphy.com/v1/gifs/random?api_key=dc6zaTOxFJmzC&tag=' + tags) - - if r.status_code == 200: - response = r.json() - self.postMessage(room_name, response['data']['image_url']) - - else: - self.postMessage(room_name, "Can't connect to giphy API =(") - - def __cmdRoll(self, room_name, username): - response = '{0} rolled {1}'.format(self.__mentionUser(username), random.randint(0, 100)) - self.postMessage(room_name, response) - - def __cmdGetLimit(self, room_name): + def __cmd_get_limit(self): message = '{0}/{1} calls remaining, update in {2} seconds'.format( self.hipster.limits['remaining'], self.hipster.limits['limit'], self.hipster.limits['reset'] - time() ) - self.postMessage(room_name, message) + + return message # Public # ================================================================== - def joinRooms(self, rooms): + def join_rooms(self, rooms): self.rooms = rooms.split(',') for room in self.rooms: - self.joinRoom(room) + self.join_room(room) - def joinRoom(self, room_name): + def join_room(self, room_name): if room_name in self.available_rooms: self.joined_rooms.update({room_name: self.available_rooms[room_name]}) - def postMessage(self, room_name, message): + def post_message(self, room_name, message): self.hipster.message_room(self.joined_rooms[room_name], self.name, message) - def start(self): - last_dates = self.__getLatestDates() + def execute_action(self, action, room_name, message_object): + args = {} + fields = set(inspect.getargspec(action).args) + + if 'room' in fields: args.update({'room': room_name}) + if 'author_id' in fields: args.update({'author_id': message_object['from']['user_id']}) + if 'author' in fields: args.update({'author': message_object['from']['name']}) + if 'message' in fields: args.update({'message': message_object['message']}) + if 'random_user' in fields: args.update({'random_user': self.__get_random_user(room_name)}) + if 'mentioned_user' in fields: + args.update({'mentioned_user': self.__get_mentioned_user(room_name, message_object['message'])}) + message = action(**args) + + self.post_message(room_name, message) + + def start(self): + last_dates = self.__get_latest_dates() while True: print('.') for room_name in self.joined_rooms: try: - messages = self.__getMessages(room_name, last_dates[room_name]) + messages = self.__get_messages(room_name, last_dates[room_name]) if messages: - last_dates[room_name] = self.__getLatestDate(messages) + last_dates[room_name] = self.__get_latest_date(messages) for message in messages: - if message['from']['name'] != self.name: - for action_name in self.actions: - fields = set(inspect.getargspec(self.actions[action_name]['action'])[0]) - args = {'room_name': room_name} - - if 'user_id' in fields: - args.update({'user_id': message['from']['user_id']}) - - if 'username' in fields: - args.update({'username': message['from']['name']}) - - if 'message' in fields: - args.update({'message': message['message']}) + if message['from']['name'] == self.name: continue - if action_name in message['message']: - self.actions[action_name]['action'](**args) + for action_name in self.actions: + if action_name in message['message']: + print("Executing action: " + action_name + " in room '" + room_name + "'") + self.execute_action(self.actions[action_name]['action'], room_name, message) except Exception, e: print(str(e)) diff --git a/src/config.py b/src/config.py index 9af541a..0ce2732 100644 --- a/src/config.py +++ b/src/config.py @@ -9,7 +9,6 @@ class Settings(): rules = { 'general': ['api_token', 'rooms', 'bot_name'], - 'aws': ['access_key', 'secret_key'], 'geckoboard': ['api', 'widget'] } diff --git a/src/ec2_helper.py b/src/ec2_helper.py deleted file mode 100644 index e0041c3..0000000 --- a/src/ec2_helper.py +++ /dev/null @@ -1,65 +0,0 @@ -# -*- coding: UTF-8 -*- - -import re -import boto.ec2 -from aws_basic import AWSBasic - -class EC2Helper(AWSBasic): - - instances = [] - launch_groups = {} - - RE_STAGING = re.compile('(stage|staging)', re.IGNORECASE) - - def connect(self, access_key, secret_key, region = False): - """ - Create connection to region - If region didn't specified - connect to default region - - :type region: str - :param region: Region to connect - """ - if not region: - region = self.REGION - - self.conn = boto.ec2.connect_to_region(region, aws_access_key_id=access_key, aws_secret_access_key=secret_key) - - if not self.conn: - print 'Invalid region name' - return False - - def __getInstances(self): - """ - Get instances list from AWS, cache it - with custom format + create dicts for fast search - """ - reservations = self.conn.get_all_instances() - - our_instances = [] - - for reservation in reservations: - for instance in reservation.instances: - our_instances.append(instance) - - self.instances = our_instances - - def getInstanceStatuses(self): - - self.__getInstances() - - instances = [] - - for instance in self.instances: - - if 'Name' in instance.tags: - - instance_name = instance.tags['Name'] - - if re.search(self.RE_STAGING, instance_name): - instances.append({ - 'name': instance_name, - 'state': instance.state, - 'state_code': instance.state_code - }) - - return instances diff --git a/src/plugin_loader.py b/src/plugin_loader.py new file mode 100644 index 0000000..b5dd285 --- /dev/null +++ b/src/plugin_loader.py @@ -0,0 +1,71 @@ +import os +import sys +import inspect + +class PluginLoader(): + + __plugins = [] + __plugin_names = {} + + conf = None + app_dir = os.path.abspath(os.path.split(inspect.getfile(inspect.currentframe()))[0]) + '/../' + + def __init__(self, conf, plugins_dir): + self.conf = conf + folder = self.app_dir + plugins_dir + + sys.path.insert(0, folder) + + for filename in os.listdir(folder): + full_path = folder + '/' + filename + + if os.path.isdir(full_path) or filename[-3:] == '.py': + self.load_plugin(filename) + + def __check_conflicts(self, command): + for plugin in self.__plugins: + if command == plugin.command: + raise Exception("Command name '" + command + "' conflicts with plugin: " + self.__plugin_names[plugin.command]) + + def get_plugins(self): + return self.__plugins + + def load_plugin(self, filename): + """ + Load plugin class, check if it has all the required properties + and initialise it + """ + + try: + + if filename[-2:] == 'py': + filename = filename[:-3:] + + __import__(filename) + + for name, class_name in inspect.getmembers(sys.modules[filename]): + if inspect.isclass(class_name): + args = {} + + if hasattr(class_name, '__init__') and 'conf' in inspect.getargspec(class_name.__init__).args: + args.update({'conf': self.conf}) + + plugin = class_name(**args) + if not hasattr(plugin, 'help') and not hasattr(plugin, 'command'): + raise Exception("Plugin class must have 'help' and 'command' attributes") + + self.__check_conflicts(plugin.command) + + self.__plugins.append(plugin) + self.__plugin_names.update({plugin.command: filename}) + + print("Plugin '" + filename + "' (/" + plugin.command + ") was loaded...") + + return True + + raise Exception("No class found in " + filename) + + except Exception, e: + print("Can't load plugin '" + filename + "': " + str(e)) + +