diff options
author | Ryan Huber <rhuber@gmail.com> | 2017-01-31 17:12:43 -0800 |
---|---|---|
committer | Ryan Huber <rhuber@gmail.com> | 2017-01-31 17:12:43 -0800 |
commit | 47607a88c578f804d76c25dd2d68386556255b21 (patch) | |
tree | f84cc2063f7c6cae3c5e9e49defee01f709118a6 | |
download | wee-slack-47607a88c578f804d76c25dd2d68386556255b21.tar.gz |
first run - lots doesn't work yet
-rw-r--r-- | wee_slack.py | 1220 |
1 files changed, 1220 insertions, 0 deletions
diff --git a/wee_slack.py b/wee_slack.py new file mode 100644 index 0000000..aed88e9 --- /dev/null +++ b/wee_slack.py @@ -0,0 +1,1220 @@ +# -*- coding: utf-8 -*- +# + +import time +import json +import pickle +import sha +import re +import urllib +import sys +import traceback +import collections +import ssl +#import random + +from websocket import create_connection, WebSocketConnectionClosedException + +# hack to make tests possible.. better way? +try: + import weechat as w +except: + pass + +SCRIPT_NAME = "slack_extension" +SCRIPT_AUTHOR = "Ryan Huber <rhuber@gmail.com>" +SCRIPT_VERSION = "1.99" +SCRIPT_LICENSE = "MIT" +SCRIPT_DESC = "Extends weechat for typing notification/search/etc on slack.com" + +BACKLOG_SIZE = 200 +SCROLLBACK_SIZE = 500 + +CACHE_VERSION = "4" + +SLACK_API_TRANSLATOR = { + "channel": { + "history": "channels.history", + "join": "channels.join", + "leave": "channels.leave", + "mark": "channels.mark", + "info": "channels.info", + }, + "im": { + "history": "im.history", + "join": "im.open", + "leave": "im.close", + "mark": "im.mark", + }, + "group": { + "history": "groups.history", + "join": "channels.join", + "leave": "groups.leave", + "mark": "groups.mark", + }, + "thread": { + "history": None, + "join": None, + "leave": None, + "mark": None, + } + + +} + +NICK_GROUP_HERE = "0|Here" +NICK_GROUP_AWAY = "1|Away" + +sslopt_ca_certs = {} +if hasattr(ssl, "get_default_verify_paths") and callable(ssl.get_default_verify_paths): + ssl_defaults = ssl.get_default_verify_paths() + if ssl_defaults.cafile is not None: + sslopt_ca_certs = {'ca_certs': ssl_defaults.cafile} + +##### BEGIN NEW + +IGNORED_EVENTS = [ + "reconnect_url", + "hello", +] + +###### New central Event router + +class EventRouter(object): + + def __init__(self): + self.queue = [] + self.teams = {} + self.weechat_buffers = {} + self.previous_buffer = "" + self.reply_buffer = {} + self.cmds = {k[8:]: v for k, v in globals().items() if k.startswith("command_")} + self.proc = {k[8:]: v for k, v in globals().items() if k.startswith("process_")} + self.handlers = {k[7:]: v for k, v in globals().items() if k.startswith("handle_")} + self.local_proc = {k[14:]: v for k, v in globals().items() if k.startswith("local_process_")} + + def register_team(self, team): + """ + Adds a team to the list of known teams for this EventRouter + """ + if isinstance(team, SlackTeam): + self.teams[team.get_team_hash()] = team + else: + raise InvalidType(type(team)) + + def register_weechat_buffer(self, buffer_ptr, channel): + """ + Adds a weechat buffer to the list of handled buffers for this EventRouter + """ + if isinstance(buffer_ptr, str): + self.weechat_buffers[buffer_ptr] = channel + else: + raise InvalidType(type(buffer_ptr)) + + def unregister_weechat_buffer(self, buffer_ptr): + """ + Adds a weechat buffer to the list of handled buffers for this EventRouter + """ + if isinstance(buffer_ptr, str): + try: + self.weechat_buffers[buffer_ptr].destroy_buffer() + del self.weechat_buffers[buffer_ptr] + except: + dbg("Tried to close unknown buffer") + else: + raise InvalidType(type(buffer_ptr)) + + def receive_ws_callback(self, team_hash): + """ + This is called by the global method of the same name. + It is triggered when we have incoming data on a websocket, + which needs to be read. Once it is read, we will ensure + the data is valid JSON, add metadata, and place it back + on the queue for processing as JSON. + """ + try: + # Read the data from the websocket associated with this team. + data = self.teams[team_hash].ws.recv() + message_json = json.loads(data) + metadata = WeeSlackMetadata({ + "team": team_hash, + #"channels": self.teams[team_hash].channels, + #"users": self.teams[team_hash].users, + }).jsonify() + #print self.teams[team_hash].domain + message_json["wee_slack_metadata"] = metadata + #print message_json + self.receive_json(json.dumps(message_json)) + except WebSocketConnectionClosedException: + #TODO: handle reconnect here + self.teams[team_hash].set_disconnected() + return w.WEECHAT_RC_OK + except Exception: + dbg("socket issue: {}\n".format(traceback.format_exc())) + return w.WEECHAT_RC_OK + + def receive_httprequest_callback(self, data, command, return_code, out, err): + #def url_processor_cb(data, command, return_code, out, err): + request_metadata = pickle.loads(data) + dbg("RECEIVED CALLBACK with request of {} id of {} and code {} of length {}".format(request_metadata.request, request_metadata.response_id, return_code, len(out)), main_buffer=True) + if return_code == 0: + if request_metadata.response_id in self.reply_buffer: + self.reply_buffer[request_metadata.response_id] += out + #print self.reply_buffer[request_metadata.response_id] + else: + self.reply_buffer[request_metadata.response_id] = "" + self.reply_buffer[request_metadata.response_id] += out + try: + j = json.loads(self.reply_buffer[request_metadata.response_id]) + j["wee_slack_process_method"] = request_metadata.request_normalized + j["wee_slack_request_metadata"] = pickle.dumps(request_metadata) + #print self.reply_buffer[request_metadata.response_id] + self.reply_buffer.pop(request_metadata.response_id) + self.receive_json(json.dumps(j)) + except: + dbg("FAILED") + pass + elif return_code != -1: + self.reply_buffer.pop(request_metadata.response_id, None) + else: + if request_metadata.response_id not in self.reply_buffer: + self.reply_buffer[request_metadata.response_id] = "" + self.reply_buffer[request_metadata.response_id] += out + + def receive_json(self, data): + dbg("RECEIVED JSON") + dbg(str(data)) + message_json = json.loads(data) + dbg(message_json) + #print message_json.keys() + self.queue.append(message_json) + def receive(self, dataobj): + dbg("RECEIVED QUEUE", main_buffer=True) + #dbg(str(len(dataobj)), main_buffer=True) + self.queue.append(dataobj) + def handle_next(self): + if len(self.queue) > 0: + j = self.queue.pop(0) + # Reply is a special case of a json reply from websocket. + kwargs = {} + if isinstance(j, SlackRequest): + if j.should_try(): + local_process_async_slack_api_request(j, self) + return + + if "reply_to" in j: + dbg("SET FROM REPLY") + function_name = "reply" + elif "type" in j: + dbg("SET FROM type") + function_name = j["type"] + elif "wee_slack_process_method" in j: + dbg("SET FROM META") + function_name = j["wee_slack_process_method"] + else: + dbg("SET FROM NADA") + function_name = "unknown" + + # Here we are passing the actual objects. No more lookups. + if "wee_slack_metadata" in j: + + if isinstance(j["wee_slack_metadata"], str): + dbg("string of metadata") + if "team" in j["wee_slack_metadata"]: + kwargs["team"] = self.teams[j["wee_slack_metadata"]["team"]] + if "user" in j: + kwargs["user"] = self.teams[j["wee_slack_metadata"]["team"]].users[j["user"]] + if "channel" in j: + kwargs["channel"] = self.teams[j["wee_slack_metadata"]["team"]].channels[j["channel"]] + + if function_name not in IGNORED_EVENTS: + dbg("running {}".format(function_name)) + if function_name.startswith("local_") and function_name in self.local_proc: + self.local_proc[function_name](j, self, **kwargs) + elif function_name in self.proc: + self.proc[function_name](j, self, **kwargs) + elif function_name in self.handlers: + self.handlers[function_name](j, self, **kwargs) + else: + raise ProcessNotImplemented(function_name) + +def handle_next(*args): + EVENTROUTER.handle_next() + return w.WEECHAT_RC_OK + +###### New Local Processors + +def local_process_async_slack_api_request(request, event_router): + """ + Sends an API request to Slack. You'll need to give this a well formed SlackRequest object. + """ + weechat_request = 'url:{}'.format(request.request_string()) + params = {'useragent': 'wee_slack {}'.format(SCRIPT_VERSION)} + request.tried() + context = pickle.dumps(request) + w.hook_process_hashtable(weechat_request, params, config.slack_timeout, "receive_httprequest_callback", context) + +###### New Callbacks + +def receive_httprequest_callback(data, command, return_code, out, err): + """ + This is a dirty hack. There must be a better way. + """ + #def url_processor_cb(data, command, return_code, out, err): + EVENTROUTER.receive_httprequest_callback(data, command, return_code, out, err) + return w.WEECHAT_RC_OK + +def receive_ws_callback(*args): + """ + The first arg is all we want here. It contains the team + hash which is set when we _hook the descriptor. + This is a dirty hack. There must be a better way. + """ + EVENTROUTER.receive_ws_callback(args[0]) + return w.WEECHAT_RC_OK + +def buffer_closing_callback(signal, sig_type, data): + eval(signal).unregister_weechat_buffer(data) + return w.WEECHAT_RC_OK + +def buffer_input_callback(signal, buffer_ptr, data): + eventrouter = eval(signal) + + dbg(signal, True) + dbg(data, True) + dbg(buffer_ptr, True) + channel = eventrouter.weechat_buffers[buffer_ptr] + print channel + if not channel: + return w.WEECHAT_RC_OK_EAT + +# reaction = re.match("^\s*(\d*)(\+|-):(.*):\s*$", data) +# if reaction: +# if reaction.group(2) == "+": +# channel.send_add_reaction(int(reaction.group(1) or 1), reaction.group(3)) +# elif reaction.group(2) == "-": +# channel.send_remove_reaction(int(reaction.group(1) or 1), reaction.group(3)) +# elif data.startswith('s/'): +# try: +# old, new, flags = re.split(r'(?<!\\)/', data)[1:] +# except ValueError: +# pass +# else: +# # Replacement string in re.sub() is a string, not a regex, so get +# # rid of escapes. +# new = new.replace(r'\/', '/') +# old = old.replace(r'\/', '/') +# channel.change_previous_message(old.decode("utf-8"), new.decode("utf-8"), flags) + else: + channel.send_message(data) + # channel.buffer_prnt(channel.server.nick, data) +# channel.mark_read(True) + return w.WEECHAT_RC_ERROR + +def buffer_switch_callback(signal, sig_type, data): + eventrouter = eval(signal) + + # this is to see if we need to gray out things in the buffer list + if eventrouter.previous_buffer in eventrouter.weechat_buffers: + pass + #channels.find(previous_buffer).mark_read() + + if data in eventrouter.weechat_buffers: + new_channel = eventrouter.weechat_buffers[data] + #if new_channel: + if not new_channel.got_history: + new_channel.get_history() + + eventrouter.previous_buffer = data + return w.WEECHAT_RC_OK + +##### New Classes + +class SlackRequest(object): + """ + Encapsulates a Slack api request. Valuable as an object that we can add to the queue and/or retry. + """ + def __init__(self, token, request, post_data={}, **kwargs): + print '=================' + for key, value in kwargs.items(): + setattr(self, key, value) + print "{} {}".format(key, value) + print '=================' + self.tries = 0 + self.domain = 'api.slack.com' + self.request = request + self.request_normalized = re.sub(r'\W+', '', request) + self.token = token + post_data["token"] = token + self.post_data = post_data + self.params = {'useragent': 'wee_slack {}'.format(SCRIPT_VERSION)} + self.url = 'https://{}/api/{}?{}'.format(self.domain, request, urllib.urlencode(post_data)) + self.response_id = sha.sha("{}{}".format(self.url, time.time())).hexdigest() +# def __repr__(self): +# return "URL: {} Tries: {} ID: {}".format(self.url, self.tries, self.response_id) + def request_string(self): + return "{}".format(self.url) + def tried(self): + self.tries += 1 + def should_try(self): + return self.tries < 3 + +class SlackTeam(object): + def __init__(self, token, team, nick, myidentifier, users, channels): + self.connected = False + self.ws = None + self.ws_counter = 0 + self.ws_replies = {} + self.token = token + self.team = team + self.domain = team + ".slack.com" + self.nick = nick + self.myidentifier = myidentifier + self.channels = channels + self.users = users + self.team_hash = str(sha.sha("{}{}".format(self.nick, self.team)).hexdigest()) + for c in self.channels.keys(): + channels[c].set_related_server(self) + channels[c].open_if_we_should() + # self.channel_set_related_server(c) + def __eq__(self, compare_str): + if compare_str == self.token: + return True + else: + return False + def add_channel(self, channel): + self.channels[channel["id"]] = channel + channel.set_related_server(self) +# def connect_request_generate(self): +# return SlackRequest(self.token, 'rtm.start', {}) + def get_team_hash(self): + return self.team_hash + def attach_websocket(self, ws): + self.ws = ws + def set_connected(self): + self.connected = True + def set_disconnected(self): + self.connected = False + def next_ws_transaction_id(self): + if self.ws_counter > 999: + self.ws_counter = 0 + self.ws_counter += 1 + return self.ws_counter + def send_to_websocket(self, data, expect_reply=True): + data["id"] = self.next_ws_transaction_id() + message = json.dumps(data) + try: + if expect_reply: + self.ws_replies[data["id"]] = data + self.ws.send(message) + dbg("Sent {}...".format(message[:100])) + except: + dbg("Unexpected error: {}\nSent: {}".format(sys.exc_info()[0], data)) + self.connected = False + + +class SlackChannel(object): + """ + Represents an individual slack channel. + """ + def __init__(self, eventrouter, **kwargs): + # We require these two things for a vaid object, + # the rest we can just learn from slack + self.eventrouter = eventrouter + self.identifier = kwargs["id"] + self.name = kwargs["name"] + self.channel_buffer = None + self.team = None + self.got_history = False + self.messages = {} + self.type = 'channel' + for key, value in kwargs.items(): + setattr(self, key, value) + def __repr__(self): + return "Name:{} Identifier:{}".format(self.name, self.identifier) + def open_if_we_should(self): + for reason in ["is_member", "is_open", "unread_count"]: + try: + if eval("self." + reason): + self.create_buffer() + except: + pass + pass + def set_related_server(self, team): + self.team = team + def create_buffer(self): + if not self.channel_buffer: + print self.team + self.channel_buffer = w.buffer_new("{}.{}".format(self.team.domain, self.name), "buffer_input_callback", "EVENTROUTER", "", "") + self.eventrouter.register_weechat_buffer(self.channel_buffer, self) + + #dbg(self.channel_buffer, True) + w.buffer_set(self.channel_buffer, "localvar_set_type", 'channel') + w.buffer_set(self.channel_buffer, "localvar_set_channel", self.name) + w.buffer_set(self.channel_buffer, "short_name", self.name) +# if self.server.alias: +# w.buffer_set(self.channel_buffer, "localvar_set_server", self.server.alias) +# else: +# w.buffer_set(self.channel_buffer, "localvar_set_server", self.server.team) +# buffer_list_update_next() +# if self.unread_count != 0 and not self.muted: +# w.buffer_set(self.channel_buffer, "hotlist", "1") + def destroy_buffer(self): + if self.channel_buffer: + self.channel_buffer = None + def buffer_prnt(self, nick, text, timestamp, *args): + if self.channel_buffer: + w.prnt(self.channel_buffer, "{}\t{}".format(nick, text)) + #dbg("should buffer print {} {}".format(nick, text), True) + def send_message(self, message): + #team = self.eventrouter.teams[self.team] + #message = self.linkify_text(message) + dbg(message) + print self.team + request = {"type": "message", "channel": self.identifier, "text": message, "_team": self.team.team_hash, "user": self.team.myidentifier} + dbg(request, True) + self.team.send_to_websocket(request) + def store_message(self, message, team, from_me=False): + if from_me: + message.message_json["user"] = team.myidentifier + self.messages[message.ts] = message + if len(self.messages.keys()) > SCROLLBACK_SIZE: + mk = self.messages.keys() + mk.sort() + for k in mk[:SCROLLBACK_SIZE]: + del self.messages[k] + def change_message(self, ts, text=None, suffix=''): + #print "should have changed" + if ts in self.messages: + m = self.messages[ts] + m.change_text(text) + #m = self.get_message(ts) + #text = m[2].render(force=True) + #timestamp, time_id = ts.split(".", 2) + #timestamp = int(timestamp) + #modify_buffer_line(self.channel_buffer, text + suffix, timestamp, time_id) + return True + def get_history(self): + #if config.cache_messages: + # for message in message_cache[self.identifier]: + # process_message(json.loads(message), True) + s = SlackRequest(self.team.token, SLACK_API_TRANSLATOR[self.type]["history"], {"channel": self.identifier, "count": BACKLOG_SIZE}, team_hash=self.team.team_hash, channel_identifier=self.identifier) + EVENTROUTER.receive(s) + #async_slack_api_request(self.server.domain, self.server.token, SLACK_API_TRANSLATOR[self.type]["history"], {"channel": self.identifier, "count": BACKLOG_SIZE}) + self.got_history = True + def sorted_message_keys(self): + return sorted(self.messages) + + +class SlackDMChannel(SlackChannel): + def __init__(self, eventrouter, users, **kwargs): + dmuser = kwargs["user"] + kwargs["name"] = users[dmuser].name + super(SlackDMChannel, self).__init__(eventrouter, **kwargs) + self.type = 'im' + def create_buffer(self): + if not self.channel_buffer: + super(SlackDMChannel, self).create_buffer() + w.buffer_set(self.channel_buffer, "localvar_set_type", 'private') + +class SlackGroupChannel(SlackChannel): + def __init__(self, eventrouter, **kwargs): + super(SlackGroupChannel, self).__init__(eventrouter, **kwargs) + self.type = "group" + +class SlackMPDMChannel(SlackChannel): + """ + An MPDM channel is a special instance of a 'group' channel. + We change the name to look less terrible in weechat. + """ + def __init__(self, eventrouter, **kwargs): + n = kwargs.get('name') + name = "|".join("-".join(n.split("-")[1:-1]).split("--")) + kwargs["name"] = name + super(SlackMPDMChannel, self).__init__(eventrouter, **kwargs) + self.type = "group" + +class SlackUser(object): + """ + Represends an individual slack user. + """ + def __init__(self, **kwargs): + # We require these two things for a vaid object, + # the rest we can just learn from slack + self.identifier = kwargs["id"] + self.name = kwargs["name"] + for key, value in kwargs.items(): + setattr(self, key, value) + def __repr__(self): + return "Name:{} Identifier:{}".format(self.name, self.identifier) + def formatted_name(self, prepend="", enable_color=True): + if config.colorize_nicks and enable_color: + print_color = self.color + else: + print_color = "" + return print_color + prepend + self.name + +class SlackMessage(object): + def __init__(self, message_json, team, channel): + self.team = team + self.channel = channel + self.message_json = message_json + self.sender = self.get_sender() + self.ts = message_json['ts'] + def render(self): + return render(self.message_json, self.team, self.channel) + def change_text(self, new_text): + dbg("should change text to {}".format(new_text), True) + return "meh" + def get_sender(self, utf8=True): + if 'bot_id' in self.message_json and self.message_json['bot_id'] is not None: + name = u"{} :]".format(self.server.bots.find(self.message_json["bot_id"]).formatted_name()) + elif 'user' in self.message_json: + if self.message_json['user'] in self.team.users: + u = self.team.users[self.message_json['user']] + if u.is_bot: + name = u"{} :]".format(u.formatted_name()) + else: + name = u.name + elif 'username' in self.message_json: + name = u"-{}-".format(self.message_json["username"]) + elif 'service_name' in self.message_json: + name = u"-{}-".format(self.message_json["service_name"]) + else: + name = u"" + if utf8: + return name.encode('utf-8') + else: + return name + +class WeeSlackMetadata(object): + def __init__(self, meta): + self.meta = meta + def jsonify(self): + return self.meta + +###### New handlers + +def handle_rtmstart(login_data, eventrouter): + """ + This handles the main entry call to slack, rtm.start + """ + if login_data["ok"]: + metadata = pickle.loads(login_data["wee_slack_request_metadata"]) + + users = {} + for item in login_data["users"]: + users[item["id"]] = SlackUser(**item) + #users.append(SlackUser(**item)) + + channels = {} + for item in login_data["channels"]: + channels[item["id"]] = SlackChannel(eventrouter, **item) + + for item in login_data["ims"]: + channels[item["id"]] = SlackDMChannel(eventrouter, users, **item) + + for item in login_data["groups"]: + if item["name"].startswith('mpdm-'): + channels[item["id"]] = SlackMPDMChannel(eventrouter, **item) + else: + channels[item["id"]] = SlackGroupChannel(eventrouter, **item) + + t = SlackTeam( + metadata.token, + login_data["team"]["domain"], + login_data["self"]["name"], + login_data["self"]["id"], + users, + channels, + ) + eventrouter.register_team(t) + + web_socket_url = login_data['url'] + try: + ws = create_connection(web_socket_url, sslopt=sslopt_ca_certs) + w.hook_fd(ws.sock._sock.fileno(), 1, 0, 0, "receive_ws_callback", t.get_team_hash()) + #ws_hook = w.hook_fd(ws.sock._sock.fileno(), 1, 0, 0, "receive_ws_callback", pickle.dumps(t)) + ws.sock.setblocking(0) + t.attach_websocket(ws) + t.set_connected() + except Exception as e: + dbg("websocket connection error: {}".format(e)) + return False + + dbg("connected to {}".format(t.domain)) + + #self.identifier = self.domain + +def handle_groupshistory(message_json, eventrouter, **kwargs): + handle_history(message_json, eventrouter, **kwargs) + +def handle_channelshistory(message_json, eventrouter, **kwargs): + handle_history(message_json, eventrouter, **kwargs) + +def handle_imhistory(message_json, eventrouter, **kwargs): + handle_history(message_json, eventrouter, **kwargs) + +def handle_history(message_json, eventrouter, **kwargs): + request_metadata = pickle.loads(message_json["wee_slack_request_metadata"]) + kwargs['team'] = eventrouter.teams[request_metadata.team_hash] + kwargs['channel'] = kwargs['team'].channels[request_metadata.channel_identifier] + for message in message_json["messages"]: + process_message(message, eventrouter, **kwargs) + +###### New/converted process_ and subprocess_ methods + +def process_manual_presence_change(message_json, eventrouter, **kwargs): + process_presence_change(message_json, eventrouter, **kwargs) + + +def process_presence_change(message_json, eventrouter, **kwargs): + kwargs["user"].presence = message_json["presence"] + + +def process_pong(message_json, eventrouter, **kwargs): + pass + +def process_message(message_json, eventrouter, store=True, **kwargs): + channel = kwargs["channel"] + team = kwargs["team"] + #try: + # send these subtype messages elsewhere + known_subtypes = [ + #'thread_message', + #'message_replied', + #'message_changed', + #'message_deleted', + #'channel_join', + #'channel_leave', + #'channel_topic', + #'group_join', + #'group_leave', + ] + if "thread_ts" in message_json and "reply_count" not in message_json: + message_json["subtype"] = "thread_message" + if "subtype" in message_json: + if message_json["subtype"] in known_subtypes: + f = eval('subprocess_' + message_json["subtype"]) + f(message_json, eventrouter, channel, team) + + else: + message = SlackMessage(message_json, team, channel) + #message = Message(message_json, server=team, channel=channel) + text = message.render() + #print text + + # special case with actions. + if text.startswith("_") and text.endswith("_"): + text = text[1:-1] + if message.sender != channel.server.nick: + text = message.sender + " " + text + channel.buffer_prnt(w.prefix("action").rstrip(), text, message.ts) + + else: + suffix = '' + if 'edited' in message_json: + suffix = ' (edited)' + channel.buffer_prnt(message.sender, text + suffix, message.ts) + + if store: + channel.store_message(message, team) + dbg("NORMAL REPLY {}".format(message_json)) + +def subprocess_thread_message(message_json, eventrouter, channel, team): + dbg("REPLIEDDDD: " + str(message_json)) +# channel = channels.find(message_json["channel"]) +# server = channel.server +# #threadinfo = channel.get_message(message_json["thread_ts"]) +# message = Message(message_json, server=server, channel=channel) +# dbg(message, main_buffer=True) +# +# orig = channel.get_message(message_json['thread_ts']) +# if orig[0]: +# channel.get_message(message_json['thread_ts'])[2].add_thread_message(message) +# else: +# dbg("COULDN'T find orig message {}".format(message_json['thread_ts']), main_buffer=True) + + #if threadinfo[0]: + # channel.messages[threadinfo[1]].become_thread() + # message_json["item"]["ts"], message_json) + #channel.change_message(message_json["thread_ts"], None, message_json["text"]) + #channel.become_thread(message_json["item"]["ts"], message_json) + + +def subprocess_message_changed(message_json, eventrouter, channel, team): + m = message_json["message"] + if "message" in message_json: + if "attachments" in m: + message_json["attachments"] = m["attachments"] + if "text" in m: + if "text" in message_json: + message_json["text"] += m["text"] + dbg("added text!") + else: + message_json["text"] = m["text"] + if "fallback" in m: + if "fallback" in message_json: + message_json["fallback"] += m["fallback"] + else: + message_json["fallback"] = m["fallback"] + + text_before = (len(m['text']) > 0) + m["text"] += unwrap_attachments(message_json, text_before) + if "edited" in m: + channel.change_message(m["ts"], m["text"], ' (edited)') + else: + channel.change_message(m["ts"], m["text"]) + +def process_reply(message_json, eventrouter, **kwargs): + dbg('processing reply') + dbg(message_json, True) + team = kwargs["team"] + identifier = message_json["reply_to"] + try: + original_message_json = team.ws_replies[identifier] + del team.ws_replies[identifier] + if "ts" in message_json: + original_message_json["ts"] = message_json["ts"] + else: + dbg("no reply ts {}".format(message_json)) + + if "channel" in original_message_json: + channel = team.channels[original_message_json["channel"]] + m = SlackMessage(original_message_json, team, channel) + # m = Message(message_json, server=server) + dbg(m, True) + + #if "type" in message_json: + # if message_json["type"] == "message" and "channel" in message_json.keys(): + # message_json["ts"] = message_json["ts"] + # channels.find(message_json["channel"]).store_message(m, from_me=True) + + # channels.find(message_json["channel"]).buffer_prnt(server.nick, m.render(), m.ts) + process_message(m.message_json, eventrouter, channel=channel, team=team) + dbg("REPLY {}".format(message_json)) + except KeyError: + dbg("Unexpected reply") + +def process_channel_marked(message_json, eventrouter, **kwargs): + channel = kwargs["channel"] + dbg(channel, True) + #channel.mark_read(False) + #w.buffer_set(channel.channel_buffer, "hotlist", "-1") + +def process_channel_joined(message_json, eventrouter, **kwargs): + print message_json + print kwargs +# server = servers.find(message_json["_server"]) +# channel = server.channels.find(message_json["channel"]) +# text = unfurl_refs(message_json["text"], ignore_alt_text=False) +# channel.buffer_prnt(w.prefix("join").rstrip(), text, message_json["ts"]) +# channel.user_join(message_json["user"]) + + +###### New module/global methods + +def render(message_json, team, channel, force=False): + # If we already have a rendered version in the object, just return that. + if not force and message_json.get("_rendered_text", ""): + return message_json["_rendered_text"] + else: + # server = servers.find(message_json["_server"]) + + if "fallback" in message_json: + text = message_json["fallback"] + elif "text" in message_json: + if message_json['text'] is not None: + text = message_json["text"] + else: + text = u"" + else: + text = u"" + + text = unfurl_refs(text, ignore_alt_text=config.unfurl_ignore_alt_text) + + text_before = (len(text) > 0) + text += unfurl_refs(unwrap_attachments(message_json, text_before), ignore_alt_text=config.unfurl_ignore_alt_text) + + text = text.lstrip() + text = text.replace("\t", " ") + text = text.replace("<", "<") + text = text.replace(">", ">") + text = text.replace("&", "&") + text = text.encode('utf-8') + +# if self.threads: +# text += " [Replies: {} Thread ID: {} ] ".format(len(self.threads), self.thread_id) +# #for thread in self.threads: + + if "reactions" in message_json: + text += create_reaction_string(message_json["reactions"]) + message_json["_rendered_text"] = text + + return text + +def linkify_text(message): + message = message.split(' ') + for item in enumerate(message): + targets = re.match('.*([@#])([\w.]+\w)(\W*)', item[1]) + if targets and targets.groups()[0] == '@': + named = targets.groups() + if named[1] in ["group", "channel", "here"]: + message[item[0]] = "<!{}>".format(named[1]) + if self.server.users.find(named[1]): + message[item[0]] = "<@{}>{}".format(self.server.users.find(named[1]).identifier, named[2]) + if targets and targets.groups()[0] == '#': + named = targets.groups() + if self.server.channels.find(named[1]): + message[item[0]] = "<#{}|{}>{}".format(self.server.channels.find(named[1]).identifier, named[1], named[2]) + dbg(message) + return " ".join(message) + +def unfurl_refs(text, ignore_alt_text=False): + """ + input : <@U096Q7CQM|someuser> has joined the channel + ouput : someuser has joined the channel + """ + # Find all strings enclosed by <> + # - <https://example.com|example with spaces> + # - <#C2147483705|#otherchannel> + # - <@U2147483697|@othernick> + # Test patterns lives in ./_pytest/test_unfurl.py + matches = re.findall(r"(<[@#]?(?:[^<]*)>)", text) + for m in matches: + # Replace them with human readable strings + text = text.replace(m, unfurl_ref(m[1:-1], ignore_alt_text)) + return text + +def unfurl_ref(ref, ignore_alt_text=False): + id = ref.split('|')[0] + display_text = ref + if ref.find('|') > -1: + if ignore_alt_text: + display_text = resolve_ref(id) + else: + if id.startswith("#C") or id.startswith("@U"): + display_text = ref.split('|')[1] + else: + url, desc = ref.split('|', 1) + display_text = u"{} ({})".format(url, desc) + else: + display_text = resolve_ref(ref) + return display_text + +def unwrap_attachments(message_json, text_before): + attachment_text = '' + if "attachments" in message_json: + if text_before: + attachment_text = u'\n' + for attachment in message_json["attachments"]: + # Attachments should be rendered roughly like: + # + # $pretext + # $author: (if rest of line is non-empty) $title ($title_link) OR $from_url + # $author: (if no $author on previous line) $text + # $fields + t = [] + prepend_title_text = '' + if 'author_name' in attachment: + prepend_title_text = attachment['author_name'] + ": " + if 'pretext' in attachment: + t.append(attachment['pretext']) + if "title" in attachment: + if 'title_link' in attachment: + t.append('%s%s (%s)' % (prepend_title_text, attachment["title"], attachment["title_link"],)) + else: + t.append(prepend_title_text + attachment["title"]) + prepend_title_text = '' + elif "from_url" in attachment: + t.append(attachment["from_url"]) + if "text" in attachment: + tx = re.sub(r' *\n[\n ]+', '\n', attachment["text"]) + t.append(prepend_title_text + tx) + prepend_title_text = '' + if 'fields' in attachment: + for f in attachment['fields']: + if f['title'] != '': + t.append('%s %s' % (f['title'], f['value'],)) + else: + t.append(f['value']) + if t == [] and "fallback" in attachment: + t.append(attachment["fallback"]) + attachment_text += "\n".join([x.strip() for x in t if x]) + return attachment_text + + +def resolve_ref(ref): + #TODO: This hack to use eventrouter needs to go + #this resolver should probably move to the slackteam or eventrouter itself + #global EVENTROUTER + if 'EVENTROUTER' in globals(): + e = EVENTROUTER + if ref.startswith('@U') or ref.startswith('@W'): + for t in e.teams.keys(): + if ref[1:] in e.teams[t].users: + #try: + return "@{}".format(e.teams[t].users[ref[1:]].name) + #except: + # dbg("NAME: {}".format(ref)) + elif ref.startswith('#C'): + for t in e.teams.keys(): + if ref[1:] in e.teams[t].channels: + #try: + return "{}".format(e.teams[t].channels[ref[1:]].name) + #except: + # dbg("CHANNEL: {}".format(ref)) + + # Something else, just return as-is + return ref + +def create_reaction_string(reactions): + count = 0 + if not isinstance(reactions, list): + reaction_string = " [{}]".format(reactions) + else: + reaction_string = ' [' + for r in reactions: + if len(r["users"]) > 0: + count += 1 + if config.show_reaction_nicks: + nicks = [resolve_ref("@{}".format(user)) for user in r["users"]] + users = "({})".format(",".join(nicks)) + else: + users = len(r["users"]) + reaction_string += ":{}:{} ".format(r["name"], users) + reaction_string = reaction_string[:-1] + ']' + if count == 0: + reaction_string = '' + return reaction_string + + +###### NEW EXCEPTIONS + +class ProcessNotImplemented(Exception): + """ + Raised when we try to call process_(something), but + (something) has not been defined as a function. + """ + def __init__(self, function_name): + super(ProcessNotImplemented, self).__init__(function_name) + +class InvalidType(Exception): + """ + Raised when we do type checking to ensure objects of the wrong + type are not used improperly. + """ + def __init__(self, type_str): + super(InvalidType, self).__init__(type_str) + +###### New but probably old and need to migrate + +def closed_slack_debug_buffer_cb(data, buffer): + global slack_debug + slack_debug = None + return w.WEECHAT_RC_OK + + +def create_slack_debug_buffer(): + global slack_debug, debug_string + if slack_debug is not None: + w.buffer_set(slack_debug, "display", "1") + else: + debug_string = None + slack_debug = w.buffer_new("slack-debug", "", "", "closed_slack_debug_buffer_cb", "") + w.buffer_set(slack_debug, "notify", "0") + + +##### END NEW + + +def dbg(message, main_buffer=False, fout=False): + """ + send debug output to the slack-debug buffer and optionally write to a file. + """ + #TODO: do this smarter + #return + global debug_string + message = "DEBUG: {}".format(message) + # message = message.encode('utf-8', 'replace') + if fout: + file('/tmp/debug.log', 'a+').writelines(message + '\n') + if main_buffer: + w.prnt("", "---------") + w.prnt("", "slack: " + message) + else: + if slack_debug and (not debug_string or debug_string in message): + w.prnt(slack_debug, "---------") + w.prnt(slack_debug, message) + + +###### Config code + +class PluginConfig(object): + # Default settings. + # These are in the (string) format that weechat expects; at __init__ time + # this value will be used to set the default for any settings not already + # defined, and then the real (python) values of the settings will be + # extracted. + # TODO: setting descriptions. + settings = { + 'colorize_messages': 'false', + 'colorize_nicks': 'true', + 'colorize_private_chats': 'false', + 'debug_mode': 'false', + 'distracting_channels': '', + 'show_reaction_nicks': 'false', + 'slack_api_token': 'INSERT VALID KEY HERE!', + 'slack_timeout': '20000', + 'switch_buffer_on_join': 'true', + 'trigger_value': 'false', + 'unfurl_ignore_alt_text': 'false', + 'cache_messages': 'true', + } + + # Set missing settings to their defaults. Load non-missing settings from + # weechat configs. + def __init__(self): + for key, default in self.settings.iteritems(): + if not w.config_get_plugin(key): + w.config_set_plugin(key, default) + self.config_changed(None, None, None) + + def __str__(self): + return "".join([x + "\t" + str(self.settings[x]) + "\n" for x in self.settings.keys()]) + + def config_changed(self, data, key, value): + for key in self.settings: + self.settings[key] = self.fetch_setting(key) + if self.debug_mode: + create_slack_debug_buffer() + return w.WEECHAT_RC_OK + + def fetch_setting(self, key): + if hasattr(self, 'get_' + key): + try: + return getattr(self, 'get_' + key)(key) + except: + return self.settings[key] + else: + # Most settings are on/off, so make get_boolean the default + return self.get_boolean(key) + + def __getattr__(self, key): + return self.settings[key] + + def get_boolean(self, key): + return w.config_string_to_boolean(w.config_get_plugin(key)) + + def get_distracting_channels(self, key): + return [x.strip() for x in w.config_get_plugin(key).split(',')] + + def get_slack_api_token(self, key): + token = w.config_get_plugin("slack_api_token") + if token.startswith('${sec.data'): + return w.string_eval_expression(token, {}, {}, {}) + else: + return token + + def get_slack_timeout(self, key): + return int(w.config_get_plugin(key)) + + +# Main +if __name__ == "__main__": + + if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, + SCRIPT_DESC, "script_unloaded", ""): + + version = w.info_get("version_number", "") or 0 + if int(version) < 0x1030000: + w.prnt("", "\nERROR: Weechat version 1.3+ is required to use {}.\n\n".format(SCRIPT_NAME)) + else: + + WEECHAT_HOME = w.info_get("weechat_dir", "") + CACHE_NAME = "slack.cache" + STOP_TALKING_TO_SLACK = False + + # Global var section + slack_debug = None + config = PluginConfig() + config_changed_cb = config.config_changed + + typing_timer = time.time() + domain = None + previous_buffer = None + slack_buffer = None + + buffer_list_update = False + previous_buffer_list_update = 0 + + never_away = False + hide_distractions = False + hotlist = w.infolist_get("hotlist", "", "") + main_weechat_buffer = w.info_get("irc_buffer", "{}.{}".format(domain, "DOESNOTEXIST!@#$")) + + message_cache = collections.defaultdict(list) + if config.cache_messages: + cache_load() + + #servers = SearchList() + #for token in config.slack_api_token.split(','): + # server = SlackServer(token) + # servers.append(server) + #channels = SearchList() + #users = SearchList() + #threads = SearchList() + + w.hook_config("plugins.var.python." + SCRIPT_NAME + ".*", "config_changed_cb", "") + #w.hook_timer(3000, 0, 0, "slack_connection_persistence_cb", "") + + # attach to the weechat hooks we need + #w.hook_timer(1000, 0, 0, "typing_update_cb", "") + #w.hook_timer(1000, 0, 0, "buffer_list_update_cb", "") + w.hook_timer(1000 * 60 * 29, 0, 0, "slack_never_away_cb", "") + w.hook_timer(1000 * 60 * 5, 0, 0, "cache_write_cb", "") + w.hook_signal('buffer_closing', "buffer_closing_callback", "EVENTROUTER") + w.hook_signal('buffer_opened', "buffer_opened_cb", "") + w.hook_signal('buffer_switch', "buffer_switch_callback", "EVENTROUTER") + w.hook_signal('window_switch', "buffer_switch_callback", "EVENTROUTER") + #w.hook_signal('input_text_changed', "typing_notification_cb", "") + w.hook_signal('quit', "quit_notification_cb", "") + w.hook_signal('window_scrolled', "scrolled_cb", "") + #w.hook_command( + # # Command name and description + # 'slack', 'Plugin to allow typing notification and sync of read markers for slack.com', + # # Usage + # '[command] [command options]', + # # Description of arguments + # 'Commands:\n' + + # '\n'.join(cmds.keys()) + + # '\nUse /slack help [command] to find out more\n', + # # Completions + # '|'.join(cmds.keys()), + # # Function name + # 'slack_command_cb', '') + # w.hook_command('me', 'me_command_cb', '') + w.hook_command('me', '', 'stuff', 'stuff2', '', 'me_command_cb', '') + w.hook_command_run('/query', 'join_command_cb', '') + w.hook_command_run('/join', 'join_command_cb', '') + w.hook_command_run('/part', 'part_command_cb', '') + w.hook_command_run('/leave', 'part_command_cb', '') + w.hook_command_run('/topic', 'topic_command_cb', '') + w.hook_command_run('/msg', 'msg_command_cb', '') + w.hook_command_run('/label', 'label_command_cb', '') + w.hook_command_run("/input complete_next", "complete_next_cb", "") + w.hook_command_run('/away', 'away_command_cb', '') + w.hook_completion("nicks", "complete @-nicks for slack", + "nick_completion_cb", "") + #w.bar_item_new('slack_typing_notice', 'typing_bar_item_cb', '') + + tok = config.slack_api_token.split(',')[0] + s = SlackRequest(tok, 'rtm.start', {}) + #async_slack_api_request("slack.com", self.token, "rtm.start", {"ts": t}) + #s = SlackRequest('xoxoxoxox', "blah.get", {"meh": "blah"}) + global EVENTROUTER + EVENTROUTER = EventRouter() + EVENTROUTER.receive(s) + EVENTROUTER.handle_next() + w.hook_timer(10, 0, 0, "handle_next", "") + # END attach to the weechat hooks we need |