diff --git a/README.md b/README.md index ad376e3..544b6a5 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ see requirements.txt - pyyaml - sleekxmpp - dnspython (optional, for dns plugin) + - slackclient (optional, for slack plugin) installation ---- diff --git a/creep.py b/creep.py index d3e7d25..e4568e0 100644 --- a/creep.py +++ b/creep.py @@ -1,6 +1,7 @@ import sleekxmpp import logging import inspect +from slack import Slack from plugins import Plugin from threading import Timer @@ -15,6 +16,8 @@ class Creep(): def __init__(self, config): logging.basicConfig(level=logging.INFO) + + self.slack = Slack(self, config) if "slack" in config.keys() else None self.config = config self.muted_rooms = set() self.xmpp = sleekxmpp.ClientXMPP( @@ -31,7 +34,7 @@ def __init__(self, config): else: self.xmpp.connect() logging.info("Connected") - + self.xmpp.process() self.xmpp.add_event_handler("session_start", self.handle_connected) @@ -41,7 +44,10 @@ def __init__(self, config): self.plugins = [] if 'plugins' in config: self._load_plugins(config['plugins'], config) - + + if self.slack: + self.slack.start() + def handle_connected(self, flap): self.xmpp.send_presence() logging.info("Started processing") @@ -51,7 +57,10 @@ def handle_connected(self, flap): wait=True) logging.info("Connected to chat room '%s'" % room) - def handle_message(self, message): + def handle_message(self, message, sender=None, user=None): + if isinstance(sender, Slack): + return self.__handle_message(message, user) + body = message['body'] if not self.from_us(message): if not message.get_mucroom(): @@ -62,7 +71,7 @@ def handle_message(self, message): message.reply(reply).send() logging.debug('Handled request "%s"' % body) - + def mute(self, room, timeout=10): def unmute_room(): self.unmute(room) @@ -77,6 +86,14 @@ def unmute(self, room): mbody="I'm back baby!", mtype='groupchat') + def send_slack_message(self, message): + if self.slack: + self.slack.send_message(message) + + def delete_slack_message(self, quote_id): + if self.slack: + self.slack.delete_message(quote_id) + def __handle_message(self, body, origin): command = body.split(' ')[0] if ' ' in body else body params = body[body.find(" ")+1:] if ' ' in body else None @@ -102,11 +119,12 @@ def from_us(self, message): return False def shutdown(self): + self.slack.shutdown() for plugin in self.plugins: plugin.shutdown() self.xmpp.disconnect(wait=True) - + def _load_plugins(self, names, config): for name in names: try: diff --git a/plugins/quotes.py b/plugins/quotes.py index d21ebb8..9256567 100644 --- a/plugins/quotes.py +++ b/plugins/quotes.py @@ -2,7 +2,6 @@ import sqlite3 from threading import Lock - class Quotes(Plugin): provides = ['aq', 'iq', 'q', 'sq', 'lq', 'dq'] @@ -10,6 +9,7 @@ class Quotes(Plugin): def __init__(self, creep, config=None): self.__initialize_db() self.lock = Lock() + self.creep = creep if 'admins' in config: self.admins = config['admins'] else: @@ -17,6 +17,7 @@ def __init__(self, creep, config=None): def aq(self, message=None, origin=None): '''Add a quote. For example: "aq this is my quote"''' + quote_id=None with self.lock: cursor = self.db.cursor() query = 'insert into quotes (content) values (?)' @@ -24,8 +25,14 @@ def aq(self, message=None, origin=None): quote_id = result.lastrowid cursor.close() self.db.commit() - - return 'inserted quote \'%s\'' % quote_id + + + if self.creep: + quote = str("%d - %s" % (quote_id, self.iq(quote_id))) + if not quote.startswith('invalid quote_id:'): + self.creep.send_slack_message(quote) + + return 'inserted quote \'%s\'' % quote_id def iq(self, message=None, origin=None): '''Query for a quote. For example: "iq 123"''' @@ -115,6 +122,9 @@ def dq(self, message=None, origin=None): cursor.execute(query, [quote_id]) cursor.close() self.db.commit() + + if self.creep: + self.creep.delete_slack_message(quote_id) return "'%s' deleted" % quote_id except ValueError: diff --git a/requirements.txt b/requirements.txt index 039b5f3..93bd17d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ PyYAML dnspython sleekxmpp +slackclient \ No newline at end of file diff --git a/slack.py b/slack.py new file mode 100644 index 0000000..c7662b4 --- /dev/null +++ b/slack.py @@ -0,0 +1,145 @@ +import time +import json +import logging +import errno +from ssl import SSLError +from slackclient import SlackClient +from threading import Thread +from random import random + +class Slack(): + + def __init__(self, creep, config, loglevel=logging.INFO): + logging.basicConfig(level=loglevel) + self.creep = creep + self.token = config["slack"]["token"] if 'slack' in config.keys() and 'token' in config["slack"].keys() else None + self.client = SlackClient(self.token) + self.channel = config["slack"]["channel"] if 'slack' in config.keys() and 'channel' in config["slack"].keys() else None + self.channel_id = None + self.user_id = None + self.connected = False + self.connect(config) + + def shutdown(self, shutup=False): + logging.debug("Going back to sleep, ZzzzZzzzz...") + self.keep_running = False + if not shutup: + self.send_message("Catch you on the flip side!") + + def start(self): + self.keep_running = True + self.thread = Thread(target=self._run) + self.thread.start() + logging.debug("Started slack service integration") + + def _run(self): + while self.keep_running and self.connected: + message = self.client.rtm_read() + if message: + self.read_message(message) + + def _set_channel(self, config): + if self.connected and 'slack' in config.keys() and 'channel' in config["slack"].keys(): + channel_info = json.loads(self.client.api_call("channels.list")) + channel = filter(lambda c: self.channel and c["name"] == self.channel, channel_info["channels"]) + if channel: + self.channel_id = channel[0]["id"] + logging.debug("Channel id set to '%s'" % self.user_id) + return True + else: + logging.exception("Channel '" + config["slack"]["channel"] + "' does not exists yet or you are not a member!") + else: + logging.info("Slack client not connected.") + return False + + def _set_user_id(self): + if self.connected: + user = json.loads(self.client.api_call("auth.test", token=self.token)) + if user: + self.user_id = user["user_id"] + logging.debug("User id set to '%s'" % self.user_id) + return True + else: + logging.exception("Unable to locate user.") + else: + logging.info("Slack client not connected.") + return False + + def connect(self, config): + logging.info("Slack client joining channel '%s'" % str(self.channel)) + if self.client.rtm_connect(): + logging.info("Slack client connected") + self.connected = True + if self._set_channel(config) and self._set_user_id() and self.channel_id: + self.client.rtm_send_message(self.channel_id, lines[int(random()*len(lines))]) + return True + else: + self.connected = False + logging.exception(("Connection Failed, invalid token: %s" % str(self.token))) + return False + + def send_message(self, message, channel=None): + if not self.channel_id: + return None + + if not channel: + channel = self.channel_id + + sc = SlackClient(self.token) + if sc.rtm_connect(): + sc.rtm_send_message(channel, message) + logging.debug("%s| %s" % (channel, message)) + + def _get_user_by_id(self, user_id=None): + result = json.loads(self.client.api_call("users.list", token=self.token)) + if 'members' in result.keys() and result["members"]: + user = filter(lambda u: 'id' in u.keys() and u["id"] == user_id, result["members"]) + if user and user[0] and 'profile' in user[0].keys() and 'email' in user[0]["profile"].keys(): + return user[0]["profile"]["email"] + return None + + def read_message(self, message): + m = message[0] + if self.user_id and set(["type", "text", "user"])<=set(m.keys()) and m["type"]=="message" and m["user"] != self.user_id: + if m["text"].startswith("<@%s>" % str(self.user_id)): + self.send_message(self._highlight()) + elif m["channel"][0]=="D": # direct message + channel = m["channel"] + response = self.creep.handle_message(m["text"], self, self._get_user_by_id(m["user"])) + self.send_message(response, channel) + else: + logging.debug("message ignored: %s" % message) + + def delete_message(self, quote_id): + message = self._search_message(quote_id) + if not message: + logging.info("Could not find message %d" % quote_id) + return False + + if not 'ts' in message.keys(): + logging.info("invalid message format: %s" % str(message)) + return False + + result = json.loads(self.client.api_call("chat.delete", token=self.token, channel=self.channel_id, ts=message["ts"])) + result_status = result and 'ok' in result.keys() and result["ok"] + logging.info("Message %d delete status: %s" % (quote_id, result_status)) + + def _search_message(self, quote_id=None): + response = json.loads(self.client.api_call("channels.history", token=self.token, channel=self.channel_id)) + if 'messages' in response.keys(): + message = filter(lambda m: 'type' in m.keys() and m["type"] == "message" and 'text' in m.keys() and m["text"].split(" ", 1)[0]==str("%d" % quote_id), response["messages"]) + if message: + return message[0] + return None + + def _highlight(self): + return "Want to use my awesome quoting functionality? DM me!" + + def __str__(self): + return 'slack' + +lines = [ + "Back in the house!", + "Respect for the man with the ice cream van!", + "It's nice to be important, but it's more important to be nice!" +] \ No newline at end of file