weechat/python/autoload/wee_slack.py @ 3420bfcadf90

Oh god
author Steve Losh <steve@stevelosh.com>
date Tue, 14 Mar 2017 13:41:40 +0000
parents 68fb1509991b
children b89b95f1cb1d
# -*- coding: utf-8 -*-
#

from functools import wraps

import time
import json
import os
import pickle
import sha
import re
import urllib
import HTMLParser
import sys
import traceback
import collections
import ssl

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 = "0.99.9"
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",
    }

}

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}


def dbg(message, fout=False, main_buffer=False):
    """
    send debug output to the slack-debug buffer and optionally write to a file.
    """
    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("", "slack: " + message)
    else:
        if slack_debug is not None:
            w.prnt(slack_debug, message)


class SearchList(list):
    """
    A normal python list with some syntactic sugar for searchability
    """
    def __init__(self):
        self.hashtable = {}
        super(SearchList, self).__init__(self)

    def find(self, name):
        if name in self.hashtable:
            return self.hashtable[name]
        # this is a fallback to __eq__ if the item isn't in the hashtable already
        if name in self:
            self.update_hashtable()
            return self[self.index(name)]

    def append(self, item, aliases=[]):
        super(SearchList, self).append(item)
        self.update_hashtable(item)

    def update_hashtable(self, item=None):
        if item is not None:
            try:
                for alias in item.get_aliases():
                    if alias is not None:
                        self.hashtable[alias] = item
            except AttributeError:
                pass
        else:
            for child in self:
                try:
                    for alias in child.get_aliases():
                        if alias is not None:
                            self.hashtable[alias] = child
                except AttributeError:
                    pass

    def find_by_class(self, class_name):
        items = []
        for child in self:
            if child.__class__ == class_name:
                items.append(child)
        return items

    def find_by_class_deep(self, class_name, attribute):
        items = []
        for child in self:
            if child.__class__ == self.__class__:
                items += child.find_by_class_deep(class_name, attribute)
            else:
                items += (eval('child.' + attribute).find_by_class(class_name))
        return items


class SlackServer(object):
    """
    Root object used to represent connection and state of the connection to a slack group.
    """
    def __init__(self, token):
        self.nick = None
        self.name = None
        self.team = None
        self.domain = None
        self.server_buffer_name = None
        self.login_data = None
        self.buffer = None
        self.token = token
        self.ws = None
        self.ws_hook = None
        self.users = SearchList()
        self.bots = SearchList()
        self.channels = SearchList()
        self.connecting = False
        self.connected = False
        self.connection_attempt_time = 0
        self.communication_counter = 0
        self.message_buffer = {}
        self.ping_hook = None
        self.alias = None
        self.got_history = False

        self.identifier = None
        self.connect_to_slack()

    def __eq__(self, compare_str):
        if compare_str == self.identifier or compare_str == self.token or compare_str == self.buffer:
            return True
        else:
            return False

    def __str__(self):
        return "{}".format(self.identifier)

    def __repr__(self):
        return "{}".format(self.identifier)

    def add_user(self, user):
        self.users.append(user, user.get_aliases())
        users.append(user, user.get_aliases())

    def add_bot(self, bot):
        self.bots.append(bot)

    def add_channel(self, channel):
        self.channels.append(channel, channel.get_aliases())
        channels.append(channel, channel.get_aliases())

    def get_aliases(self):
        aliases = filter(None, [self.identifier, self.token, self.buffer, self.alias])
        return aliases

    def find(self, name, attribute):
        attribute = eval("self." + attribute)
        return attribute.find(name)

    def get_communication_id(self):
        if self.communication_counter > 999:
            self.communication_counter = 0
        self.communication_counter += 1
        return self.communication_counter

    def send_to_websocket(self, data, expect_reply=True):
        data["id"] = self.get_communication_id()
        message = json.dumps(data)
        try:
            if expect_reply:
                self.message_buffer[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

    def ping(self):
        request = {"type": "ping"}
        self.send_to_websocket(request)

    def should_connect(self):
        """
        If we haven't tried to connect OR we tried and never heard back and it
        has been 125 seconds consider the attempt dead and try again
        """
        if self.connection_attempt_time == 0 or self.connection_attempt_time + 125 < int(time.time()):
            return True
        else:
            return False

    def connect_to_slack(self):
        t = time.time()
        # Double check that we haven't exceeded a long wait to connect and try again.
        if self.connecting and self.should_connect():
            self.connecting = False
        if not self.connecting:
            async_slack_api_request("slack.com", self.token, "rtm.start", {"ts": t})
            self.connection_attempt_time = int(time.time())
            self.connecting = True

    def connected_to_slack(self, login_data):
        if login_data["ok"]:
            self.team = login_data["team"]["domain"]
            self.domain = login_data["team"]["domain"] + ".slack.com"
            dbg("connected to {}".format(self.domain))
            self.identifier = self.domain

            alias = w.config_get_plugin("server_alias.{}".format(login_data["team"]["domain"]))
            if alias:
                self.server_buffer_name = alias
                self.alias = alias
            else:
                self.server_buffer_name = self.domain

            self.nick = login_data["self"]["name"]
            self.create_local_buffer()

            if self.create_slack_websocket(login_data):
                if self.ping_hook:
                    w.unhook(self.ping_hook)
                    self.communication_counter = 0
                self.ping_hook = w.hook_timer(1000 * 5, 0, 0, "slack_ping_cb", self.domain)
                if len(self.users) == 0 or len(self.channels) == 0:
                    self.create_slack_mappings(login_data)

                self.connected = True
                self.connecting = False

                self.print_connection_info(login_data)
                if len(self.message_buffer) > 0:
                    for message_id in self.message_buffer.keys():
                        if self.message_buffer[message_id]["type"] != 'ping':
                            resend = self.message_buffer.pop(message_id)
                            dbg("Resent failed message.")
                            self.send_to_websocket(resend)
                            # sleep to prevent being disconnected by websocket server
                            time.sleep(1)
                        else:
                            self.message_buffer.pop(message_id)
            for chan in self.channels:
                # Set channel history back to false because we will miss messages that came
                # while we were disconnected otherwise.
                chan.got_history = False
                if chan.channel_buffer and chan.muted:
                    w.buffer_set(chan.channel_buffer, "hotlist", "-1")
            return True
        else:
            token_start = self.token[:10]
            error = """
!! slack.com login error: {}
 The problematic token starts with {}
 Please check your API token with
 "/set plugins.var.python.slack_extension.slack_api_token (token)"

""".format(login_data["error"], token_start)
            w.prnt("", error)
            self.connected = False

    def print_connection_info(self, login_data):
        self.buffer_prnt('Connected to Slack', backlog=True)
        self.buffer_prnt('{:<20} {}'.format(u"Websocket URL", login_data["url"]), backlog=True)
        self.buffer_prnt('{:<20} {}'.format(u"User name", login_data["self"]["name"]), backlog=True)
        self.buffer_prnt('{:<20} {}'.format(u"User ID", login_data["self"]["id"]), backlog=True)
        self.buffer_prnt('{:<20} {}'.format(u"Team name", login_data["team"]["name"]), backlog=True)
        self.buffer_prnt('{:<20} {}'.format(u"Team domain", login_data["team"]["domain"]), backlog=True)
        self.buffer_prnt('{:<20} {}'.format(u"Team id", login_data["team"]["id"]), backlog=True)

    def create_local_buffer(self):
        if not w.buffer_search("", self.server_buffer_name):
            self.buffer = w.buffer_new(self.server_buffer_name, "buffer_input_cb", "", "", "")
            if w.config_string(w.config_get('irc.look.server_buffer')) == 'merge_with_core':
                w.buffer_merge(self.buffer, w.buffer_search_main())
            w.buffer_set(self.buffer, "nicklist", "1")

    def create_slack_websocket(self, data):
        web_socket_url = data['url']
        try:
            self.ws = create_connection(web_socket_url, sslopt=sslopt_ca_certs)
            self.ws_hook = w.hook_fd(self.ws.sock._sock.fileno(), 1, 0, 0, "slack_websocket_cb", self.identifier)
            self.ws.sock.setblocking(0)
            return True
        except Exception as e:
            print("websocket connection error: {}".format(e))
            return False

    def create_slack_mappings(self, data):

        for item in data["users"]:
            self.add_user(User(self, item["name"], item["id"], item["presence"], item["deleted"], is_bot=item.get('is_bot', False)))

        for item in data["bots"]:
            self.add_bot(Bot(self, item["name"], item["id"], item["deleted"]))

        for item in data["channels"]:
            item["is_open"] = item["is_member"]
            item["prepend_name"] = "#"
            if not item["is_archived"]:
                self.add_channel(Channel(self, **item))

        for item in data["groups"]:
            item["prepend_name"] = "#"
            if not item["is_archived"]:
                if item["name"].startswith("mpdm-"):
                    self.add_channel(MpdmChannel(self, **item))
                else:
                    self.add_channel(GroupChannel(self, **item))

        for item in data["ims"]:
            if item["unread_count"] > 0 or item["is_open"]:
                item["is_open"] = True
            item['name'] = self.users.find(item["user"]).name
            self.add_channel(DmChannel(self, **item))

        for item in data['self']['prefs']['muted_channels'].split(','):
            if item == '':
                continue
            maybe_muted_chan = self.channels.find(item)
            if maybe_muted_chan is not None:
                maybe_muted_chan.muted = True

        #for item in self.channels:
        #    item.get_history()

    def buffer_prnt(self, message='no message', user="SYSTEM", backlog=False):
        message = message.encode('ascii', 'ignore')
        if backlog:
            tags = "no_highlight,notify_none,logger_backlog_end"
        else:
            tags = ""
        if user == "SYSTEM":
            user = w.config_string(w.config_get('weechat.look.prefix_network'))
        if self.buffer:
            w.prnt_date_tags(self.buffer, 0, tags, "{}\t{}".format(user, message))
        else:
            pass
            # w.prnt("", "%s\t%s" % (user, message))

    def set_away(self, msg):
        async_slack_api_request(self.domain, self.token, 'presence.set', {"presence": "away"})
        for c in self.channels:
            if c.channel_buffer is not None:
                w.buffer_set(c.channel_buffer, "localvar_set_away", msg)

    def set_active(self):
        async_slack_api_request(self.domain, self.token, 'presence.set', {"presence": "active"})
        for c in self.channels:
            if c.channel_buffer is not None:
                w.buffer_set(c.channel_buffer, "localvar_set_away", '')
                w.buffer_set(c.channel_buffer, "localvar_del_away", '')


def buffer_input_cb(b, buffer, data):
    channel = channels.find(buffer)
    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


class Channel(object):
    """
    Represents a single channel and is the source of truth
    for channel <> weechat buffer
    """
    #def __init__(self, server, name, identifier, active, last_read=0, prepend_name="", members=[], topic="", unread_count=0):
    def __init__(self, server, **kwargs):

        self.name = kwargs.get('prepend_name', "") + kwargs.get('name')
        self.current_short_name = kwargs.get('prepend_name', "") + kwargs.get('name')
        self.identifier = kwargs.get('id', 0)
        self.active = kwargs.get('is_open', False)
        self.last_read = float(kwargs.get('last_read', 0))
        self.members = set(kwargs.get('members', []))
        self.topic = kwargs.get('topic', {"value": ""})["value"]
        self.unread_count = kwargs.get('unread_count_display', 0)

        self.members_table = {}
        self.channel_buffer = None
        self.type = "channel"
        self.server = server
        self.typing = {}
        self.last_received = None
        self.messages = []
        self.scrolling = False
        self.last_active_user = None
        self.muted = False
        self.got_history = False
        #w.prnt("", "unread: {}".format(self.unread_count))
        if self.active:
            self.create_buffer()
            self.attach_buffer()
            self.create_members_table()
            self.update_nicklist()
            self.set_topic(self.topic)
            buffer_list_update_next()

    def __str__(self):
        return self.name

    def __repr__(self):
        return self.name

    def __eq__(self, compare_str):
        if compare_str == self.fullname() or compare_str == self.name or compare_str == self.identifier or compare_str == self.name[1:] or (compare_str == self.channel_buffer and self.channel_buffer is not None):
            return True
        else:
            return False

    def get_aliases(self):
        aliases = [self.fullname(), self.name, self.identifier, self.name[1:], ]
        if self.channel_buffer is not None:
            aliases.append(self.channel_buffer)
        return aliases

    def create_members_table(self):
        for user in self.members:
            self.members_table[user] = self.server.users.find(user)

    def create_buffer(self):
        channel_buffer = w.buffer_search("", "{}.{}".format(self.server.server_buffer_name, self.name))
        if channel_buffer:
            self.channel_buffer = channel_buffer
        else:
            self.channel_buffer = w.buffer_new("{}.{}".format(self.server.server_buffer_name, self.name), "buffer_input_cb", self.name, "", "")
            if self.type == "im":
                w.buffer_set(self.channel_buffer, "localvar_set_type", 'private')
            else:
                w.buffer_set(self.channel_buffer, "localvar_set_type", 'channel')
            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)
            w.buffer_set(self.channel_buffer, "localvar_set_channel", self.name)
            w.buffer_set(self.channel_buffer, "short_name", self.name)
            buffer_list_update_next()
        if self.unread_count != 0 and not self.muted:
            w.buffer_set(self.channel_buffer, "hotlist", "1")

    def attach_buffer(self):
        channel_buffer = w.buffer_search("", "{}.{}".format(self.server.server_buffer_name, self.name))
        if channel_buffer != main_weechat_buffer:
            self.channel_buffer = channel_buffer
            w.buffer_set(self.channel_buffer, "localvar_set_nick", self.server.nick)
            w.buffer_set(self.channel_buffer, "highlight_words", self.server.nick)
        else:
            self.channel_buffer = None
        channels.update_hashtable()
        self.server.channels.update_hashtable()

    def detach_buffer(self):
        if self.channel_buffer is not None:
            w.buffer_close(self.channel_buffer)
            self.channel_buffer = None
        channels.update_hashtable()
        self.server.channels.update_hashtable()

    def update_nicklist(self, user=None):
        if not self.channel_buffer:
            return

        w.buffer_set(self.channel_buffer, "nicklist", "1")

        # create nicklists for the current channel if they don't exist
        # if they do, use the existing pointer
        here = w.nicklist_search_group(self.channel_buffer, '', NICK_GROUP_HERE)
        if not here:
            here = w.nicklist_add_group(self.channel_buffer, '', NICK_GROUP_HERE, "weechat.color.nicklist_group", 1)
        afk = w.nicklist_search_group(self.channel_buffer, '', NICK_GROUP_AWAY)
        if not afk:
            afk = w.nicklist_add_group(self.channel_buffer, '', NICK_GROUP_AWAY, "weechat.color.nicklist_group", 1)

        if user:
            user = self.members_table[user]
            nick = w.nicklist_search_nick(self.channel_buffer, "", user.name)
            # since this is a change just remove it regardless of where it is
            w.nicklist_remove_nick(self.channel_buffer, nick)
            # now add it back in to whichever..
            w.nicklist_add_nick(self.channel_buffer, here, user.name, user.color_name, "", "", 1)

        # if we didn't get a user, build a complete list. this is expensive.
        else:
            try:
                for user in self.members:
                    user = self.members_table[user]
                    if user.deleted:
                        continue
                    w.nicklist_add_nick(self.channel_buffer, here, user.name, user.color_name, "", "", 1)
            except Exception as e:
                dbg("DEBUG: {} {} {}".format(self.identifier, self.name, e))

    def fullname(self):
        return "{}.{}".format(self.server.server_buffer_name, self.name)

    def has_user(self, name):
        return name in self.members

    def user_join(self, name):
        self.members.add(name)
        self.create_members_table()
        self.update_nicklist()

    def user_leave(self, name):
        if name in self.members:
            self.members.remove(name)
        self.create_members_table()
        self.update_nicklist()

    def set_active(self):
        self.active = True

    def set_inactive(self):
        self.active = False

    def set_typing(self, user):
        if self.channel_buffer:
            if w.buffer_get_integer(self.channel_buffer, "hidden") == 0:
                self.typing[user] = time.time()
                buffer_list_update_next()

    def unset_typing(self, user):
        if self.channel_buffer:
            if w.buffer_get_integer(self.channel_buffer, "hidden") == 0:
                try:
                    del self.typing[user]
                    buffer_list_update_next()
                except:
                    pass

    def send_message(self, message):
        message = self.linkify_text(message)
        dbg(message)
        request = {"type": "message", "channel": self.identifier, "text": message, "_server": self.server.domain}
        self.server.send_to_websocket(request)

    def linkify_text(self, 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 set_topic(self, topic):
        self.topic = topic.encode('utf-8')
        w.buffer_set(self.channel_buffer, "title", self.topic)

    def open(self, update_remote=True):
        self.create_buffer()
        self.active = True
        self.get_history()
        if "info" in SLACK_API_TRANSLATOR[self.type]:
            async_slack_api_request(self.server.domain, self.server.token, SLACK_API_TRANSLATOR[self.type]["info"], {"name": self.name.lstrip("#")})
        if update_remote:
            if "join" in SLACK_API_TRANSLATOR[self.type]:
                async_slack_api_request(self.server.domain, self.server.token, SLACK_API_TRANSLATOR[self.type]["join"], {"name": self.name.lstrip("#")})

    def close(self, update_remote=True):
        # remove from cache so messages don't reappear when reconnecting
        if self.active:
            self.active = False
            self.current_short_name = ""
            self.detach_buffer()
        if update_remote:
            async_slack_api_request(self.server.domain, self.server.token, SLACK_API_TRANSLATOR[self.type]["leave"], {"channel": self.identifier})

    def closed(self):
        self.channel_buffer = None
        self.last_received = None
        self.close()

    def is_someone_typing(self):
        for user in self.typing.keys():
            if self.typing[user] + 4 > time.time():
                return True
        if len(self.typing) > 0:
            self.typing = {}
            buffer_list_update_next()
        return False

    def get_typing_list(self):
        typing = []
        for user in self.typing.keys():
            if self.typing[user] + 4 > time.time():
                typing.append(user)
        return typing

    def mark_read(self, update_remote=True):
        if self.channel_buffer:
            w.buffer_set(self.channel_buffer, "unread", "")
        if update_remote:
            self.last_read = time.time()
            self.update_read_marker(self.last_read)

    def update_read_marker(self, time):
        async_slack_api_request(self.server.domain, self.server.token, SLACK_API_TRANSLATOR[self.type]["mark"], {"channel": self.identifier, "ts": time})

    def rename(self):
        if self.is_someone_typing():
            new_name = ">{}".format(self.name[1:])
        else:
            new_name = self.name
        if self.channel_buffer:
            if self.current_short_name != new_name:
                self.current_short_name = new_name
                w.buffer_set(self.channel_buffer, "short_name", new_name)

    def buffer_prnt(self, user='unknown_user', message='no message', time=0):
        """
        writes output (message) to a buffer (channel)
        """
        set_read_marker = False
        time_float = float(time)
        tags = "nick_" + user
        user_obj = self.server.users.find(user)
        # XXX: we should not set log1 for robots.
        if time_float != 0 and self.last_read >= time_float:
            tags += ",no_highlight,notify_none,logger_backlog_end"
            set_read_marker = True
        elif message.find(self.server.nick.encode('utf-8')) > -1:
            tags += ",notify_highlight,log1"
        elif user != self.server.nick and self.name in self.server.users:
            tags += ",notify_private,notify_message,log1,irc_privmsg"
        elif self.muted:
            tags += ",no_highlight,notify_none,logger_backlog_end"
        elif user in [x.strip() for x in w.prefix("join"), w.prefix("quit")]:
            tags += ",irc_smart_filter"
        else:
            tags += ",notify_message,log1,irc_privmsg"
        # don't write these to local log files
        # tags += ",no_log"
        time_int = int(time_float)
        if self.channel_buffer:
            prefix_same_nick = w.config_string(w.config_get('weechat.look.prefix_same_nick'))
            if user == self.last_active_user and prefix_same_nick != "":
                if config.colorize_nicks and user_obj:
                    name = user_obj.color + prefix_same_nick
                else:
                    name = prefix_same_nick
            else:
                nick_prefix = w.config_string(w.config_get('weechat.look.nick_prefix'))
                nick_prefix_color_name = w.config_string(w.config_get('weechat.color.chat_nick_prefix'))
                nick_prefix_color = w.color(nick_prefix_color_name)

                nick_suffix = w.config_string(w.config_get('weechat.look.nick_suffix'))
                nick_suffix_color_name = w.config_string(w.config_get('weechat.color.chat_nick_prefix'))
                nick_suffix_color = w.color(nick_suffix_color_name)

                if user_obj:
                    name = user_obj.formatted_name()
                    self.last_active_user = user
                    # XXX: handle bots properly here.
                else:
                    name = user
                    self.last_active_user = None
                name = nick_prefix_color + nick_prefix + w.color("reset") + name + nick_suffix_color + nick_suffix + w.color("reset")
            name = name.decode('utf-8')
            # colorize nicks in each line
            chat_color = w.config_string(w.config_get('weechat.color.chat'))
            if type(message) is not unicode:
                message = message.decode('UTF-8', 'replace')
            curr_color = w.color(chat_color)
            if config.colorize_nicks and config.colorize_messages and user_obj:
                curr_color = user_obj.color
            message = curr_color + message
            for user in self.server.users:
                if user.name in message:
                    message = user.name_regex.sub(
                        r'\1\2{}\3'.format(user.formatted_name() + curr_color),
                        message)

            message = HTMLParser.HTMLParser().unescape(message)
            data = u"{}\t{}".format(name, message).encode('utf-8')
            w.prnt_date_tags(self.channel_buffer, time_int, tags, data)

            if set_read_marker:
                self.mark_read(False)
        else:
            self.open(False)
        self.last_received = time
        self.unset_typing(user)

    def buffer_redraw(self):
        if self.channel_buffer and not self.scrolling:
            w.buffer_clear(self.channel_buffer)
            self.messages.sort()
            for message in self.messages:
                process_message(message.message_json, False)

    def set_scrolling(self):
        self.scrolling = True

    def unset_scrolling(self):
        self.scrolling = False

    def has_message(self, ts):
        return self.messages.count(ts) > 0

    def change_message(self, ts, text=None, suffix=''):
        if self.has_message(ts):
            message_index = self.messages.index(ts)

            if text is not None:
                self.messages[message_index].change_text(text)
            text = render_message(self.messages[message_index].message_json, True)

            # if there is only one message with this timestamp, modify it directly.
            # we do this because time resolution in weechat is less than slack
            int_time = int(float(ts))
            if self.messages.count(str(int_time)) == 1:
                modify_buffer_line(self.channel_buffer, text + suffix, int_time)
            # otherwise redraw the whole buffer, which is expensive
            else:
                self.buffer_redraw()
            return True

    def add_reaction(self, ts, reaction, user):
        if self.has_message(ts):
            message_index = self.messages.index(ts)
            self.messages[message_index].add_reaction(reaction, user)
            self.change_message(ts)
            return True

    def remove_reaction(self, ts, reaction, user):
        if self.has_message(ts):
            message_index = self.messages.index(ts)
            self.messages[message_index].remove_reaction(reaction, user)
            self.change_message(ts)
            return True

    def send_add_reaction(self, msg_number, reaction):
        self.send_change_reaction("reactions.add", msg_number, reaction)

    def send_remove_reaction(self, msg_number, reaction):
        self.send_change_reaction("reactions.remove", msg_number, reaction)

    def send_change_reaction(self, method, msg_number, reaction):
        if 0 < msg_number < len(self.messages):
            timestamp = self.messages[-msg_number].message_json["ts"]
            data = {"channel": self.identifier, "timestamp": timestamp, "name": reaction}
            async_slack_api_request(self.server.domain, self.server.token, method, data)

    def change_previous_message(self, old, new, flags):
        message = self.my_last_message()
        if new == "" and old == "":
            async_slack_api_request(self.server.domain, self.server.token, 'chat.delete', {"channel": self.identifier, "ts": message['ts']})
        else:
            num_replace = 1
            if 'g' in flags:
                num_replace = 0
            new_message = re.sub(old, new, message["text"], num_replace)
            if new_message != message["text"]:
                async_slack_api_request(self.server.domain, self.server.token, 'chat.update', {"channel": self.identifier, "ts": message['ts'], "text": new_message.encode("utf-8")})

    def my_last_message(self):
        for message in reversed(self.messages):
            if "user" in message.message_json and "text" in message.message_json and message.message_json["user"] == self.server.users.find(self.server.nick).identifier:
                return message.message_json

    def cache_message(self, message_json, from_me=False):
        if from_me:
            message_json["user"] = self.server.users.find(self.server.nick).identifier
        self.messages.append(Message(message_json))
        if len(self.messages) > SCROLLBACK_SIZE:
            self.messages = self.messages[-SCROLLBACK_SIZE:]

    def get_history(self):
        if self.active:
            for message in message_cache[self.identifier]:
                process_message(json.loads(message), True)
            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


class GroupChannel(Channel):

    def __init__(self, server, **kwargs):
        super(GroupChannel, self).__init__(server, **kwargs)
        self.type = "group"


class MpdmChannel(Channel):

    def __init__(self, server, **kwargs):
        n = kwargs.get('name')
        name = "|".join("-".join(n.split("-")[1:-1]).split("--"))
        kwargs["name"] = name
        super(MpdmChannel, self).__init__(server, **kwargs)
        self.type = "group"


class DmChannel(Channel):

    def __init__(self, server, **kwargs):
        super(DmChannel, self).__init__(server, **kwargs)
        self.type = "im"

    def rename(self):
        if self.server.users.find(self.name).presence == "active":
            new_name = self.server.users.find(self.name).formatted_name('+', config.colorize_private_chats)
        else:
            new_name = self.server.users.find(self.name).formatted_name(' ', config.colorize_private_chats)

        if self.channel_buffer:
            if self.current_short_name != new_name:
                self.current_short_name = new_name
                w.buffer_set(self.channel_buffer, "short_name", new_name)

    def update_nicklist(self, user=None):
        pass


class User(object):

    def __init__(self, server, name, identifier, presence="away", deleted=False, is_bot=False):
        self.server = server
        self.name = name
        self.identifier = identifier
        self.deleted = deleted
        self.presence = presence

        self.channel_buffer = w.info_get("irc_buffer", "{}.{}".format(domain, self.name))
        self.update_color()
        self.name_regex = re.compile(r"([\W]|\A)(@{0,1})" + self.name + "('s|[^'\w]|\Z)")
        self.is_bot = is_bot

        if deleted:
            return
        self.nicklist_pointer = w.nicklist_add_nick(server.buffer, "", self.name, self.color_name, "", "", 1)
        if self.presence == 'away':
            w.nicklist_nick_set(self.server.buffer, self.nicklist_pointer, "visible", "0")
        else:
            w.nicklist_nick_set(self.server.buffer, self.nicklist_pointer, "visible", "1")
#        w.nicklist_add_nick(server.buffer, "", self.formatted_name(), "", "", "", 1)

    def __str__(self):
        return self.name

    def __repr__(self):
        return self.name

    def __eq__(self, compare_str):
        try:
            if compare_str == self.name or compare_str == self.identifier:
                return True
            elif compare_str[0] == '@' and compare_str[1:] == self.name:
                return True
            else:
                return False
        except:
            return False

    def get_aliases(self):
        return [self.name, "@" + self.name, self.identifier]

    def set_active(self):
        if not self.deleted:
            self.presence = "active"
            dm_channel = self.server.channels.find(self.name)
            if dm_channel and dm_channel.active:
                buffer_list_update_next()

        return #temporarily noop this
        for channel in self.server.channels:
            if channel.has_user(self.identifier):
                channel.update_nicklist(self.identifier)
        w.nicklist_nick_set(self.server.buffer, self.nicklist_pointer, "visible", "1")

    def set_inactive(self):
        if not self.deleted:
            self.presence = "away"
            dm_channel = self.server.channels.find(self.name)
            if dm_channel and dm_channel.active:
                buffer_list_update_next()

        return #temporarily noop this
        if self.deleted:
            return

        for channel in self.server.channels:
            if channel.has_user(self.identifier):
                channel.update_nicklist(self.identifier)
        w.nicklist_nick_set(self.server.buffer, self.nicklist_pointer, "visible", "0")

    def update_color(self):
        if config.colorize_nicks:
            if self.name == self.server.nick:
                self.color_name = w.config_string(w.config_get('weechat.color.chat_nick_self'))
            else:
                self.color_name = w.info_get('irc_nick_color_name', self.name)
            self.color = w.color(self.color_name)
        else:
            self.color = ""
            self.color_name = ""

    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

    def create_dm_channel(self):
        async_slack_api_request(self.server.domain, self.server.token, "im.open", {"user": self.identifier})


class Bot(object):

    def __init__(self, server, name, identifier, deleted=False):
        self.server = server
        self.name = name
        self.identifier = identifier
        self.deleted = deleted
        self.update_color()

    def __eq__(self, compare_str):
        if compare_str == self.identifier or compare_str == self.name:
            return True
        else:
            return False

    def __str__(self):
        return "{}".format(self.identifier)

    def __repr__(self):
        return "{}".format(self.identifier)

    def update_color(self):
        if config.colorize_nicks:
            self.color_name = w.info_get('irc_nick_color_name', self.name.encode('utf-8'))
            self.color = w.color(self.color_name)
        else:
            self.color_name = ""
            self.color = ""

    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 Message(object):

    def __init__(self, message_json):
        self.message_json = message_json
        self.ts = message_json['ts']
        # split timestamp into time and counter
        self.ts_time, self.ts_counter = message_json['ts'].split('.')

    def change_text(self, new_text):
        if not isinstance(new_text, unicode):
            new_text = unicode(new_text, 'utf-8')
        self.message_json["text"] = new_text

    def add_reaction(self, reaction, user):
        if "reactions" in self.message_json:
            found = False
            for r in self.message_json["reactions"]:
                if r["name"] == reaction and user not in r["users"]:
                    r["users"].append(user)
                    found = True

            if not found:
                self.message_json["reactions"].append({u"name": reaction, u"users": [user]})
        else:
            self.message_json["reactions"] = [{u"name": reaction, u"users": [user]}]

    def remove_reaction(self, reaction, user):
        if "reactions" in self.message_json:
            for r in self.message_json["reactions"]:
                if r["name"] == reaction and user in r["users"]:
                    r["users"].remove(user)
        else:
            pass

    def __eq__(self, other):
        return self.ts_time == other or self.ts == other

    def __repr__(self):
        return "{} {} {} {}\n".format(self.ts_time, self.ts_counter, self.ts, self.message_json)

    def __lt__(self, other):
        return self.ts < other.ts


def slack_buffer_or_ignore(f):
    """
    Only run this function if we're in a slack buffer, else ignore
    """
    @wraps(f)
    def wrapper(current_buffer, *args, **kwargs):
        server = servers.find(current_domain_name())
        if not server:
            return w.WEECHAT_RC_OK
        return f(current_buffer, *args, **kwargs)
    return wrapper


def slack_command_cb(data, current_buffer, args):
    a = args.split(' ', 1)
    if len(a) > 1:
        function_name, args = a[0], " ".join(a[1:])
    else:
        function_name, args = a[0], None

    try:
        cmds[function_name](current_buffer, args)
    except KeyError:
        w.prnt("", "Command not found: " + function_name)
    return w.WEECHAT_RC_OK


@slack_buffer_or_ignore
def me_command_cb(data, current_buffer, args):
    if channels.find(current_buffer):
        # channel = channels.find(current_buffer)
        # nick = channel.server.nick
        message = "_{}_".format(args)
        buffer_input_cb("", current_buffer, message)
    return w.WEECHAT_RC_OK


@slack_buffer_or_ignore
def join_command_cb(data, current_buffer, args):
    args = args.split()
    if len(args) < 2:
        w.prnt(current_buffer, "Missing channel argument")
        return w.WEECHAT_RC_OK_EAT
    elif command_talk(current_buffer, args[1]):
        return w.WEECHAT_RC_OK_EAT
    else:
        return w.WEECHAT_RC_OK


@slack_buffer_or_ignore
def part_command_cb(data, current_buffer, args):
    if channels.find(current_buffer) or servers.find(current_buffer):
        args = args.split()
        if len(args) > 1:
            channel = args[1:]
            servers.find(current_domain_name()).channels.find(channel).close(True)
        else:
            channels.find(current_buffer).close(True)
        return w.WEECHAT_RC_OK_EAT
    else:
        return w.WEECHAT_RC_OK


# Wrap command_ functions that require they be performed in a slack buffer
def slack_buffer_required(f):
    @wraps(f)
    def wrapper(current_buffer, *args, **kwargs):
        server = servers.find(current_domain_name())
        if not server:
            w.prnt(current_buffer, "This command must be used in a slack buffer")
            return w.WEECHAT_RC_ERROR
        return f(current_buffer, *args, **kwargs)
    return wrapper


def command_register(current_buffer, args):
    CLIENT_ID = "2468770254.51917335286"
    CLIENT_SECRET = "dcb7fe380a000cba0cca3169a5fe8d70"  # this is not really a secret
    if not args:
        message = """
# ### Retrieving a Slack token via OAUTH ####

1) Paste this into a browser: https://slack.com/oauth/authorize?client_id=2468770254.51917335286&scope=client
2) Select the team you wish to access from wee-slack in your browser.
3) Click "Authorize" in the browser **IMPORTANT: the redirect will fail, this is expected**
4) Copy the "code" portion of the URL to your clipboard
5) Return to weechat and run `/slack register [code]`
6) Add the returned token per the normal wee-slack setup instructions


"""
        w.prnt(current_buffer, message)
    else:
        aargs = args.split(None, 2)
        if len(aargs) != 1:
            w.prnt(current_buffer, "ERROR: invalid args to register")
        else:
            # w.prnt(current_buffer, "https://slack.com/api/oauth.access?client_id={}&client_secret={}&code={}".format(CLIENT_ID, CLIENT_SECRET, aargs[0]))
            ret = urllib.urlopen("https://slack.com/api/oauth.access?client_id={}&client_secret={}&code={}".format(CLIENT_ID, CLIENT_SECRET, aargs[0])).read()
            d = json.loads(ret)
            if d["ok"] == True:
                w.prnt(current_buffer, "Success! Access token is: " + d['access_token'])
            else:
                w.prnt(current_buffer, "Failed! Error is: " + d['error'])


@slack_buffer_or_ignore
def msg_command_cb(data, current_buffer, args):
    dbg("msg_command_cb")
    aargs = args.split(None, 2)
    who = aargs[1]

    command_talk(current_buffer, who)

    if len(aargs) > 2:
        message = aargs[2]
        server = servers.find(current_domain_name())
        if server:
            channel = server.channels.find(who)
            channel.send_message(message)
    return w.WEECHAT_RC_OK_EAT


@slack_buffer_required
def command_upload(current_buffer, args):
    """
    Uploads a file to the current buffer
    /slack upload [file_path]
    """
    post_data = {}
    channel = current_buffer_name(short=True)
    domain = current_domain_name()
    token = servers.find(domain).token

    if servers.find(domain).channels.find(channel):
        channel_identifier = servers.find(domain).channels.find(channel).identifier

    if channel_identifier:
        post_data["token"] = token
        post_data["channels"] = channel_identifier
        post_data["file"] = args
        async_slack_api_upload_request(token, "files.upload", post_data)


def command_talk(current_buffer, args):
    """
    Open a chat with the specified user
    /slack talk [user]
    """

    server = servers.find(current_domain_name())
    if server:
        channel = server.channels.find(args)
        if channel is None:
            user = server.users.find(args)
            if user:
                user.create_dm_channel()
            else:
                server.buffer_prnt("User or channel {} not found.".format(args))
        else:
            channel.open()
            if config.switch_buffer_on_join:
                w.buffer_set(channel.channel_buffer, "display", "1")
        return True
    else:
        return False


def command_join(current_buffer, args):
    """
    Join the specified channel
    /slack join [channel]
    """
    domain = current_domain_name()
    if domain == "":
        if len(servers) == 1:
            domain = servers[0]
        else:
            w.prnt(current_buffer, "You are connected to multiple Slack instances, please execute /join from a server buffer. i.e. (domain).slack.com")
            return
    channel = servers.find(domain).channels.find(args)
    if channel is not None:
        servers.find(domain).channels.find(args).open()
    else:
        w.prnt(current_buffer, "Channel not found.")


@slack_buffer_required
def command_channels(current_buffer, args):
    """
    List all the channels for the slack instance (name, id, active)
    /slack channels
    """
    server = servers.find(current_domain_name())
    for channel in server.channels:
        line = "{:<25} {} {}".format(channel.name, channel.identifier, channel.active)
        server.buffer_prnt(line)


def command_nodistractions(current_buffer, args):
    global hide_distractions
    hide_distractions = not hide_distractions
    if config.distracting_channels != ['']:
        for channel in config.distracting_channels:
            try:
                channel_buffer = channels.find(channel).channel_buffer
                if channel_buffer:
                    w.buffer_set(channels.find(channel).channel_buffer, "hidden", str(int(hide_distractions)))
            except:
                dbg("Can't hide channel {} .. removing..".format(channel), main_buffer=True)
                config.distracting_channels.pop(config.distracting_channels.index(channel))
                save_distracting_channels()


def command_distracting(current_buffer, args):
    if channels.find(current_buffer) is None:
        w.prnt(current_buffer, "This command must be used in a channel buffer")
        return
    fullname = channels.find(current_buffer).fullname()
    if config.distracting_channels.count(fullname) == 0:
        config.distracting_channels.append(fullname)
    else:
        config.distracting_channels.pop(config.distracting_channels.index(fullname))
    save_distracting_channels()


def save_distracting_channels():
    w.config_set_plugin('distracting_channels', ','.join(config.distracting_channels))


@slack_buffer_required
def command_users(current_buffer, args):
    """
    List all the users for the slack instance (name, id, away)
    /slack users
    """
    server = servers.find(current_domain_name())
    for user in server.users:
        line = "{:<40} {} {}".format(user.formatted_name(), user.identifier, user.presence)
        server.buffer_prnt(line)


def command_setallreadmarkers(current_buffer, args):
    """
    Sets the read marker for all channels
    /slack setallreadmarkers
    """
    for channel in channels:
        channel.mark_read()


def command_changetoken(current_buffer, args):
    w.config_set_plugin('slack_api_token', args)


def command_test(current_buffer, args):
    w.prnt(current_buffer, "worked!")


def away_command_cb(data, current_buffer, args):
    (all, message) = re.match("^/away(?:\s+(-all))?(?:\s+(.+))?", args).groups()
    if all is None:
        server = servers.find(current_domain_name())
        if not server:
            return w.WEECHAT_RC_OK
        if message is None:
            server.set_active()
        else:
            server.set_away(message)
        return w.WEECHAT_RC_OK_EAT
    for server in servers:
        if message is None:
            server.set_active()
        else:
            server.set_away(message)
        return w.WEECHAT_RC_OK


@slack_buffer_required
def command_away(current_buffer, args):
    """
    Sets your status as 'away'
    /slack away
    """
    server = servers.find(current_domain_name())
    async_slack_api_request(server.domain, server.token, 'presence.set', {"presence": "away"})


@slack_buffer_required
def command_back(current_buffer, args):
    """
    Sets your status as 'back'
    /slack back
    """
    server = servers.find(current_domain_name())
    async_slack_api_request(server.domain, server.token, 'presence.set', {"presence": "active"})


@slack_buffer_required
def command_markread(current_buffer, args):
    """
    Marks current channel as read
    /slack markread
    """
    # refactor this - one liner i think
    channel = current_buffer_name(short=True)
    domain = current_domain_name()
    if servers.find(domain).channels.find(channel):
        servers.find(domain).channels.find(channel).mark_read()


@slack_buffer_required
def command_slash(current_buffer, args):
    """
    Support for custom slack commands
    /slack slash /customcommand arg1 arg2 arg3
    """

    server = servers.find(current_domain_name())
    channel = current_buffer_name(short=True)
    domain = current_domain_name()

    if args is None:
        server.buffer_prnt("Usage: /slack slash /someslashcommand [arguments...].")
        return

    split_args = args.split(None, 1)

    command = split_args[0]
    text = split_args[1] if len(split_args) > 1 else ""

    if servers.find(domain).channels.find(channel):
        channel_identifier = servers.find(domain).channels.find(channel).identifier

    if channel_identifier:
        async_slack_api_request(server.domain, server.token, 'chat.command', {'command': command, 'text': text, 'channel': channel_identifier})
    else:
        server.buffer_prnt("User or channel not found.")


def command_flushcache(current_buffer, args):
    global message_cache
    message_cache = collections.defaultdict(list)
    cache_write_cb("", "")


def command_cachenow(current_buffer, args):
    cache_write_cb("", "")


def command_neveraway(current_buffer, args):
    global never_away
    if never_away:
        never_away = False
        dbg("unset never_away", main_buffer=True)
    else:
        never_away = True
        dbg("set never_away", main_buffer=True)


def command_printvar(current_buffer, args):
    w.prnt("", "{}".format(eval(args)))


def command_p(current_buffer, args):
    w.prnt("", "{}".format(eval(args)))


def command_debug(current_buffer, args):
    create_slack_debug_buffer()


def command_debugstring(current_buffer, args):
    global debug_string
    if args == '':
        debug_string = None
    else:
        debug_string = args


def command_search(current_buffer, args):
    pass
#    if not slack_buffer:
#        create_slack_buffer()
#    w.buffer_set(slack_buffer, "display", "1")
#    query = args
#    w.prnt(slack_buffer,"\nSearched for: %s\n\n" % (query))
#    reply = slack_api_request('search.messages', {"query":query}).read()
#    data = json.loads(reply)
#    for message in data['messages']['matches']:
#        message["text"] = message["text"].encode('ascii', 'ignore')
#        formatted_message = "%s / %s:\t%s" % (message["channel"]["name"], message['username'], message['text'])
#        w.prnt(slack_buffer,str(formatted_message))


def command_nick(current_buffer, args):
    pass
#    urllib.urlopen("https://%s/account/settings" % (domain))
#    browser.select_form(nr=0)
#    browser.form['username'] = args
#    reply = browser.submit()


def command_help(current_buffer, args):
    help_cmds = {k[8:]: v.__doc__ for k, v in globals().items() if k.startswith("command_")}

    if args:
        try:
            help_cmds = {args: help_cmds[args]}
        except KeyError:
            w.prnt("", "Command not found: " + args)
            return

    for cmd, helptext in help_cmds.items():
        w.prnt('', w.color("bold") + cmd)
        w.prnt('', (helptext or 'No help text').strip())
        w.prnt('', '')

# Websocket handling methods


def command_openweb(current_buffer, args):
    trigger = config.trigger_value
    if trigger != "0":
        if args is None:
            channel = channels.find(current_buffer)
            url = "{}/messages/{}".format(channel.server.server_buffer_name, channel.name)
            topic = w.buffer_get_string(channel.channel_buffer, "title")
            w.buffer_set(channel.channel_buffer, "title", "{}:{}".format(trigger, url))
            w.hook_timer(1000, 0, 1, "command_openweb", json.dumps({"topic": topic, "buffer": current_buffer}))
        else:
            # TODO: fix this dirty hack because i don't know the right way to send multiple args.
            args = current_buffer
            data = json.loads(args)
            channel_buffer = channels.find(data["buffer"]).channel_buffer
            w.buffer_set(channel_buffer, "title", data["topic"])
    return w.WEECHAT_RC_OK


@slack_buffer_or_ignore
def topic_command_cb(data, current_buffer, args):
    n = len(args.split())
    if n < 2:
        channel = channels.find(current_buffer)
        if channel:
            w.prnt(current_buffer, 'Topic for {} is "{}"'.format(channel.name, channel.topic))
        return w.WEECHAT_RC_OK_EAT
    elif command_topic(current_buffer, args.split(None, 1)[1]):
        return w.WEECHAT_RC_OK_EAT
    else:
        return w.WEECHAT_RC_ERROR


def command_topic(current_buffer, args):
    """
    Change the topic of a channel
    /slack topic [<channel>] [<topic>|-delete]
    """
    server = servers.find(current_domain_name())
    if server:
        arrrrgs = args.split(None, 1)
        if arrrrgs[0].startswith('#'):
            channel = server.channels.find(arrrrgs[0])
            topic = arrrrgs[1]
        else:
            channel = server.channels.find(current_buffer)
            topic = args

        if channel:
            if topic == "-delete":
                async_slack_api_request(server.domain, server.token, 'channels.setTopic', {"channel": channel.identifier, "topic": ""})
            else:
                async_slack_api_request(server.domain, server.token, 'channels.setTopic', {"channel": channel.identifier, "topic": topic})
            return True
        else:
            return False
    else:
        return False


def slack_websocket_cb(server, fd):
    try:
        data = servers.find(server).ws.recv()
        message_json = json.loads(data)
        # this magic attaches json that helps find the right dest
        message_json['_server'] = server
    except WebSocketConnectionClosedException:
        servers.find(server).ws.close()
        return w.WEECHAT_RC_OK
    except Exception:
        dbg("socket issue: {}\n".format(traceback.format_exc()))
        return w.WEECHAT_RC_OK
    # dispatch here
    if "reply_to" in message_json:
        function_name = "reply"
    elif "type" in message_json:
        function_name = message_json["type"]
    else:
        function_name = "unknown"
    try:
        proc[function_name](message_json)
    except KeyError:
        if function_name:
            dbg("Function not implemented: {}\n{}".format(function_name, message_json))
        else:
            dbg("Function not implemented\n{}".format(message_json))
    w.bar_item_update("slack_typing_notice")
    return w.WEECHAT_RC_OK


def process_reply(message_json):
    server = servers.find(message_json["_server"])
    identifier = message_json["reply_to"]
    item = server.message_buffer.pop(identifier)
    if 'text' in item and type(item['text']) is not unicode:
        item['text'] = item['text'].decode('UTF-8', 'replace')
    if "type" in item:
        if item["type"] == "message" and "channel" in item.keys():
            item["ts"] = message_json["ts"]
            channels.find(item["channel"]).cache_message(item, from_me=True)
            text = unfurl_refs(item["text"], ignore_alt_text=config.unfurl_ignore_alt_text)

            channels.find(item["channel"]).buffer_prnt(item["user"], text, item["ts"])
    dbg("REPLY {}".format(item))


def process_pong(message_json):
    pass


def process_pref_change(message_json):
    server = servers.find(message_json["_server"])
    if message_json['name'] == u'muted_channels':
        muted = message_json['value'].split(',')
        for c in server.channels:
            if c.identifier in muted:
                c.muted = True
            else:
                c.muted = False
    else:
        dbg("Preference change not implemented: {}\n".format(message_json['name']))


def process_team_join(message_json):
    server = servers.find(message_json["_server"])
    item = message_json["user"]
    server.add_user(User(server, item["name"], item["id"], item["presence"]))
    server.buffer_prnt("New user joined: {}".format(item["name"]))


def process_manual_presence_change(message_json):
    process_presence_change(message_json)


def process_presence_change(message_json):
    server = servers.find(message_json["_server"])
    identifier = message_json.get("user", server.nick)
    if message_json["presence"] == 'active':
        server.users.find(identifier).set_active()
    else:
        server.users.find(identifier).set_inactive()


def process_channel_marked(message_json):
    channel = channels.find(message_json["channel"])
    channel.mark_read(False)
    w.buffer_set(channel.channel_buffer, "hotlist", "-1")


def process_group_marked(message_json):
    channel = channels.find(message_json["channel"])
    channel.mark_read(False)
    w.buffer_set(channel.channel_buffer, "hotlist", "-1")


def process_channel_created(message_json):
    server = servers.find(message_json["_server"])
    item = message_json["channel"]
    if server.channels.find(message_json["channel"]["name"]):
        server.channels.find(message_json["channel"]["name"]).open(False)
    else:
        item = message_json["channel"]
        item["prepend_name"] = "#"
        server.add_channel(Channel(server, **item))
    server.buffer_prnt("New channel created: {}".format(item["name"]))


def process_channel_left(message_json):
    server = servers.find(message_json["_server"])
    server.channels.find(message_json["channel"]).close(False)


def process_channel_join(message_json):
    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"])


def process_channel_topic(message_json):
    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("network").rstrip(), text, message_json["ts"])
    channel.set_topic(message_json["topic"])


def process_channel_joined(message_json):
    server = servers.find(message_json["_server"])
    if server.channels.find(message_json["channel"]["name"]):
        server.channels.find(message_json["channel"]["name"]).open(False)
    else:
        item = message_json["channel"]
        item["prepend_name"] = "#"
        server.add_channel(Channel(server, **item))


def process_channel_leave(message_json):
    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("quit").rstrip(), text, message_json["ts"])
    channel.user_leave(message_json["user"])


def process_channel_archive(message_json):
    server = servers.find(message_json["_server"])
    channel = server.channels.find(message_json["channel"])
    channel.detach_buffer()


def process_group_join(message_json):
    process_channel_join(message_json)


def process_group_leave(message_json):
    process_channel_leave(message_json)


def process_group_topic(message_json):
    process_channel_topic(message_json)


def process_group_left(message_json):
    server = servers.find(message_json["_server"])
    server.channels.find(message_json["channel"]).close(False)


def process_group_joined(message_json):
    server = servers.find(message_json["_server"])
    if server.channels.find(message_json["channel"]["name"]):
        server.channels.find(message_json["channel"]["name"]).open(False)
    else:
        item = message_json["channel"]
        item["prepend_name"] = "#"
        if item["name"].startswith("mpdm-"):
            server.add_channel(MpdmChannel(server, **item))
        else:
            server.add_channel(GroupChannel(server, **item))

def process_group_archive(message_json):
    channel = server.channels.find(message_json["channel"])
    channel.detach_buffer()


def process_mpim_close(message_json):
    server = servers.find(message_json["_server"])
    server.channels.find(message_json["channel"]).close(False)


def process_mpim_open(message_json):
    server = servers.find(message_json["_server"])
    server.channels.find(message_json["channel"]).open(False)


def process_im_close(message_json):
    server = servers.find(message_json["_server"])
    server.channels.find(message_json["channel"]).close(False)


def process_im_open(message_json):
    server = servers.find(message_json["_server"])
    server.channels.find(message_json["channel"]).open()


def process_im_marked(message_json):
    channel = channels.find(message_json["channel"])
    channel.mark_read(False)
    if channel.channel_buffer is not None:
        w.buffer_set(channel.channel_buffer, "hotlist", "-1")


def process_im_created(message_json):
    server = servers.find(message_json["_server"])
    item = message_json["channel"]
    channel_name = server.users.find(item["user"]).name
    if server.channels.find(channel_name):
        server.channels.find(channel_name).open(False)
    else:
        item = message_json["channel"]
        item['name'] = server.users.find(item["user"]).name
        server.add_channel(DmChannel(server, **item))
    server.buffer_prnt("New direct message channel created: {}".format(item["name"]))


def process_user_typing(message_json):
    server = servers.find(message_json["_server"])
    channel = server.channels.find(message_json["channel"])
    if channel:
        channel.set_typing(server.users.find(message_json["user"]).name)


def process_bot_enable(message_json):
    process_bot_integration(message_json)


def process_bot_disable(message_json):
    process_bot_integration(message_json)


def process_bot_integration(message_json):
    server = servers.find(message_json["_server"])
    channel = server.channels.find(message_json["channel"])

    time = message_json['ts']
    text = "{} {}".format(server.users.find(message_json['user']).formatted_name(),
                          render_message(message_json))
    bot_name = get_user(message_json, server)
    bot_name = bot_name.encode('utf-8')
    channel.buffer_prnt(bot_name, text, time)

# todo: does this work?


def process_error(message_json):
    pass


def process_reaction_added(message_json):
    if message_json["item"].get("type") == "message":
        channel = channels.find(message_json["item"]["channel"])
        channel.add_reaction(message_json["item"]["ts"], message_json["reaction"], message_json["user"])
    else:
        dbg("Reaction to item type not supported: " + str(message_json))


def process_reaction_removed(message_json):
    if message_json["item"].get("type") == "message":
        channel = channels.find(message_json["item"]["channel"])
        channel.remove_reaction(message_json["item"]["ts"], message_json["reaction"], message_json["user"])
    else:
        dbg("Reaction to item type not supported: " + str(message_json))


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


def modify_buffer_line(buffer, new_line, time):
    time = int(float(time))
    # get a pointer to this buffer's lines
    own_lines = w.hdata_pointer(w.hdata_get('buffer'), buffer, 'own_lines')
    if own_lines:
        # get a pointer to the last line
        line_pointer = w.hdata_pointer(w.hdata_get('lines'), own_lines, 'last_line')
        # hold the structure of a line and of line data
        struct_hdata_line = w.hdata_get('line')
        struct_hdata_line_data = w.hdata_get('line_data')

        while line_pointer:
            # get a pointer to the data in line_pointer via layout of struct_hdata_line
            data = w.hdata_pointer(struct_hdata_line, line_pointer, 'data')
            if data:
                date = w.hdata_time(struct_hdata_line_data, data, 'date')
                # prefix = w.hdata_string(struct_hdata_line_data, data, 'prefix')

                if int(date) == int(time):
                    # w.prnt("", "found matching time date is {}, time is {} ".format(date, time))
                    w.hdata_update(struct_hdata_line_data, data, {"message": new_line})
                    break
                else:
                    pass
            # move backwards one line and try again - exit the while if you hit the end
            line_pointer = w.hdata_move(struct_hdata_line, line_pointer, -1)
    return w.WEECHAT_RC_OK


def render_message(message_json, 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("&lt;", "<")
        text = text.replace("&gt;", ">")
        text = text.replace("&amp;", "&")
        text = text.encode('utf-8')

        if "reactions" in message_json:
            text += create_reaction_string(message_json["reactions"])
        message_json["_rendered_text"] = text
        return text


def process_message(message_json, cache=True):
    try:
        # send these subtype messages elsewhere
        known_subtypes = ["message_changed", 'message_deleted', 'channel_join', 'channel_leave', 'channel_topic', 'group_join', 'group_leave', 'group_topic', 'bot_enable', 'bot_disable']
        if "subtype" in message_json and message_json["subtype"] in known_subtypes:
            proc[message_json["subtype"]](message_json)

        else:
            server = servers.find(message_json["_server"])
            channel = channels.find(message_json["channel"])

            # do not process messages in unexpected channels
            if not channel.active:
                channel.open(False)
                dbg("message came for closed channel {}".format(channel.name))
                return

            time = message_json['ts']
            text = render_message(message_json)
            name = get_user(message_json, server)
            name = name.encode('utf-8')

            # special case with actions.
            if text.startswith("_") and text.endswith("_"):
                text = text[1:-1]
                if name != channel.server.nick:
                    text = name + " " + text
                channel.buffer_prnt(w.prefix("action").rstrip(), text, time)

            else:
                suffix = ''
                if 'edited' in message_json:
                    suffix = ' (edited)'
                channel.buffer_prnt(name, text + suffix, time)

            if cache:
                channel.cache_message(message_json)

    except Exception:
        channel = channels.find(message_json["channel"])
        dbg("cannot process message {}\n{}".format(message_json, traceback.format_exc()))
        if channel and ("text" in message_json) and message_json['text'] is not None:
            channel.buffer_prnt('unknown', message_json['text'])


def process_message_changed(message_json):
    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)
    channel = channels.find(message_json["channel"])
    if "edited" in m:
        channel.change_message(m["ts"], m["text"], ' (edited)')
    else:
        channel.change_message(m["ts"], m["text"])


def process_message_deleted(message_json):
    channel = channels.find(message_json["channel"])
    channel.change_message(message_json["deleted_ts"], "(deleted)")


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):
    if ref.startswith('@U') or ref.startswith('@W'):
        if users.find(ref[1:]):
            try:
                return "@{}".format(users.find(ref[1:]).name)
            except:
                dbg("NAME: {}".format(ref))
    elif ref.startswith('#C'):
        if channels.find(ref[1:]):
            try:
                return "{}".format(channels.find(ref[1:]).name)
            except:
                dbg("CHANNEL: {}".format(ref))

    # Something else, just return as-is
    return ref


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 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 get_user(message_json, server):
    if 'bot_id' in message_json and message_json['bot_id'] is not None:
        name = u"{} :]".format(server.bots.find(message_json["bot_id"]).formatted_name())
    elif 'user' in message_json:
        u = server.users.find(message_json['user'])
        if u.is_bot:
            name = u"{} :]".format(u.formatted_name())
        else:
            name = u.name
    elif 'username' in message_json:
        name = u"-{}-".format(message_json["username"])
    elif 'service_name' in message_json:
        name = u"-{}-".format(message_json["service_name"])
    else:
        name = u""
    return name

# END Websocket handling methods


def typing_bar_item_cb(data, buffer, args):
    typers = [x for x in channels if x.is_someone_typing()]
    if len(typers) > 0:
        direct_typers = []
        channel_typers = []
        for dm in channels.find_by_class(DmChannel):
            direct_typers.extend(dm.get_typing_list())
        direct_typers = ["D/" + x for x in direct_typers]
        current_channel = w.current_buffer()
        channel = channels.find(current_channel)
        try:
            if channel and channel.__class__ != DmChannel:
                channel_typers = channels.find(current_channel).get_typing_list()
        except:
            w.prnt("", "Bug on {}".format(channel))
        typing_here = ", ".join(channel_typers + direct_typers)
        if len(typing_here) > 0:
            color = w.color('yellow')
            return color + "typing: " + typing_here
    return ""


def typing_update_cb(data, remaining_calls):
    w.bar_item_update("slack_typing_notice")
    return w.WEECHAT_RC_OK


def buffer_list_update_cb(data, remaining_calls):
    global buffer_list_update

    now = time.time()
    if buffer_list_update and previous_buffer_list_update + 1 < now:
        # gray_check = False
        # if len(servers) > 1:
        #    gray_check = True
        for channel in channels:
            channel.rename()
        buffer_list_update = False
    return w.WEECHAT_RC_OK


def buffer_list_update_next():
    global buffer_list_update
    buffer_list_update = True


def hotlist_cache_update_cb(data, remaining_calls):
    # this keeps the hotlist dupe up to date for the buffer switch, but is prob technically a race condition. (meh)
    global hotlist
    prev_hotlist = hotlist
    hotlist = w.infolist_get("hotlist", "", "")
    w.infolist_free(prev_hotlist)
    return w.WEECHAT_RC_OK


def buffer_closing_cb(signal, sig_type, data):
    if channels.find(data):
        channels.find(data).closed()
    return w.WEECHAT_RC_OK


def buffer_opened_cb(signal, sig_type, data):
    channels.update_hashtable()
    return w.WEECHAT_RC_OK


def buffer_switch_cb(signal, sig_type, data):
    global previous_buffer, hotlist
    # this is to see if we need to gray out things in the buffer list
    if channels.find(previous_buffer):
        channels.find(previous_buffer).mark_read()

    new_channel = channels.find(data)
    if new_channel:
        if new_channel.got_history == False:
            new_channel.get_history()
    # channel_name = current_buffer_name()
    previous_buffer = data
    return w.WEECHAT_RC_OK


def typing_notification_cb(signal, sig_type, data):
    msg = w.buffer_get_string(data, "input")
    if len(msg) > 8 and msg[:1] != "/":
        global typing_timer
        now = time.time()
        if typing_timer + 4 < now:
            channel = channels.find(current_buffer_name())
            if channel:
                identifier = channel.identifier
                request = {"type": "typing", "channel": identifier}
                channel.server.send_to_websocket(request, expect_reply=False)
                typing_timer = now
    return w.WEECHAT_RC_OK


def slack_ping_cb(data, remaining):
    """
    Periodic websocket ping to detect broken connection.
    """
    servers.find(data).ping()
    return w.WEECHAT_RC_OK


def slack_connection_persistence_cb(data, remaining_calls):
    """
    Reconnect if a connection is detected down
    """
    for server in servers:
        if not server.connected:
            server.buffer_prnt("Disconnected from slack, trying to reconnect..")
            if server.ws_hook is not None:
                w.unhook(server.ws_hook)
            server.connect_to_slack()
    return w.WEECHAT_RC_OK


def slack_never_away_cb(data, remaining):
    global never_away
    if never_away:
        for server in servers:
            identifier = server.channels.find("slackbot").identifier
            request = {"type": "typing", "channel": identifier}
            # request = {"type":"typing","channel":"slackbot"}
            server.send_to_websocket(request, expect_reply=False)
    return w.WEECHAT_RC_OK


def nick_completion_cb(data, completion_item, buffer, completion):
    """
    Adds all @-prefixed nicks to completion list
    """

    channel = channels.find(buffer)
    if channel is None or channel.members is None:
        return w.WEECHAT_RC_OK
    for m in channel.members:
        user = channel.server.users.find(m)
        w.hook_completion_list_add(completion, "@" + user.name, 1, w.WEECHAT_LIST_POS_SORT)
    return w.WEECHAT_RC_OK


def complete_next_cb(data, buffer, command):
    """Extract current word, if it is equal to a nick, prefix it with @ and
    rely on nick_completion_cb adding the @-prefixed versions to the
    completion lists, then let Weechat's internal completion do its
    thing

    """

    channel = channels.find(buffer)
    if channel is None or channel.members is None:
        return w.WEECHAT_RC_OK
    input = w.buffer_get_string(buffer, "input")
    current_pos = w.buffer_get_integer(buffer, "input_pos") - 1
    input_length = w.buffer_get_integer(buffer, "input_length")
    word_start = 0
    word_end = input_length
    # If we're on a non-word, look left for something to complete
    while current_pos >= 0 and input[current_pos] != '@' and not input[current_pos].isalnum():
        current_pos = current_pos - 1
    if current_pos < 0:
        current_pos = 0
    for l in range(current_pos, 0, -1):
        if input[l] != '@' and not input[l].isalnum():
            word_start = l + 1
            break
    for l in range(current_pos, input_length):
        if not input[l].isalnum():
            word_end = l
            break
    word = input[word_start:word_end]
    for m in channel.members:
        user = channel.server.users.find(m)
        if user.name == word:
            # Here, we cheat.  Insert a @ in front and rely in the @
            # nicks being in the completion list
            w.buffer_set(buffer, "input", input[:word_start] + "@" + input[word_start:])
            w.buffer_set(buffer, "input_pos", str(w.buffer_get_integer(buffer, "input_pos") + 1))
            return w.WEECHAT_RC_OK_EAT
    return w.WEECHAT_RC_OK


# Slack specific requests
def async_slack_api_request(domain, token, request, post_data, priority=False):
    if not STOP_TALKING_TO_SLACK:
        post_data["token"] = token
        url = 'url:https://{}/api/{}?{}'.format(domain, request, urllib.urlencode(post_data))
        context = pickle.dumps({"request": request, "token": token, "post_data": post_data})
        params = {'useragent': 'wee_slack {}'.format(SCRIPT_VERSION)}
        dbg("URL: {} context: {} params: {}".format(url, context, params))
        w.hook_process_hashtable(url, params, config.slack_timeout, "url_processor_cb", context)


def async_slack_api_upload_request(token, request, post_data, priority=False):
    if not STOP_TALKING_TO_SLACK:
        url = 'https://slack.com/api/{}'.format(request)
        file_path = os.path.expanduser(post_data["file"])
        if ' ' in file_path:
            file_path = file_path.replace(' ','\ ')
        command = 'curl -F file=@{} -F channels={} -F token={} {}'.format(file_path, post_data["channels"], token, url)
        context = pickle.dumps({"request": request, "token": token, "post_data": post_data})
        w.hook_process(command, config.slack_timeout, "url_processor_cb", context)


# funny, right?
big_data = {}


def url_processor_cb(data, command, return_code, out, err):
    global big_data
    data = pickle.loads(data)
    identifier = sha.sha("{}{}".format(data, command)).hexdigest()
    if identifier not in big_data:
        big_data[identifier] = ''
    big_data[identifier] += out
    if return_code == 0:
        try:
            my_json = json.loads(big_data[identifier])
        except:
            dbg("request failed, doing again...")
            dbg("response length: {} identifier {}\n{}".format(len(big_data[identifier]), identifier, data))
            my_json = False

        big_data.pop(identifier, None)

        if my_json:
            if data["request"] == 'rtm.start':
                servers.find(data["token"]).connected_to_slack(my_json)
                servers.update_hashtable()

            else:
                if "channel" in data["post_data"]:
                    channel = data["post_data"]["channel"]
                token = data["token"]
                if "messages" in my_json:
                    my_json["messages"].reverse()
                    for message in my_json["messages"]:
                        message["_server"] = servers.find(token).domain
                        message["channel"] = servers.find(token).channels.find(channel).identifier
                        process_message(message)
                if "channel" in my_json:
                    if "members" in my_json["channel"]:
                        channels.find(my_json["channel"]["id"]).members = set(my_json["channel"]["members"])
    else:
        if return_code != -1:
            big_data.pop(identifier, None)
        dbg("return code: {}, data: {}, output: {}, error: {}".format(return_code, data, out, err))

    return w.WEECHAT_RC_OK


def cache_write_cb(data, remaining):
    cache_file = open("{}/{}".format(WEECHAT_HOME, CACHE_NAME), 'w')
    cache_file.write(CACHE_VERSION + "\n")
    for channel in channels:
        if channel.active:
            for message in channel.messages:
                cache_file.write("{}\n".format(json.dumps(message.message_json)))
    return w.WEECHAT_RC_OK


def cache_load():
    global message_cache
    try:
        file_name = "{}/{}".format(WEECHAT_HOME, CACHE_NAME)
        cache_file = open(file_name, 'r')
        if cache_file.readline() == CACHE_VERSION + "\n":
            dbg("Loading messages from cache.", main_buffer=True)
            for line in cache_file:
                j = json.loads(line)
                message_cache[j["channel"]].append(line)
            dbg("Completed loading messages from cache.", main_buffer=True)
    except ValueError:
        w.prnt("", "Failed to load cache file, probably illegal JSON.. Ignoring")
        pass
    except IOError:
        w.prnt("", "cache file not found")
        pass

# END Slack specific requests

# Utility Methods


def current_domain_name():
    buffer = w.current_buffer()
    if servers.find(buffer):
        return servers.find(buffer).domain
    else:
        # number = w.buffer_get_integer(buffer, "number")
        name = w.buffer_get_string(buffer, "name")
        name = ".".join(name.split(".")[:-1])
        return name


def current_buffer_name(short=False):
    buffer = w.current_buffer()
    # number = w.buffer_get_integer(buffer, "number")
    name = w.buffer_get_string(buffer, "name")
    if short:
        try:
            name = name.split('.')[-1]
        except:
            pass
    return name


def closed_slack_buffer_cb(data, buffer):
    global slack_buffer
    slack_buffer = None
    return w.WEECHAT_RC_OK


def create_slack_buffer():
    global slack_buffer
    slack_buffer = w.buffer_new("slack", "", "", "closed_slack_buffer_cb", "")
    w.buffer_set(slack_buffer, "notify", "0")
    # w.buffer_set(slack_buffer, "display", "1")
    return w.WEECHAT_RC_OK


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")


def quit_notification_cb(signal, sig_type, data):
    stop_talking_to_slack()


def script_unloaded():
    stop_talking_to_slack()
    return w.WEECHAT_RC_OK


def stop_talking_to_slack():
    """
    Prevents a race condition where quitting closes buffers
    which triggers leaving the channel because of how close
    buffer is handled
    """
    global STOP_TALKING_TO_SLACK
    STOP_TALKING_TO_SLACK = True
    cache_write_cb("", "")
    return w.WEECHAT_RC_OK


def scrolled_cb(signal, sig_type, data):
    try:
        if w.window_get_integer(data, "scrolling") == 1:
            channels.find(w.current_buffer()).set_scrolling()
        else:
            channels.find(w.current_buffer()).unset_scrolling()
    except:
        pass
    return w.WEECHAT_RC_OK

# END Utility Methods

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',
    }

    # 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

            cmds = {k[8:]: v for k, v in globals().items() if k.startswith("command_")}
            proc = {k[8:]: v for k, v in globals().items() if k.startswith("process_")}

            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)
            cache_load()

            servers = SearchList()
            for token in config.slack_api_token.split(','):
                server = SlackServer(token)
                servers.append(server)
            channels = SearchList()
            users = 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, 0, 0, "hotlist_cache_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_cb", "")
            w.hook_signal('buffer_opened', "buffer_opened_cb", "")
            w.hook_signal('buffer_switch', "buffer_switch_cb", "")
            w.hook_signal('window_switch', "buffer_switch_cb", "")
            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("/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', '')
            # END attach to the weechat hooks we need