about summary refs log tree commit diff
path: root/weechat/.weechat/python
diff options
context:
space:
mode:
authorBen Harris <ben@tilde.team>2020-04-27 20:42:35 -0400
committerBen Harris <ben@tilde.team>2020-04-27 20:42:35 -0400
commitb1bae56eea38e176ba5225265b84a3cc9d256da3 (patch)
tree78e9f602130e24c0af0703966e3221771dcddc8c /weechat/.weechat/python
parent2fd57e8772db6c2775757118bf9e521839631cdd (diff)
update weechats and vim submodules
Diffstat (limited to 'weechat/.weechat/python')
-rw-r--r--weechat/.weechat/python/wee_slack.py5041
1 files changed, 5041 insertions, 0 deletions
diff --git a/weechat/.weechat/python/wee_slack.py b/weechat/.weechat/python/wee_slack.py
new file mode 100644
index 0000000..05c4a4d
--- /dev/null
+++ b/weechat/.weechat/python/wee_slack.py
@@ -0,0 +1,5041 @@
+# Copyright (c) 2014-2016 Ryan Huber <rhuber@gmail.com>
+# Copyright (c) 2015-2018 Tollef Fog Heen <tfheen@err.no>
+# Copyright (c) 2015-2020 Trygve Aaberge <trygveaa@gmail.com>
+# Released under the MIT license.
+
+from __future__ import print_function, unicode_literals
+
+from collections import OrderedDict
+from datetime import date, datetime, timedelta
+from functools import partial, wraps
+from io import StringIO
+from itertools import chain, count, islice
+
+import errno
+import textwrap
+import time
+import json
+import hashlib
+import os
+import re
+import sys
+import traceback
+import collections
+import ssl
+import random
+import socket
+import string
+
+# Prevent websocket from using numpy (it's an optional dependency). We do this
+# because numpy causes python (and thus weechat) to crash when it's reloaded.
+# See https://github.com/numpy/numpy/issues/11925
+sys.modules["numpy"] = None
+
+from websocket import ABNF, create_connection, WebSocketConnectionClosedException
+
+try:
+    basestring     # Python 2
+    unicode
+    str = unicode
+except NameError:  # Python 3
+    basestring = unicode = str
+
+try:
+    from urllib.parse import urlencode
+except ImportError:
+    from urllib import urlencode
+
+try:
+    from json import JSONDecodeError
+except:
+    JSONDecodeError = ValueError
+
+# hack to make tests possible.. better way?
+try:
+    import weechat
+except ImportError:
+    pass
+
+SCRIPT_NAME = "slack"
+SCRIPT_AUTHOR = "Ryan Huber <rhuber@gmail.com>"
+SCRIPT_VERSION = "2.4.0"
+SCRIPT_LICENSE = "MIT"
+SCRIPT_DESC = "Extends weechat for typing notification/search/etc on slack.com"
+REPO_URL = "https://github.com/wee-slack/wee-slack"
+
+BACKLOG_SIZE = 200
+SCROLLBACK_SIZE = 500
+
+RECORD_DIR = "/tmp/weeslack-debug"
+
+SLACK_API_TRANSLATOR = {
+    "channel": {
+        "history": "channels.history",
+        "join": "conversations.join",
+        "leave": "conversations.leave",
+        "mark": "channels.mark",
+        "info": "channels.info",
+    },
+    "im": {
+        "history": "im.history",
+        "join": "conversations.open",
+        "leave": "conversations.close",
+        "mark": "im.mark",
+    },
+    "mpim": {
+        "history": "mpim.history",
+        "join": "mpim.open",  # conversations.open lacks unread_count_display
+        "leave": "conversations.close",
+        "mark": "mpim.mark",
+        "info": "groups.info",
+    },
+    "group": {
+        "history": "groups.history",
+        "join": "conversations.join",
+        "leave": "conversations.leave",
+        "mark": "groups.mark",
+        "info": "groups.info"
+    },
+    "private": {
+        "history": "conversations.history",
+        "join": "conversations.join",
+        "leave": "conversations.leave",
+        "mark": "conversations.mark",
+        "info": "conversations.info",
+    },
+    "shared": {
+        "history": "conversations.history",
+        "join": "conversations.join",
+        "leave": "conversations.leave",
+        "mark": "channels.mark",
+        "info": "conversations.info",
+    },
+    "thread": {
+        "history": None,
+        "join": None,
+        "leave": None,
+        "mark": None,
+    }
+
+
+}
+
+###### Decorators have to be up here
+
+
+def slack_buffer_or_ignore(f):
+    """
+    Only run this function if we're in a slack buffer, else ignore
+    """
+    @wraps(f)
+    def wrapper(data, current_buffer, *args, **kwargs):
+        if current_buffer not in EVENTROUTER.weechat_controller.buffers:
+            return w.WEECHAT_RC_OK
+        return f(data, current_buffer, *args, **kwargs)
+    return wrapper
+
+
+def slack_buffer_required(f):
+    """
+    Only run this function if we're in a slack buffer, else print error
+    """
+    @wraps(f)
+    def wrapper(data, current_buffer, *args, **kwargs):
+        if current_buffer not in EVENTROUTER.weechat_controller.buffers:
+            command_name = f.__name__.replace('command_', '', 1)
+            w.prnt('', 'slack: command "{}" must be executed on slack buffer'.format(command_name))
+            return w.WEECHAT_RC_ERROR
+        return f(data, current_buffer, *args, **kwargs)
+    return wrapper
+
+
+def utf8_decode(f):
+    """
+    Decode all arguments from byte strings to unicode strings. Use this for
+    functions called from outside of this script, e.g. callbacks from weechat.
+    """
+    @wraps(f)
+    def wrapper(*args, **kwargs):
+        return f(*decode_from_utf8(args), **decode_from_utf8(kwargs))
+    return wrapper
+
+
+NICK_GROUP_HERE = "0|Here"
+NICK_GROUP_AWAY = "1|Away"
+NICK_GROUP_EXTERNAL = "2|External"
+
+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}
+
+EMOJI = {}
+EMOJI_WITH_SKIN_TONES_REVERSE = {}
+
+###### Unicode handling
+
+
+def encode_to_utf8(data):
+    if sys.version_info.major > 2:
+        return data
+    elif isinstance(data, unicode):
+        return data.encode('utf-8')
+    if isinstance(data, bytes):
+        return data
+    elif isinstance(data, collections.Mapping):
+        return type(data)(map(encode_to_utf8, data.items()))
+    elif isinstance(data, collections.Iterable):
+        return type(data)(map(encode_to_utf8, data))
+    else:
+        return data
+
+
+def decode_from_utf8(data):
+    if sys.version_info.major > 2:
+        return data
+    elif isinstance(data, bytes):
+        return data.decode('utf-8')
+    if isinstance(data, unicode):
+        return data
+    elif isinstance(data, collections.Mapping):
+        return type(data)(map(decode_from_utf8, data.items()))
+    elif isinstance(data, collections.Iterable):
+        return type(data)(map(decode_from_utf8, data))
+    else:
+        return data
+
+
+class WeechatWrapper(object):
+    def __init__(self, wrapped_class):
+        self.wrapped_class = wrapped_class
+
+    # Helper method used to encode/decode method calls.
+    def wrap_for_utf8(self, method):
+        def hooked(*args, **kwargs):
+            result = method(*encode_to_utf8(args), **encode_to_utf8(kwargs))
+            # Prevent wrapped_class from becoming unwrapped
+            if result == self.wrapped_class:
+                return self
+            return decode_from_utf8(result)
+        return hooked
+
+    # Encode and decode everything sent to/received from weechat. We use the
+    # unicode type internally in wee-slack, but has to send utf8 to weechat.
+    def __getattr__(self, attr):
+        orig_attr = self.wrapped_class.__getattribute__(attr)
+        if callable(orig_attr):
+            return self.wrap_for_utf8(orig_attr)
+        else:
+            return decode_from_utf8(orig_attr)
+
+    # Ensure all lines sent to weechat specifies a prefix. For lines after the
+    # first, we want to disable the prefix, which is done by specifying a space.
+    def prnt_date_tags(self, buffer, date, tags, message):
+        message = message.replace("\n", "\n \t")
+        return self.wrap_for_utf8(self.wrapped_class.prnt_date_tags)(buffer, date, tags, message)
+
+
+class ProxyWrapper(object):
+    def __init__(self):
+        self.proxy_name = w.config_string(w.config_get('weechat.network.proxy_curl'))
+        self.proxy_string = ""
+        self.proxy_type = ""
+        self.proxy_address = ""
+        self.proxy_port = ""
+        self.proxy_user = ""
+        self.proxy_password = ""
+        self.has_proxy = False
+
+        if self.proxy_name:
+            self.proxy_string = "weechat.proxy.{}".format(self.proxy_name)
+            self.proxy_type = w.config_string(w.config_get("{}.type".format(self.proxy_string)))
+            if self.proxy_type == "http":
+                self.proxy_address = w.config_string(w.config_get("{}.address".format(self.proxy_string)))
+                self.proxy_port = w.config_integer(w.config_get("{}.port".format(self.proxy_string)))
+                self.proxy_user = w.config_string(w.config_get("{}.username".format(self.proxy_string)))
+                self.proxy_password = w.config_string(w.config_get("{}.password".format(self.proxy_string)))
+                self.has_proxy = True
+            else:
+                w.prnt("", "\nWarning: weechat.network.proxy_curl is set to {} type (name : {}, conf string : {}). Only HTTP proxy is supported.\n\n".format(self.proxy_type, self.proxy_name, self.proxy_string))
+
+    def curl(self):
+        if not self.has_proxy:
+            return ""
+
+        if self.proxy_user and self.proxy_password:
+            user = "{}:{}@".format(self.proxy_user, self.proxy_password)
+        else:
+            user = ""
+
+        if self.proxy_port:
+            port = ":{}".format(self.proxy_port)
+        else:
+            port = ""
+
+        return "-x{}{}{}".format(user, self.proxy_address, port)
+
+
+##### Helpers
+
+
+def colorize_string(color, string, reset_color='reset'):
+    if color:
+        return w.color(color) + string + w.color(reset_color)
+    else:
+        return string
+
+
+def print_error(message, buffer=''):
+    w.prnt(buffer, '{}Error: {}'.format(w.prefix('error'), message))
+
+
+def format_exc_tb():
+    return decode_from_utf8(traceback.format_exc())
+
+
+def format_exc_only():
+    etype, value, _ = sys.exc_info()
+    return ''.join(decode_from_utf8(traceback.format_exception_only(etype, value)))
+
+
+def get_nick_color(nick):
+    info_name_prefix = "irc_" if int(weechat_version) < 0x1050000 else ""
+    return w.info_get(info_name_prefix + "nick_color_name", nick)
+
+
+def get_thread_color(thread_id):
+    if config.color_thread_suffix == 'multiple':
+        return get_nick_color(thread_id)
+    else:
+        return config.color_thread_suffix
+
+
+def sha1_hex(s):
+    return hashlib.sha1(s.encode('utf-8')).hexdigest()
+
+
+def get_functions_with_prefix(prefix):
+    return {name[len(prefix):]: ref for name, ref in globals().items()
+            if name.startswith(prefix)}
+
+
+def handle_socket_error(exception, team, caller_name):
+    if not (isinstance(exception, WebSocketConnectionClosedException) or
+            exception.errno in (errno.EPIPE, errno.ECONNRESET, errno.ETIMEDOUT)):
+        raise
+
+    w.prnt(team.channel_buffer,
+            'Lost connection to slack team {} (on {}), reconnecting.'.format(
+                team.domain, caller_name))
+    dbg('Socket failed on {} with exception:\n{}'.format(
+        caller_name, format_exc_tb()), level=5)
+    team.set_disconnected()
+
+
+EMOJI_NAME_REGEX = re.compile(':([^: ]+):')
+EMOJI_REGEX_STRING = '[\U00000080-\U0010ffff]+'
+
+
+def regex_match_to_emoji(match, include_name=False):
+    emoji = match.group(1)
+    full_match = match.group()
+    char = EMOJI.get(emoji, full_match)
+    if include_name and char != full_match:
+        return '{} ({})'.format(char, full_match)
+    return char
+
+
+def replace_string_with_emoji(text):
+    if config.render_emoji_as_string == 'both':
+        return EMOJI_NAME_REGEX.sub(
+            partial(regex_match_to_emoji, include_name=True),
+            text,
+        )
+    elif config.render_emoji_as_string:
+        return text
+    return EMOJI_NAME_REGEX.sub(regex_match_to_emoji, text)
+
+
+def replace_emoji_with_string(text):
+    return EMOJI_WITH_SKIN_TONES_REVERSE.get(text, text)
+
+
+###### New central Event router
+
+class EventRouter(object):
+
+    def __init__(self):
+        """
+        complete
+        Eventrouter is the central hub we use to route:
+        1) incoming websocket data
+        2) outgoing http requests and incoming replies
+        3) local requests
+        It has a recorder that, when enabled, logs most events
+        to the location specified in RECORD_DIR.
+        """
+        self.queue = []
+        self.slow_queue = []
+        self.slow_queue_timer = 0
+        self.teams = {}
+        self.subteams = {}
+        self.context = {}
+        self.weechat_controller = WeechatController(self)
+        self.previous_buffer = ""
+        self.reply_buffer = {}
+        self.cmds = get_functions_with_prefix("command_")
+        self.proc = get_functions_with_prefix("process_")
+        self.handlers = get_functions_with_prefix("handle_")
+        self.local_proc = get_functions_with_prefix("local_process_")
+        self.shutting_down = False
+        self.recording = False
+        self.recording_path = "/tmp"
+        self.handle_next_hook = None
+        self.handle_next_hook_interval = -1
+
+    def record(self):
+        """
+        complete
+        Toggles the event recorder and creates a directory for data if enabled.
+        """
+        self.recording = not self.recording
+        if self.recording:
+            if not os.path.exists(RECORD_DIR):
+                os.makedirs(RECORD_DIR)
+
+    def record_event(self, message_json, file_name_field, subdir=None):
+        """
+        complete
+        Called each time you want to record an event.
+        message_json is a json in dict form
+        file_name_field is the json key whose value you want to be part of the file name
+        """
+        now = time.time()
+        if subdir:
+            directory = "{}/{}".format(RECORD_DIR, subdir)
+        else:
+            directory = RECORD_DIR
+        if not os.path.exists(directory):
+            os.makedirs(directory)
+        mtype = message_json.get(file_name_field, 'unknown')
+        f = open('{}/{}-{}.json'.format(directory, now, mtype), 'w')
+        f.write("{}".format(json.dumps(message_json)))
+        f.close()
+
+    def store_context(self, data):
+        """
+        A place to store data and vars needed by callback returns. We need this because
+        weechat's "callback_data" has a limited size and weechat will crash if you exceed
+        this size.
+        """
+        identifier = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(40))
+        self.context[identifier] = data
+        dbg("stored context {} {} ".format(identifier, data.url))
+        return identifier
+
+    def retrieve_context(self, identifier):
+        """
+        A place to retrieve data and vars needed by callback returns. We need this because
+        weechat's "callback_data" has a limited size and weechat will crash if you exceed
+        this size.
+        """
+        return self.context.get(identifier)
+
+    def delete_context(self, identifier):
+        """
+        Requests can span multiple requests, so we may need to delete this as a last step
+        """
+        if identifier in self.context:
+            del self.context[identifier]
+
+    def shutdown(self):
+        """
+        complete
+        This toggles shutdown mode. Shutdown mode tells us not to
+        talk to Slack anymore. Without this, typing /quit will trigger
+        a race with the buffer close callback and may result in you
+        leaving every slack channel.
+        """
+        self.shutting_down = not self.shutting_down
+
+    def register_team(self, team):
+        """
+        complete
+        Adds a team to the list of known teams for this EventRouter.
+        """
+        if isinstance(team, SlackTeam):
+            self.teams[team.get_team_hash()] = team
+        else:
+            raise InvalidType(type(team))
+
+    def reconnect_if_disconnected(self):
+        for team in self.teams.values():
+            time_since_last_ping = time.time() - team.last_ping_time
+            time_since_last_pong = time.time() - team.last_pong_time
+            if team.connected and time_since_last_ping < 5 and time_since_last_pong > 30:
+                w.prnt(team.channel_buffer,
+                        'Lost connection to slack team {} (no pong), reconnecting.'.format(
+                            team.domain))
+                team.set_disconnected()
+            if not team.connected:
+                team.connect()
+                dbg("reconnecting {}".format(team))
+
+    @utf8_decode
+    def receive_ws_callback(self, team_hash, fd):
+        """
+        This is called by the global method of the same name.
+        It is triggered when we have incoming data on a websocket,
+        which needs to be read. Once it is read, we will ensure
+        the data is valid JSON, add metadata, and place it back
+        on the queue for processing as JSON.
+        """
+        team = self.teams[team_hash]
+        while True:
+            try:
+                # Read the data from the websocket associated with this team.
+                opcode, data = team.ws.recv_data(control_frame=True)
+            except ssl.SSLWantReadError:
+                # No more data to read at this time.
+                return w.WEECHAT_RC_OK
+            except (WebSocketConnectionClosedException, socket.error) as e:
+                handle_socket_error(e, team, 'receive')
+                return w.WEECHAT_RC_OK
+
+            if opcode == ABNF.OPCODE_PONG:
+                team.last_pong_time = time.time()
+                return w.WEECHAT_RC_OK
+            elif opcode != ABNF.OPCODE_TEXT:
+                return w.WEECHAT_RC_OK
+
+            message_json = json.loads(data.decode('utf-8'))
+            message_json["wee_slack_metadata_team"] = team
+            if self.recording:
+                self.record_event(message_json, 'type', 'websocket')
+            self.receive(message_json)
+        return w.WEECHAT_RC_OK
+
+    @utf8_decode
+    def receive_httprequest_callback(self, data, command, return_code, out, err):
+        """
+        complete
+        Receives the result of an http request we previously handed
+        off to weechat (weechat bundles libcurl). Weechat can fragment
+        replies, so it buffers them until the reply is complete.
+        It is then populated with metadata here so we can identify
+        where the request originated and route properly.
+        """
+        request_metadata = self.retrieve_context(data)
+        dbg("RECEIVED CALLBACK with request of {} id of {} and  code {} of length {}".format(request_metadata.request, request_metadata.response_id, return_code, len(out)))
+        if return_code == 0:
+            if len(out) > 0:
+                if request_metadata.response_id not in self.reply_buffer:
+                    self.reply_buffer[request_metadata.response_id] = StringIO()
+                self.reply_buffer[request_metadata.response_id].write(out)
+                try:
+                    j = json.loads(self.reply_buffer[request_metadata.response_id].getvalue())
+                except:
+                    pass
+                    # dbg("Incomplete json, awaiting more", True)
+                try:
+                    j["wee_slack_process_method"] = request_metadata.request_normalized
+                    if self.recording:
+                        self.record_event(j, 'wee_slack_process_method', 'http')
+                    j["wee_slack_request_metadata"] = request_metadata
+                    self.reply_buffer.pop(request_metadata.response_id)
+                    self.receive(j)
+                    self.delete_context(data)
+                except:
+                    dbg("HTTP REQUEST CALLBACK FAILED", True)
+                    pass
+            # We got an empty reply and this is weird so just ditch it and retry
+            else:
+                dbg("length was zero, probably a bug..")
+                self.delete_context(data)
+                self.receive(request_metadata)
+        elif return_code == -1:
+            if request_metadata.response_id not in self.reply_buffer:
+                self.reply_buffer[request_metadata.response_id] = StringIO()
+            self.reply_buffer[request_metadata.response_id].write(out)
+        else:
+            self.reply_buffer.pop(request_metadata.response_id, None)
+            self.delete_context(data)
+            if request_metadata.request.startswith('rtm.'):
+                retry_text = ('retrying' if request_metadata.should_try() else
+                        'will not retry after too many failed attempts')
+                w.prnt('', ('Failed connecting to slack team with token starting with {}, {}. ' +
+                        'If this persists, try increasing slack_timeout. Error: {}')
+                        .format(request_metadata.token[:15], retry_text, err))
+                dbg('rtm.start failed with return_code {}. stack:\n{}'
+                        .format(return_code, ''.join(traceback.format_stack())), level=5)
+                self.receive(request_metadata)
+        return w.WEECHAT_RC_OK
+
+    def receive(self, dataobj):
+        """
+        complete
+        Receives a raw object and places it on the queue for
+        processing. Object must be known to handle_next or
+        be JSON.
+        """
+        dbg("RECEIVED FROM QUEUE")
+        self.queue.append(dataobj)
+
+    def receive_slow(self, dataobj):
+        """
+        complete
+        Receives a raw object and places it on the slow queue for
+        processing. Object must be known to handle_next or
+        be JSON.
+        """
+        dbg("RECEIVED FROM QUEUE")
+        self.slow_queue.append(dataobj)
+
+    def handle_next(self):
+        """
+        complete
+        Main handler of the EventRouter. This is called repeatedly
+        via callback to drain events from the queue. It also attaches
+        useful metadata and context to events as they are processed.
+        """
+        wanted_interval = 100
+        if len(self.slow_queue) > 0 or len(self.queue) > 0:
+            wanted_interval = 10
+        if self.handle_next_hook is None or wanted_interval != self.handle_next_hook_interval:
+            if self.handle_next_hook:
+                w.unhook(self.handle_next_hook)
+            self.handle_next_hook = w.hook_timer(wanted_interval, 0, 0, "handle_next", "")
+            self.handle_next_hook_interval = wanted_interval
+
+
+        if len(self.slow_queue) > 0 and ((self.slow_queue_timer + 1) < time.time()):
+            dbg("from slow queue", 0)
+            self.queue.append(self.slow_queue.pop())
+            self.slow_queue_timer = time.time()
+        if len(self.queue) > 0:
+            j = self.queue.pop(0)
+            # Reply is a special case of a json reply from websocket.
+            kwargs = {}
+            if isinstance(j, SlackRequest):
+                if j.should_try():
+                    if j.retry_ready():
+                        local_process_async_slack_api_request(j, self)
+                    else:
+                        self.slow_queue.append(j)
+                else:
+                    dbg("Max retries for Slackrequest")
+
+            else:
+
+                if "reply_to" in j:
+                    dbg("SET FROM REPLY")
+                    function_name = "reply"
+                elif "type" in j:
+                    dbg("SET FROM type")
+                    function_name = j["type"]
+                elif "wee_slack_process_method" in j:
+                    dbg("SET FROM META")
+                    function_name = j["wee_slack_process_method"]
+                else:
+                    dbg("SET FROM NADA")
+                    function_name = "unknown"
+
+                request = j.get("wee_slack_request_metadata")
+                if request:
+                    team = request.team
+                    channel = request.channel
+                    metadata = request.metadata
+                else:
+                    team = j.get("wee_slack_metadata_team")
+                    channel = None
+                    metadata = {}
+
+                if team:
+                    if "channel" in j:
+                        channel_id = j["channel"]["id"] if type(j["channel"]) == dict else j["channel"]
+                        channel = team.channels.get(channel_id, channel)
+                    if "user" in j:
+                        user_id = j["user"]["id"] if type(j["user"]) == dict else j["user"]
+                        metadata['user'] = team.users.get(user_id)
+
+                dbg("running {}".format(function_name))
+                if function_name.startswith("local_") and function_name in self.local_proc:
+                    self.local_proc[function_name](j, self, team, channel, metadata)
+                elif function_name in self.proc:
+                    self.proc[function_name](j, self, team, channel, metadata)
+                elif function_name in self.handlers:
+                    self.handlers[function_name](j, self, team, channel, metadata)
+                else:
+                    dbg("Callback not implemented for event: {}".format(function_name))
+
+
+def handle_next(data, remaining_calls):
+    try:
+        EVENTROUTER.handle_next()
+    except:
+        if config.debug_mode:
+            traceback.print_exc()
+        else:
+            pass
+    return w.WEECHAT_RC_OK
+
+
+class WeechatController(object):
+    """
+    Encapsulates our interaction with weechat
+    """
+
+    def __init__(self, eventrouter):
+        self.eventrouter = eventrouter
+        self.buffers = {}
+        self.previous_buffer = None
+        self.buffer_list_stale = False
+
+    def iter_buffers(self):
+        for b in self.buffers:
+            yield (b, self.buffers[b])
+
+    def register_buffer(self, buffer_ptr, channel):
+        """
+        complete
+        Adds a weechat buffer to the list of handled buffers for this EventRouter
+        """
+        if isinstance(buffer_ptr, basestring):
+            self.buffers[buffer_ptr] = channel
+        else:
+            raise InvalidType(type(buffer_ptr))
+
+    def unregister_buffer(self, buffer_ptr, update_remote=False, close_buffer=False):
+        """
+        complete
+        Adds a weechat buffer to the list of handled buffers for this EventRouter
+        """
+        channel = self.buffers.get(buffer_ptr)
+        if channel:
+            channel.destroy_buffer(update_remote)
+            del self.buffers[buffer_ptr]
+            if close_buffer:
+                w.buffer_close(buffer_ptr)
+
+    def get_channel_from_buffer_ptr(self, buffer_ptr):
+        return self.buffers.get(buffer_ptr)
+
+    def get_all(self, buffer_ptr):
+        return self.buffers
+
+    def get_previous_buffer_ptr(self):
+        return self.previous_buffer
+
+    def set_previous_buffer(self, data):
+        self.previous_buffer = data
+
+    def check_refresh_buffer_list(self):
+        return self.buffer_list_stale and self.last_buffer_list_update + 1 < time.time()
+
+    def set_refresh_buffer_list(self, setting):
+        self.buffer_list_stale = setting
+
+###### New Local Processors
+
+
+def local_process_async_slack_api_request(request, event_router):
+    """
+    complete
+    Sends an API request to Slack. You'll need to give this a well formed SlackRequest object.
+    DEBUGGING!!! The context here cannot be very large. Weechat will crash.
+    """
+    if not event_router.shutting_down:
+        weechat_request = 'url:{}'.format(request.request_string())
+        weechat_request += '&nonce={}'.format(''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(4)))
+        params = {'useragent': 'wee_slack {}'.format(SCRIPT_VERSION)}
+        request.tried()
+        context = event_router.store_context(request)
+        # TODO: let flashcode know about this bug - i have to 'clear' the hashtable or retry requests fail
+        w.hook_process_hashtable('url:', params, config.slack_timeout, "", context)
+        w.hook_process_hashtable(weechat_request, params, config.slack_timeout, "receive_httprequest_callback", context)
+
+###### New Callbacks
+
+
+@utf8_decode
+def ws_ping_cb(data, remaining_calls):
+    for team in EVENTROUTER.teams.values():
+        if team.ws and team.connected:
+            try:
+                team.ws.ping()
+                team.last_ping_time = time.time()
+            except (WebSocketConnectionClosedException, socket.error) as e:
+                handle_socket_error(e, team, 'ping')
+    return w.WEECHAT_RC_OK
+
+
+@utf8_decode
+def reconnect_callback(*args):
+    EVENTROUTER.reconnect_if_disconnected()
+    return w.WEECHAT_RC_OK
+
+
+@utf8_decode
+def buffer_closing_callback(signal, sig_type, data):
+    """
+    Receives a callback from weechat when a buffer is being closed.
+    """
+    EVENTROUTER.weechat_controller.unregister_buffer(data, True, False)
+    return w.WEECHAT_RC_OK
+
+
+@utf8_decode
+def buffer_input_callback(signal, buffer_ptr, data):
+    """
+    incomplete
+    Handles everything a user types in the input bar. In our case
+    this includes add/remove reactions, modifying messages, and
+    sending messages.
+    """
+    eventrouter = eval(signal)
+    channel = eventrouter.weechat_controller.get_channel_from_buffer_ptr(buffer_ptr)
+    if not channel:
+        return w.WEECHAT_RC_ERROR
+
+    def get_id(message_id):
+        if not message_id:
+            return 1
+        elif message_id[0] == "$":
+            return message_id[1:]
+        else:
+            return int(message_id)
+
+    message_id_regex = r"(\d*|\$[0-9a-fA-F]{3,})"
+    reaction = re.match(r"^{}(\+|-)(:(.+):|{})\s*$".format(message_id_regex, EMOJI_REGEX_STRING), data)
+    substitute = re.match("^{}s/".format(message_id_regex), data)
+    if reaction:
+        emoji_match = reaction.group(4) or reaction.group(3)
+        emoji = replace_emoji_with_string(emoji_match)
+        if reaction.group(2) == "+":
+            channel.send_add_reaction(get_id(reaction.group(1)), emoji)
+        elif reaction.group(2) == "-":
+            channel.send_remove_reaction(get_id(reaction.group(1)), emoji)
+    elif substitute:
+        msg_id = get_id(substitute.group(1))
+        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.edit_nth_previous_message(msg_id, old, new, flags)
+    else:
+        if data.startswith(('//', ' ')):
+            data = data[1:]
+        channel.send_message(data)
+        # this is probably wrong channel.mark_read(update_remote=True, force=True)
+    return w.WEECHAT_RC_OK
+
+
+# Workaround for supporting multiline messages. It intercepts before the input
+# callback is called, as this is called with the whole message, while it is
+# normally split on newline before being sent to buffer_input_callback
+def input_text_for_buffer_cb(data, modifier, current_buffer, string):
+    if current_buffer not in EVENTROUTER.weechat_controller.buffers:
+        return string
+    message = decode_from_utf8(string)
+    if not message.startswith("/") and "\n" in message:
+        buffer_input_callback("EVENTROUTER", current_buffer, message)
+        return ""
+    return string
+
+
+@utf8_decode
+def buffer_switch_callback(signal, sig_type, data):
+    """
+    Every time we change channels in weechat, we call this to:
+    1) set read marker 2) determine if we have already populated
+    channel history data 3) set presence to active
+    """
+    eventrouter = eval(signal)
+
+    prev_buffer_ptr = eventrouter.weechat_controller.get_previous_buffer_ptr()
+    # this is to see if we need to gray out things in the buffer list
+    prev = eventrouter.weechat_controller.get_channel_from_buffer_ptr(prev_buffer_ptr)
+    if prev:
+        prev.mark_read()
+
+    new_channel = eventrouter.weechat_controller.get_channel_from_buffer_ptr(data)
+    if new_channel:
+        if not new_channel.got_history:
+            new_channel.get_history()
+        set_own_presence_active(new_channel.team)
+
+    eventrouter.weechat_controller.set_previous_buffer(data)
+    return w.WEECHAT_RC_OK
+
+
+@utf8_decode
+def buffer_list_update_callback(data, somecount):
+    """
+    incomplete
+    A simple timer-based callback that will update the buffer list
+    if needed. We only do this max 1x per second, as otherwise it
+    uses a lot of cpu for minimal changes. We use buffer short names
+    to indicate typing via "#channel" <-> ">channel" and
+    user presence via " name" <-> "+name".
+    """
+    eventrouter = eval(data)
+
+    for b in eventrouter.weechat_controller.iter_buffers():
+        b[1].refresh()
+#    buffer_list_update = True
+#    if eventrouter.weechat_controller.check_refresh_buffer_list():
+#        # gray_check = False
+#        # if len(servers) > 1:
+#        #    gray_check = True
+#        eventrouter.weechat_controller.set_refresh_buffer_list(False)
+    return w.WEECHAT_RC_OK
+
+
+def quit_notification_callback(signal, sig_type, data):
+    stop_talking_to_slack()
+    return w.WEECHAT_RC_OK
+
+
+@utf8_decode
+def typing_notification_cb(data, signal, current_buffer):
+    msg = w.buffer_get_string(current_buffer, "input")
+    if len(msg) > 8 and msg[0] != "/":
+        global typing_timer
+        now = time.time()
+        if typing_timer + 4 < now:
+            channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer)
+            if channel and channel.type != "thread":
+                identifier = channel.identifier
+                request = {"type": "typing", "channel": identifier}
+                channel.team.send_to_websocket(request, expect_reply=False)
+                typing_timer = now
+    return w.WEECHAT_RC_OK
+
+
+@utf8_decode
+def typing_update_cb(data, remaining_calls):
+    w.bar_item_update("slack_typing_notice")
+    return w.WEECHAT_RC_OK
+
+
+@utf8_decode
+def slack_never_away_cb(data, remaining_calls):
+    if config.never_away:
+        for team in EVENTROUTER.teams.values():
+            set_own_presence_active(team)
+    return w.WEECHAT_RC_OK
+
+
+@utf8_decode
+def typing_bar_item_cb(data, item, current_window, current_buffer, extra_info):
+    """
+    Privides a bar item indicating who is typing in the current channel AND
+    why is typing a DM to you globally.
+    """
+    typers = []
+    current_channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer)
+
+    # first look for people typing in this channel
+    if current_channel:
+        # this try is mostly becuase server buffers don't implement is_someone_typing
+        try:
+            if current_channel.type != 'im' and current_channel.is_someone_typing():
+                typers += current_channel.get_typing_list()
+        except:
+            pass
+
+    # here is where we notify you that someone is typing in DM
+    # regardless of which buffer you are in currently
+    for team in EVENTROUTER.teams.values():
+        for channel in team.channels.values():
+            if channel.type == "im":
+                if channel.is_someone_typing():
+                    typers.append("D/" + channel.slack_name)
+                pass
+
+    typing = ", ".join(typers)
+    if typing != "":
+        typing = colorize_string(config.color_typing_notice, "typing: " + typing)
+
+    return typing
+
+
+@utf8_decode
+def away_bar_item_cb(data, item, current_window, current_buffer, extra_info):
+    channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer)
+    if not channel:
+        return ''
+
+    if channel.team.is_user_present(channel.team.myidentifier):
+        return ''
+    else:
+        away_color = w.config_string(w.config_get('weechat.color.item_away'))
+        if channel.team.my_manual_presence == 'away':
+            return colorize_string(away_color, 'manual away')
+        else:
+            return colorize_string(away_color, 'auto away')
+
+
+@utf8_decode
+def channel_completion_cb(data, completion_item, current_buffer, completion):
+    """
+    Adds all channels on all teams to completion list
+    """
+    current_channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer)
+    should_include_channel = lambda channel: channel.active and channel.type in ['channel', 'group', 'private', 'shared']
+
+    other_teams = [team for team in EVENTROUTER.teams.values() if not current_channel or team != current_channel.team]
+    for team in other_teams:
+        for channel in team.channels.values():
+            if should_include_channel(channel):
+                w.hook_completion_list_add(completion, channel.name, 0, w.WEECHAT_LIST_POS_SORT)
+
+    if current_channel:
+        for channel in sorted(current_channel.team.channels.values(), key=lambda channel: channel.name, reverse=True):
+            if should_include_channel(channel):
+                w.hook_completion_list_add(completion, channel.name, 0, w.WEECHAT_LIST_POS_BEGINNING)
+
+        if should_include_channel(current_channel):
+            w.hook_completion_list_add(completion, current_channel.name, 0, w.WEECHAT_LIST_POS_BEGINNING)
+    return w.WEECHAT_RC_OK
+
+
+@utf8_decode
+def dm_completion_cb(data, completion_item, current_buffer, completion):
+    """
+    Adds all dms/mpdms on all teams to completion list
+    """
+    for team in EVENTROUTER.teams.values():
+        for channel in team.channels.values():
+            if channel.active and channel.type in ['im', 'mpim']:
+                w.hook_completion_list_add(completion, channel.name, 0, w.WEECHAT_LIST_POS_SORT)
+    return w.WEECHAT_RC_OK
+
+
+@utf8_decode
+def nick_completion_cb(data, completion_item, current_buffer, completion):
+    """
+    Adds all @-prefixed nicks to completion list
+    """
+    current_channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer)
+    if current_channel is None or current_channel.members is None:
+        return w.WEECHAT_RC_OK
+
+    base_command = w.hook_completion_get_string(completion, "base_command")
+    if base_command in ['invite', 'msg', 'query', 'whois']:
+        members = current_channel.team.members
+    else:
+        members = current_channel.members
+
+    for member in members:
+        user = current_channel.team.users.get(member)
+        if user and not user.deleted:
+            w.hook_completion_list_add(completion, user.name, 1, w.WEECHAT_LIST_POS_SORT)
+            w.hook_completion_list_add(completion, "@" + user.name, 1, w.WEECHAT_LIST_POS_SORT)
+    return w.WEECHAT_RC_OK
+
+
+@utf8_decode
+def emoji_completion_cb(data, completion_item, current_buffer, completion):
+    """
+    Adds all :-prefixed emoji to completion list
+    """
+    current_channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer)
+    if current_channel is None:
+        return w.WEECHAT_RC_OK
+
+    base_word = w.hook_completion_get_string(completion, "base_word")
+    if ":" not in base_word:
+        return w.WEECHAT_RC_OK
+    prefix = base_word.split(":")[0] + ":"
+
+    for emoji in current_channel.team.emoji_completions:
+        w.hook_completion_list_add(completion, prefix + emoji + ":", 0, w.WEECHAT_LIST_POS_SORT)
+    return w.WEECHAT_RC_OK
+
+
+@utf8_decode
+def thread_completion_cb(data, completion_item, current_buffer, completion):
+    """
+    Adds all $-prefixed thread ids to completion list
+    """
+    current_channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer)
+    if current_channel is None or not hasattr(current_channel, 'hashed_messages'):
+        return w.WEECHAT_RC_OK
+
+    threads = current_channel.hashed_messages.items()
+    for thread_id, message_ts in sorted(threads, key=lambda item: item[1]):
+        message = current_channel.messages.get(message_ts)
+        if message and message.number_of_replies():
+            w.hook_completion_list_add(completion, "$" + thread_id, 0, w.WEECHAT_LIST_POS_BEGINNING)
+    return w.WEECHAT_RC_OK
+
+
+@utf8_decode
+def topic_completion_cb(data, completion_item, current_buffer, completion):
+    """
+    Adds topic for current channel to completion list
+    """
+    current_channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer)
+    if current_channel is None:
+        return w.WEECHAT_RC_OK
+
+    topic = current_channel.render_topic()
+    channel_names = [channel.name for channel in current_channel.team.channels.values()]
+    if topic.split(' ', 1)[0] in channel_names:
+        topic = '{} {}'.format(current_channel.name, topic)
+
+    w.hook_completion_list_add(completion, topic, 0, w.WEECHAT_LIST_POS_SORT)
+    return w.WEECHAT_RC_OK
+
+
+@utf8_decode
+def usergroups_completion_cb(data, completion_item, current_buffer, completion):
+    """
+    Adds all @-prefixed usergroups to completion list
+    """
+    current_channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer)
+    if current_channel is None:
+        return w.WEECHAT_RC_OK
+
+    subteam_handles = [subteam.handle for subteam in current_channel.team.subteams.values()]
+    for group in subteam_handles + ["@channel", "@everyone", "@here"]:
+        w.hook_completion_list_add(completion, group, 1, w.WEECHAT_LIST_POS_SORT)
+    return w.WEECHAT_RC_OK
+
+
+@utf8_decode
+def complete_next_cb(data, current_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
+    """
+    current_channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer)
+    if not hasattr(current_channel, 'members') or current_channel is None or current_channel.members is None:
+        return w.WEECHAT_RC_OK
+
+    line_input = w.buffer_get_string(current_buffer, "input")
+    current_pos = w.buffer_get_integer(current_buffer, "input_pos") - 1
+    input_length = w.buffer_get_integer(current_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 line_input[current_pos] != '@' and not line_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 line_input[l] != '@' and not line_input[l].isalnum():
+            word_start = l + 1
+            break
+    for l in range(current_pos, input_length):
+        if not line_input[l].isalnum():
+            word_end = l
+            break
+    word = line_input[word_start:word_end]
+
+    for member in current_channel.members:
+        user = current_channel.team.users.get(member)
+        if user and user.name == word:
+            # Here, we cheat.  Insert a @ in front and rely in the @
+            # nicks being in the completion list
+            w.buffer_set(current_buffer, "input", line_input[:word_start] + "@" + line_input[word_start:])
+            w.buffer_set(current_buffer, "input_pos", str(w.buffer_get_integer(current_buffer, "input_pos") + 1))
+            return w.WEECHAT_RC_OK_EAT
+    return w.WEECHAT_RC_OK
+
+
+def script_unloaded():
+    stop_talking_to_slack()
+    return w.WEECHAT_RC_OK
+
+
+def stop_talking_to_slack():
+    """
+    complete
+    Prevents a race condition where quitting closes buffers
+    which triggers leaving the channel because of how close
+    buffer is handled
+    """
+    EVENTROUTER.shutdown()
+    for team in EVENTROUTER.teams.values():
+        team.ws.shutdown()
+    return w.WEECHAT_RC_OK
+
+##### New Classes
+
+
+class SlackRequest(object):
+    """
+    Encapsulates a Slack api request. Valuable as an object that we can add to the queue and/or retry.
+    makes a SHA of the requst url and current time so we can re-tag this on the way back through.
+    """
+
+    def __init__(self, team, request, post_data=None, channel=None, metadata=None, retries=3, token=None):
+        if team is None and token is None:
+            raise ValueError("Both team and token can't be None")
+        self.team = team
+        self.request = request
+        self.post_data = post_data if post_data else {}
+        self.channel = channel
+        self.metadata = metadata if metadata else {}
+        self.retries = retries
+        self.token = token if token else team.token
+        self.tries = 0
+        self.start_time = time.time()
+        self.request_normalized = re.sub(r'\W+', '', request)
+        self.domain = 'api.slack.com'
+        self.post_data['token'] = self.token
+        self.url = 'https://{}/api/{}?{}'.format(self.domain, self.request, urlencode(encode_to_utf8(self.post_data)))
+        self.params = {'useragent': 'wee_slack {}'.format(SCRIPT_VERSION)}
+        self.response_id = sha1_hex('{}{}'.format(self.url, self.start_time))
+
+    def __repr__(self):
+        return ("SlackRequest(team={}, request='{}', post_data={}, retries={}, token='{}...', "
+                "tries={}, start_time={})").format(self.team, self.request, self.post_data,
+                        self.retries, self.token[:15], self.tries, self.start_time)
+
+    def request_string(self):
+        return "{}".format(self.url)
+
+    def tried(self):
+        self.tries += 1
+        self.response_id = sha1_hex("{}{}".format(self.url, time.time()))
+
+    def should_try(self):
+        return self.tries < self.retries
+
+    def retry_ready(self):
+        return (self.start_time + (self.tries**2)) < time.time()
+
+
+class SlackSubteam(object):
+   """
+   Represents a slack group or subteam
+   """
+
+   def __init__(self, originating_team_id, is_member, **kwargs):
+       self.handle = '@{}'.format(kwargs['handle'])
+       self.identifier = kwargs['id']
+       self.name = kwargs['name']
+       self.description = kwargs.get('description')
+       self.team_id = originating_team_id
+       self.is_member = is_member
+
+   def __repr__(self):
+       return "Name:{} Identifier:{}".format(self.name, self.identifier)
+
+   def __eq__(self, compare_str):
+       return compare_str == self.identifier
+
+
+class SlackTeam(object):
+    """
+    incomplete
+    Team object under which users and channels live.. Does lots.
+    """
+
+    def __init__(self, eventrouter, token, websocket_url, team_info, subteams,  nick, myidentifier, my_manual_presence, users, bots, channels, **kwargs):
+        self.identifier = team_info["id"]
+        self.active = True
+        self.ws_url = websocket_url
+        self.connected = False
+        self.connecting_rtm = False
+        self.connecting_ws = False
+        self.ws = None
+        self.ws_counter = 0
+        self.ws_replies = {}
+        self.last_ping_time = 0
+        self.last_pong_time = time.time()
+        self.eventrouter = eventrouter
+        self.token = token
+        self.team = self
+        self.subteams = subteams
+        self.team_info = team_info
+        self.subdomain = team_info["domain"]
+        self.domain = self.subdomain + ".slack.com"
+        self.preferred_name = self.domain
+        self.nick = nick
+        self.myidentifier = myidentifier
+        self.my_manual_presence = my_manual_presence
+        try:
+            if self.channels:
+                for c in channels.keys():
+                    if not self.channels.get(c):
+                        self.channels[c] = channels[c]
+        except:
+            self.channels = channels
+        self.users = users
+        self.bots = bots
+        self.team_hash = SlackTeam.generate_team_hash(self.nick, self.subdomain)
+        self.name = self.domain
+        self.channel_buffer = None
+        self.got_history = True
+        self.create_buffer()
+        self.set_muted_channels(kwargs.get('muted_channels', ""))
+        self.set_highlight_words(kwargs.get('highlight_words', ""))
+        for c in self.channels.keys():
+            channels[c].set_related_server(self)
+            channels[c].check_should_open()
+        # Last step is to make sure my nickname is the set color
+        self.users[self.myidentifier].force_color(w.config_string(w.config_get('weechat.color.chat_nick_self')))
+        # This highlight step must happen after we have set related server
+        self.load_emoji_completions()
+        self.type = "team"
+
+    def __repr__(self):
+        return "domain={} nick={}".format(self.subdomain, self.nick)
+
+    def __eq__(self, compare_str):
+         return compare_str == self.token or compare_str == self.domain or compare_str == self.subdomain
+
+    @property
+    def members(self):
+        return self.users.keys()
+
+    def load_emoji_completions(self):
+        self.emoji_completions = list(EMOJI.keys())
+        if self.emoji_completions:
+            s = SlackRequest(self, "emoji.list")
+            self.eventrouter.receive(s)
+
+    def add_channel(self, channel):
+        self.channels[channel["id"]] = channel
+        channel.set_related_server(self)
+
+    def generate_usergroup_map(self):
+        return {s.handle: s.identifier for s in self.subteams.values()}
+
+    def create_buffer(self):
+        if not self.channel_buffer:
+            alias = config.server_aliases.get(self.subdomain)
+            if alias:
+                self.preferred_name = alias
+            elif config.short_buffer_names:
+                self.preferred_name = self.subdomain
+            else:
+                self.preferred_name = self.domain
+            self.channel_buffer = w.buffer_new(self.preferred_name, "buffer_input_callback", "EVENTROUTER", "", "")
+            self.eventrouter.weechat_controller.register_buffer(self.channel_buffer, self)
+            w.buffer_set(self.channel_buffer, "localvar_set_type", 'server')
+            w.buffer_set(self.channel_buffer, "localvar_set_nick", self.nick)
+            w.buffer_set(self.channel_buffer, "localvar_set_server", self.preferred_name)
+            self.buffer_merge()
+
+    def buffer_merge(self, config_value=None):
+        if not config_value:
+            config_value = w.config_string(w.config_get('irc.look.server_buffer'))
+        if config_value == 'merge_with_core':
+            w.buffer_merge(self.channel_buffer, w.buffer_search_main())
+        else:
+            w.buffer_unmerge(self.channel_buffer, 0)
+
+    def destroy_buffer(self, update_remote):
+        pass
+
+    def set_muted_channels(self, muted_str):
+        self.muted_channels = {x for x in muted_str.split(',') if x}
+        for channel in self.channels.values():
+            channel.set_highlights()
+
+    def set_highlight_words(self, highlight_str):
+        self.highlight_words = {x for x in highlight_str.split(',') if x}
+        for channel in self.channels.values():
+            channel.set_highlights()
+
+    def formatted_name(self, **kwargs):
+        return self.domain
+
+    def buffer_prnt(self, data, message=False):
+        tag_name = "team_message" if message else "team_info"
+        w.prnt_date_tags(self.channel_buffer, SlackTS().major, tag(tag_name), data)
+
+    def send_message(self, message, subtype=None, request_dict_ext={}):
+        w.prnt("", "ERROR: Sending a message in the team buffer is not supported")
+
+    def find_channel_by_members(self, members, channel_type=None):
+        for channel in self.channels.values():
+            if channel.get_members() == members and (
+                    channel_type is None or channel.type == channel_type):
+                return channel
+
+    def get_channel_map(self):
+        return {v.name: k for k, v in self.channels.items()}
+
+    def get_username_map(self):
+        return {v.name: k for k, v in self.users.items()}
+
+    def get_team_hash(self):
+        return self.team_hash
+
+    @staticmethod
+    def generate_team_hash(nick, subdomain):
+        return str(sha1_hex("{}{}".format(nick, subdomain)))
+
+    def refresh(self):
+        self.rename()
+
+    def rename(self):
+        pass
+
+    def is_user_present(self, user_id):
+        user = self.users.get(user_id)
+        if user and user.presence == 'active':
+            return True
+        else:
+            return False
+
+    def mark_read(self, ts=None, update_remote=True, force=False):
+        pass
+
+    def connect(self):
+        if not self.connected and not self.connecting_ws:
+            if self.ws_url:
+                self.connecting_ws = True
+                try:
+                    # only http proxy is currently supported
+                    proxy = ProxyWrapper()
+                    if proxy.has_proxy == True:
+                        ws = create_connection(self.ws_url, sslopt=sslopt_ca_certs, http_proxy_host=proxy.proxy_address, http_proxy_port=proxy.proxy_port, http_proxy_auth=(proxy.proxy_user, proxy.proxy_password))
+                    else:
+                        ws = create_connection(self.ws_url, sslopt=sslopt_ca_certs)
+
+                    self.hook = w.hook_fd(ws.sock.fileno(), 1, 0, 0, "receive_ws_callback", self.get_team_hash())
+                    ws.sock.setblocking(0)
+                    self.ws = ws
+                    self.set_reconnect_url(None)
+                    self.set_connected()
+                    self.connecting_ws = False
+                except:
+                    w.prnt(self.channel_buffer,
+                            'Failed connecting to slack team {}, retrying.'.format(self.domain))
+                    dbg('connect failed with exception:\n{}'.format(format_exc_tb()), level=5)
+                    self.connecting_ws = False
+                    return False
+            elif not self.connecting_rtm:
+                # The fast reconnect failed, so start over-ish
+                for chan in self.channels:
+                    self.channels[chan].got_history = False
+                s = initiate_connection(self.token, retries=999, team=self)
+                self.eventrouter.receive(s)
+                self.connecting_rtm = True
+
+    def set_connected(self):
+        self.connected = True
+        self.last_pong_time = time.time()
+        self.buffer_prnt('Connected to Slack team {} ({}) with username {}'.format(
+            self.team_info["name"], self.domain, self.nick))
+        dbg("connected to {}".format(self.domain))
+
+    def set_disconnected(self):
+        w.unhook(self.hook)
+        self.connected = False
+
+    def set_reconnect_url(self, url):
+        self.ws_url = url
+
+    def next_ws_transaction_id(self):
+        self.ws_counter += 1
+        return self.ws_counter
+
+    def send_to_websocket(self, data, expect_reply=True):
+        data["id"] = self.next_ws_transaction_id()
+        message = json.dumps(data)
+        try:
+            if expect_reply:
+                self.ws_replies[data["id"]] = data
+            self.ws.send(encode_to_utf8(message))
+            dbg("Sent {}...".format(message[:100]))
+        except (WebSocketConnectionClosedException, socket.error) as e:
+            handle_socket_error(e, self, 'send')
+
+    def update_member_presence(self, user, presence):
+        user.presence = presence
+
+        for c in self.channels:
+            c = self.channels[c]
+            if user.id in c.members:
+                c.update_nicklist(user.id)
+
+    def subscribe_users_presence(self):
+        # FIXME: There is a limitation in the API to the size of the
+        # json we can send.
+        # We should try to be smarter to fetch the users whom we want to
+        # subscribe to.
+        users = list(self.users.keys())[:750]
+        if self.myidentifier not in users:
+            users.append(self.myidentifier)
+        self.send_to_websocket({
+            "type": "presence_sub",
+            "ids": users,
+        }, expect_reply=False)
+
+
+class SlackChannelCommon(object):
+    def send_add_reaction(self, msg_id, reaction):
+        self.send_change_reaction("reactions.add", msg_id, reaction)
+
+    def send_remove_reaction(self, msg_id, reaction):
+        self.send_change_reaction("reactions.remove", msg_id, reaction)
+
+    def send_change_reaction(self, method, msg_id, reaction):
+        if type(msg_id) is not int:
+            if msg_id in self.hashed_messages:
+                timestamp = str(self.hashed_messages[msg_id])
+            else:
+                return
+        elif 0 < msg_id <= len(self.messages):
+            keys = self.main_message_keys_reversed()
+            timestamp = next(islice(keys, msg_id - 1, None))
+        else:
+            return
+        data = {"channel": self.identifier, "timestamp": timestamp, "name": reaction}
+        s = SlackRequest(self.team, method, data, channel=self, metadata={'reaction': reaction})
+        self.eventrouter.receive(s)
+
+    def edit_nth_previous_message(self, msg_id, old, new, flags):
+        message = self.my_last_message(msg_id)
+        if message is None:
+            return
+        if new == "" and old == "":
+            s = SlackRequest(self.team, "chat.delete", {"channel": self.identifier, "ts": message['ts']}, channel=self)
+            self.eventrouter.receive(s)
+        else:
+            num_replace = 0 if 'g' in flags else 1
+            f = re.UNICODE
+            f |= re.IGNORECASE if 'i' in flags else 0
+            f |= re.MULTILINE if 'm' in flags else 0
+            f |= re.DOTALL if 's' in flags else 0
+            new_message = re.sub(old, new, message["text"], num_replace, f)
+            if new_message != message["text"]:
+                s = SlackRequest(self.team, "chat.update",
+                        {"channel": self.identifier, "ts": message['ts'], "text": new_message}, channel=self)
+                self.eventrouter.receive(s)
+
+    def my_last_message(self, msg_id):
+        if type(msg_id) is not int:
+            ts = self.hashed_messages.get(msg_id)
+            m = self.messages.get(ts)
+            if m is not None and m.message_json.get("user") == self.team.myidentifier:
+                return m.message_json
+        else:
+            for key in self.main_message_keys_reversed():
+                m = self.messages[key]
+                if m.message_json.get("user") == self.team.myidentifier:
+                    msg_id -= 1
+                    if msg_id == 0:
+                        return m.message_json
+
+    def change_message(self, ts, message_json=None, text=None):
+        ts = SlackTS(ts)
+        m = self.messages.get(ts)
+        if not m:
+            return
+        if message_json:
+            m.message_json.update(message_json)
+        if text:
+            m.change_text(text)
+
+        if type(m) == SlackMessage or config.thread_messages_in_channel:
+            new_text = self.render(m, force=True)
+            modify_buffer_line(self.channel_buffer, ts, new_text)
+        if type(m) == SlackThreadMessage:
+            thread_channel = m.parent_message.thread_channel
+            if thread_channel and thread_channel.active:
+                new_text = thread_channel.render(m, force=True)
+                modify_buffer_line(thread_channel.channel_buffer, ts, new_text)
+
+    def hash_message(self, ts):
+        ts = SlackTS(ts)
+
+        def calc_hash(ts):
+            return sha1_hex(str(ts))
+
+        if ts in self.messages and not self.messages[ts].hash:
+            message = self.messages[ts]
+            tshash = calc_hash(message.ts)
+            hl = 3
+
+            for i in range(hl, len(tshash) + 1):
+                shorthash = tshash[:i]
+                if self.hashed_messages.get(shorthash) == ts:
+                    message.hash = shorthash
+                    return shorthash
+
+            shorthash = tshash[:hl]
+            while any(x.startswith(shorthash) for x in self.hashed_messages):
+                hl += 1
+                shorthash = tshash[:hl]
+
+            if shorthash[:-1] in self.hashed_messages:
+                col_ts = self.hashed_messages.pop(shorthash[:-1])
+                col_new_hash = calc_hash(col_ts)[:hl]
+                self.hashed_messages[col_new_hash] = col_ts
+                col_msg = self.messages.get(col_ts)
+                if col_msg:
+                    col_msg.hash = col_new_hash
+                    self.change_message(str(col_msg.ts))
+                    if col_msg.thread_channel:
+                        col_msg.thread_channel.rename()
+
+            self.hashed_messages[shorthash] = message.ts
+            message.hash = shorthash
+            return shorthash
+        elif ts in self.messages:
+            return self.messages[ts].hash
+
+
+
+class SlackChannel(SlackChannelCommon):
+    """
+    Represents an individual slack channel.
+    """
+
+    def __init__(self, eventrouter, **kwargs):
+        # We require these two things for a valid object,
+        # the rest we can just learn from slack
+        self.active = False
+        for key, value in kwargs.items():
+            setattr(self, key, value)
+        self.eventrouter = eventrouter
+        self.slack_name = kwargs["name"]
+        self.slack_purpose = kwargs.get("purpose", {"value": ""})
+        self.topic = kwargs.get("topic", {"value": ""})
+        self.identifier = kwargs["id"]
+        self.last_read = SlackTS(kwargs.get("last_read", SlackTS()))
+        self.channel_buffer = None
+        self.team = kwargs.get('team')
+        self.got_history = False
+        self.messages = OrderedDict()
+        self.hashed_messages = {}
+        self.thread_channels = {}
+        self.new_messages = False
+        self.typing = {}
+        self.type = 'channel'
+        self.set_name(self.slack_name)
+        # short name relates to the localvar we change for typing indication
+        self.current_short_name = self.name
+        self.set_members(kwargs.get('members', []))
+        self.unread_count_display = 0
+        self.last_line_from = None
+
+    def __eq__(self, compare_str):
+        if compare_str == self.slack_name or compare_str == self.formatted_name() or compare_str == self.formatted_name(style="long_default"):
+            return True
+        else:
+            return False
+
+    def __repr__(self):
+        return "Name:{} Identifier:{}".format(self.name, self.identifier)
+
+    @property
+    def muted(self):
+        return self.identifier in self.team.muted_channels
+
+    def set_name(self, slack_name):
+        self.name = "#" + slack_name
+
+    def refresh(self):
+        return self.rename()
+
+    def rename(self):
+        if self.channel_buffer:
+            new_name = self.formatted_name(typing=self.is_someone_typing(), style="sidebar")
+            if self.current_short_name != new_name:
+                self.current_short_name = new_name
+                w.buffer_set(self.channel_buffer, "short_name", new_name)
+                return True
+        return False
+
+    def set_members(self, members):
+        self.members = set(members)
+        self.update_nicklist()
+
+    def get_members(self):
+        return self.members
+
+    def set_unread_count_display(self, count):
+        self.unread_count_display = count
+        self.new_messages = bool(self.unread_count_display)
+        if self.muted and config.muted_channels_activity != "all":
+            return
+        for c in range(self.unread_count_display):
+            if self.type in ["im", "mpim"]:
+                w.buffer_set(self.channel_buffer, "hotlist", "2")
+            else:
+                w.buffer_set(self.channel_buffer, "hotlist", "1")
+
+    def formatted_name(self, style="default", typing=False, **kwargs):
+        if typing and config.channel_name_typing_indicator:
+            prepend = ">"
+        elif self.type == "group" or self.type == "private":
+            prepend = config.group_name_prefix
+        elif self.type == "shared":
+            prepend = config.shared_name_prefix
+        else:
+            prepend = "#"
+        sidebar_color = config.color_buflist_muted_channels if self.muted else ""
+        select = {
+            "default": prepend + self.slack_name,
+            "sidebar": colorize_string(sidebar_color, prepend + self.slack_name),
+            "base": self.slack_name,
+            "long_default": "{}.{}{}".format(self.team.preferred_name, prepend, self.slack_name),
+            "long_base": "{}.{}".format(self.team.preferred_name, self.slack_name),
+        }
+        return select[style]
+
+    def render_topic(self, fallback_to_purpose=False):
+        topic = self.topic['value']
+        if not topic and fallback_to_purpose:
+            topic = self.slack_purpose['value']
+        return unhtmlescape(unfurl_refs(topic))
+
+    def set_topic(self, value=None):
+        if value is not None:
+            self.topic = {"value": value}
+        if self.channel_buffer:
+            topic = self.render_topic(fallback_to_purpose=True)
+            w.buffer_set(self.channel_buffer, "title", topic)
+
+    def update_from_message_json(self, message_json):
+        for key, value in message_json.items():
+            setattr(self, key, value)
+
+    def open(self, update_remote=True):
+        if update_remote:
+            if "join" in SLACK_API_TRANSLATOR[self.type]:
+                s = SlackRequest(self.team, SLACK_API_TRANSLATOR[self.type]["join"],
+                        {"channel": self.identifier}, channel=self)
+                self.eventrouter.receive(s)
+        self.create_buffer()
+        self.active = True
+        self.get_history()
+
+    def check_should_open(self, force=False):
+        if hasattr(self, "is_archived") and self.is_archived:
+            return
+
+        if force:
+            self.create_buffer()
+            return
+
+        # Only check is_member if is_open is not set, because in some cases
+        # (e.g. group DMs), is_member should be ignored in favor of is_open.
+        is_open = self.is_open if hasattr(self, "is_open") else self.is_member
+        if is_open or self.unread_count_display:
+            self.create_buffer()
+            if config.background_load_all_history:
+                self.get_history(slow_queue=True)
+
+    def set_related_server(self, team):
+        self.team = team
+
+    def highlights(self):
+        nick_highlights = {'@' + self.team.nick, self.team.myidentifier}
+        subteam_highlights = {subteam.handle for subteam in self.team.subteams.values()
+                if subteam.is_member}
+        highlights = nick_highlights | subteam_highlights | self.team.highlight_words
+        if self.muted and config.muted_channels_activity == "personal_highlights":
+            return highlights
+        else:
+            return highlights | {"@channel", "@everyone", "@group", "@here"}
+
+    def set_highlights(self):
+        # highlight my own name and any set highlights
+        if self.channel_buffer:
+            h_str = ",".join(self.highlights())
+            w.buffer_set(self.channel_buffer, "highlight_words", h_str)
+
+            if self.muted and config.muted_channels_activity != "all":
+                notify_level = "0" if config.muted_channels_activity == "none" else "1"
+                w.buffer_set(self.channel_buffer, "notify", notify_level)
+            else:
+                w.buffer_set(self.channel_buffer, "notify", "3")
+
+            if self.muted and config.muted_channels_activity == "none":
+                w.buffer_set(self.channel_buffer, "highlight_tags_restrict", "highlight_force")
+            else:
+                w.buffer_set(self.channel_buffer, "highlight_tags_restrict", "")
+
+            for thread_channel in self.thread_channels.values():
+                thread_channel.set_highlights(h_str)
+
+    def create_buffer(self):
+        """
+        Creates the weechat buffer where the channel magic happens.
+        """
+        if not self.channel_buffer:
+            self.active = True
+            self.channel_buffer = w.buffer_new(self.formatted_name(style="long_default"), "buffer_input_callback", "EVENTROUTER", "", "")
+            self.eventrouter.weechat_controller.register_buffer(self.channel_buffer, self)
+            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')
+            w.buffer_set(self.channel_buffer, "localvar_set_channel", self.formatted_name())
+            w.buffer_set(self.channel_buffer, "localvar_set_nick", self.team.nick)
+            w.buffer_set(self.channel_buffer, "short_name", self.formatted_name(style="sidebar", enable_color=True))
+            self.set_highlights()
+            self.set_topic()
+            self.eventrouter.weechat_controller.set_refresh_buffer_list(True)
+            if self.channel_buffer:
+                w.buffer_set(self.channel_buffer, "localvar_set_server", self.team.preferred_name)
+        self.update_nicklist()
+
+        if "info" in SLACK_API_TRANSLATOR[self.type]:
+            s = SlackRequest(self.team, SLACK_API_TRANSLATOR[self.type]["info"],
+                    {"channel": self.identifier}, channel=self)
+            self.eventrouter.receive(s)
+
+        if self.type == "im":
+            if "join" in SLACK_API_TRANSLATOR[self.type]:
+                s = SlackRequest(self.team, SLACK_API_TRANSLATOR[self.type]["join"],
+                        {"users": self.user, "return_im": True}, channel=self)
+                self.eventrouter.receive(s)
+
+    def clear_messages(self):
+        w.buffer_clear(self.channel_buffer)
+        self.messages = OrderedDict()
+        self.got_history = False
+
+    def destroy_buffer(self, update_remote):
+        self.clear_messages()
+        self.channel_buffer = None
+        self.active = False
+        if update_remote and not self.eventrouter.shutting_down:
+            s = SlackRequest(self.team, SLACK_API_TRANSLATOR[self.type]["leave"],
+                    {"channel": self.identifier}, channel=self)
+            self.eventrouter.receive(s)
+
+    def buffer_prnt(self, nick, text, timestamp=str(time.time()), tagset=None, tag_nick=None, history_message=False, extra_tags=None):
+        data = "{}\t{}".format(format_nick(nick, self.last_line_from), text)
+        self.last_line_from = nick
+        ts = SlackTS(timestamp)
+        last_read = SlackTS(self.last_read)
+        # without this, DMs won't open automatically
+        if not self.channel_buffer and ts > last_read:
+            self.open(update_remote=False)
+        if self.channel_buffer:
+            # backlog messages - we will update the read marker as we print these
+            backlog = ts <= last_read
+            if not backlog:
+                self.new_messages = True
+
+            if not tagset:
+                if self.type in ["im", "mpim"]:
+                    tagset = "dm"
+                else:
+                    tagset = "channel"
+
+            no_log = history_message and backlog
+            self_msg = tag_nick == self.team.nick
+            tags = tag(tagset, user=tag_nick, self_msg=self_msg, backlog=backlog, no_log=no_log, extra_tags=extra_tags)
+
+            try:
+                if (config.unhide_buffers_with_activity
+                        and not self.is_visible() and not self.muted):
+                    w.buffer_set(self.channel_buffer, "hidden", "0")
+
+                w.prnt_date_tags(self.channel_buffer, ts.major, tags, data)
+                modify_last_print_time(self.channel_buffer, ts.minor)
+                if backlog or self_msg:
+                    self.mark_read(ts, update_remote=False, force=True)
+            except:
+                dbg("Problem processing buffer_prnt")
+
+    def send_message(self, message, subtype=None, request_dict_ext={}):
+        message = linkify_text(message, self.team)
+        dbg(message)
+        if subtype == 'me_message':
+            s = SlackRequest(self.team, "chat.meMessage", {"channel": self.identifier, "text": message}, channel=self)
+            self.eventrouter.receive(s)
+        else:
+            request = {"type": "message", "channel": self.identifier,
+                    "text": message, "user": self.team.myidentifier}
+            request.update(request_dict_ext)
+            self.team.send_to_websocket(request)
+
+    def store_message(self, message, team, from_me=False):
+        if not self.active:
+            return
+        if from_me:
+            message.message_json["user"] = team.myidentifier
+        self.messages[SlackTS(message.ts)] = message
+
+        sorted_messages = sorted(self.messages.items())
+        messages_to_delete = sorted_messages[:-SCROLLBACK_SIZE]
+        messages_to_keep = sorted_messages[-SCROLLBACK_SIZE:]
+        for message_hash in [m[1].hash for m in messages_to_delete]:
+            if message_hash in self.hashed_messages:
+                del self.hashed_messages[message_hash]
+        self.messages = OrderedDict(messages_to_keep)
+
+    def is_visible(self):
+        return w.buffer_get_integer(self.channel_buffer, "hidden") == 0
+
+    def get_history(self, slow_queue=False):
+        if not self.got_history:
+            # we have probably reconnected. flush the buffer
+            if self.team.connected:
+                self.clear_messages()
+            w.prnt_date_tags(self.channel_buffer, SlackTS().major,
+                    tag(backlog=True, no_log=True), '\tgetting channel history...')
+            s = SlackRequest(self.team, SLACK_API_TRANSLATOR[self.type]["history"],
+                    {"channel": self.identifier, "count": BACKLOG_SIZE}, channel=self, metadata={'clear': True})
+            if not slow_queue:
+                self.eventrouter.receive(s)
+            else:
+                self.eventrouter.receive_slow(s)
+            self.got_history = True
+
+    def main_message_keys_reversed(self):
+        return (key for key in reversed(self.messages)
+                if type(self.messages[key]) == SlackMessage)
+
+    # Typing related
+    def set_typing(self, user):
+        if self.channel_buffer and self.is_visible():
+            self.typing[user] = time.time()
+            self.eventrouter.weechat_controller.set_refresh_buffer_list(True)
+
+    def unset_typing(self, user):
+        if self.channel_buffer and self.is_visible():
+            u = self.typing.get(user)
+            if u:
+                self.eventrouter.weechat_controller.set_refresh_buffer_list(True)
+
+    def is_someone_typing(self):
+        """
+        Walks through dict of typing folks in a channel and fast
+        returns if any of them is actively typing. If none are,
+        nulls the dict and returns false.
+        """
+        for user, timestamp in self.typing.items():
+            if timestamp + 4 > time.time():
+                return True
+        if len(self.typing) > 0:
+            self.typing = {}
+            self.eventrouter.weechat_controller.set_refresh_buffer_list(True)
+        return False
+
+    def get_typing_list(self):
+        """
+        Returns the names of everyone in the channel who is currently typing.
+        """
+        typing = []
+        for user, timestamp in self.typing.items():
+            if timestamp + 4 > time.time():
+                typing.append(user)
+            else:
+                del self.typing[user]
+        return typing
+
+    def mark_read(self, ts=None, update_remote=True, force=False):
+        if self.new_messages or force:
+            if self.channel_buffer:
+                w.buffer_set(self.channel_buffer, "unread", "")
+                w.buffer_set(self.channel_buffer, "hotlist", "-1")
+            if not ts:
+                ts = next(reversed(self.messages), SlackTS())
+            if ts > self.last_read:
+                self.last_read = ts
+            if update_remote:
+                s = SlackRequest(self.team, SLACK_API_TRANSLATOR[self.type]["mark"],
+                        {"channel": self.identifier, "ts": ts}, channel=self)
+                self.eventrouter.receive(s)
+                self.new_messages = False
+
+    def user_joined(self, user_id):
+        # ugly hack - for some reason this gets turned into a list
+        self.members = set(self.members)
+        self.members.add(user_id)
+        self.update_nicklist(user_id)
+
+    def user_left(self, user_id):
+        self.members.discard(user_id)
+        self.update_nicklist(user_id)
+
+    def update_nicklist(self, user=None):
+        if not self.channel_buffer:
+            return
+        if self.type not in ["channel", "group", "mpim", "private", "shared"]:
+            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)
+
+        # Add External nicklist group only for shared channels
+        if self.type == 'shared':
+            external = w.nicklist_search_group(self.channel_buffer, '', NICK_GROUP_EXTERNAL)
+            if not external:
+                external = w.nicklist_add_group(self.channel_buffer, '', NICK_GROUP_EXTERNAL, 'weechat.color.nicklist_group', 2)
+
+        if user and len(self.members) < 1000:
+            user = self.team.users.get(user)
+            # External users that have left shared channels won't exist
+            if not user or user.deleted:
+                return
+            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..
+            nick_group = afk
+            if user.is_external:
+                nick_group = external
+            elif self.team.is_user_present(user.identifier):
+                nick_group = here
+            if user.identifier in self.members:
+                w.nicklist_add_nick(self.channel_buffer, nick_group, user.name, user.color_name, "", "", 1)
+
+        # if we didn't get a user, build a complete list. this is expensive.
+        else:
+            if len(self.members) < 1000:
+                try:
+                    for user in self.members:
+                        user = self.team.users.get(user)
+                        if user.deleted:
+                            continue
+                        nick_group = afk
+                        if user.is_external:
+                            nick_group = external
+                        elif self.team.is_user_present(user.identifier):
+                            nick_group = here
+                        w.nicklist_add_nick(self.channel_buffer, nick_group, user.name, user.color_name, "", "", 1)
+                except:
+                    dbg("DEBUG: {} {} {}".format(self.identifier, self.name, format_exc_only()))
+            else:
+                w.nicklist_remove_all(self.channel_buffer)
+                for fn in ["1| too", "2| many", "3| users", "4| to", "5| show"]:
+                    w.nicklist_add_group(self.channel_buffer, '', fn, w.color('white'), 1)
+
+    def render(self, message, force=False):
+        text = message.render(force)
+        if isinstance(message, SlackThreadMessage):
+            thread_id = message.parent_message.hash or message.parent_message.ts
+            return colorize_string(get_thread_color(thread_id), '[{}]'.format(thread_id)) + ' {}'.format(text)
+
+        return text
+
+
+class SlackDMChannel(SlackChannel):
+    """
+    Subclass of a normal channel for person-to-person communication, which
+    has some important differences.
+    """
+
+    def __init__(self, eventrouter, users, **kwargs):
+        dmuser = kwargs["user"]
+        kwargs["name"] = users[dmuser].name if dmuser in users else dmuser
+        super(SlackDMChannel, self).__init__(eventrouter, **kwargs)
+        self.type = 'im'
+        self.update_color()
+        self.set_name(self.slack_name)
+        if dmuser in users:
+            self.set_topic(create_user_status_string(users[dmuser].profile))
+
+    def set_related_server(self, team):
+        super(SlackDMChannel, self).set_related_server(team)
+        if self.user not in self.team.users:
+            s = SlackRequest(self.team, 'users.info', {'user': self.slack_name}, channel=self)
+            self.eventrouter.receive(s)
+
+    def set_name(self, slack_name):
+        self.name = slack_name
+
+    def get_members(self):
+        return {self.user}
+
+    def create_buffer(self):
+        if not self.channel_buffer:
+            super(SlackDMChannel, self).create_buffer()
+            w.buffer_set(self.channel_buffer, "localvar_set_type", 'private')
+
+    def update_color(self):
+        if config.colorize_private_chats:
+            self.color_name = get_nick_color(self.name)
+        else:
+            self.color_name = ""
+
+    def formatted_name(self, style="default", typing=False, present=True, enable_color=False, **kwargs):
+        prepend = ""
+        if config.show_buflist_presence:
+            prepend = "+" if present else " "
+        select = {
+            "default": self.slack_name,
+            "sidebar": prepend + self.slack_name,
+            "base": self.slack_name,
+            "long_default": "{}.{}".format(self.team.preferred_name, self.slack_name),
+            "long_base": "{}.{}".format(self.team.preferred_name, self.slack_name),
+        }
+        if config.colorize_private_chats and enable_color:
+            return colorize_string(self.color_name, select[style])
+        else:
+            return select[style]
+
+    def open(self, update_remote=True):
+        self.create_buffer()
+        self.get_history()
+        if "info" in SLACK_API_TRANSLATOR[self.type]:
+            s = SlackRequest(self.team, SLACK_API_TRANSLATOR[self.type]["info"],
+                    {"name": self.identifier}, channel=self)
+            self.eventrouter.receive(s)
+        if update_remote:
+            if "join" in SLACK_API_TRANSLATOR[self.type]:
+                s = SlackRequest(self.team, SLACK_API_TRANSLATOR[self.type]["join"],
+                        {"users": self.user, "return_im": True}, channel=self)
+                self.eventrouter.receive(s)
+
+    def rename(self):
+        if self.channel_buffer:
+            new_name = self.formatted_name(style="sidebar", present=self.team.is_user_present(self.user), enable_color=config.colorize_private_chats)
+            if self.current_short_name != new_name:
+                self.current_short_name = new_name
+                w.buffer_set(self.channel_buffer, "short_name", new_name)
+                return True
+        return False
+
+    def refresh(self):
+        return self.rename()
+
+
+class SlackGroupChannel(SlackChannel):
+    """
+    A group channel is a private discussion group.
+    """
+
+    def __init__(self, eventrouter, **kwargs):
+        super(SlackGroupChannel, self).__init__(eventrouter, **kwargs)
+        self.type = "group"
+        self.set_name(self.slack_name)
+
+    def set_name(self, slack_name):
+        self.name = config.group_name_prefix + slack_name
+
+
+class SlackPrivateChannel(SlackGroupChannel):
+    """
+    A private channel is a private discussion group. At the time of writing, it
+    differs from group channels in that group channels are channels initially
+    created as private, while private channels are public channels which are
+    later converted to private.
+    """
+
+    def __init__(self, eventrouter, **kwargs):
+        super(SlackPrivateChannel, self).__init__(eventrouter, **kwargs)
+        self.type = "private"
+
+    def set_related_server(self, team):
+        super(SlackPrivateChannel, self).set_related_server(team)
+        # Fetch members here (after the team is known) since they aren't
+        # included in rtm.start
+        s = SlackRequest(team, 'conversations.members', {'channel': self.identifier}, channel=self)
+        self.eventrouter.receive(s)
+
+
+class SlackMPDMChannel(SlackChannel):
+    """
+    An MPDM channel is a special instance of a 'group' channel.
+    We change the name to look less terrible in weechat.
+    """
+
+    def __init__(self, eventrouter, team_users, myidentifier, **kwargs):
+        kwargs["name"] = ','.join(sorted(
+                getattr(team_users.get(user_id), 'name', user_id)
+                for user_id in kwargs["members"]
+                if user_id != myidentifier
+        ))
+        super(SlackMPDMChannel, self).__init__(eventrouter, **kwargs)
+        self.type = "mpim"
+
+    def open(self, update_remote=True):
+        self.create_buffer()
+        self.active = True
+        self.get_history()
+        if "info" in SLACK_API_TRANSLATOR[self.type]:
+            s = SlackRequest(self.team, SLACK_API_TRANSLATOR[self.type]["info"],
+                    {"channel": self.identifier}, channel=self)
+            self.eventrouter.receive(s)
+        if update_remote and 'join' in SLACK_API_TRANSLATOR[self.type]:
+            s = SlackRequest(self.team, SLACK_API_TRANSLATOR[self.type]['join'],
+                    {'users': ','.join(self.members)}, channel=self)
+            self.eventrouter.receive(s)
+
+    def set_name(self, slack_name):
+        self.name = slack_name
+
+    def formatted_name(self, style="default", typing=False, **kwargs):
+        if typing and config.channel_name_typing_indicator:
+            prepend = ">"
+        else:
+            prepend = "@"
+        select = {
+            "default": self.name,
+            "sidebar": prepend + self.name,
+            "base": self.name,
+            "long_default": "{}.{}".format(self.team.preferred_name, self.name),
+            "long_base": "{}.{}".format(self.team.preferred_name, self.name),
+        }
+        return select[style]
+
+    def rename(self):
+        pass
+
+
+class SlackSharedChannel(SlackChannel):
+    def __init__(self, eventrouter, **kwargs):
+        super(SlackSharedChannel, self).__init__(eventrouter, **kwargs)
+        self.type = 'shared'
+
+    def set_related_server(self, team):
+        super(SlackSharedChannel, self).set_related_server(team)
+        # Fetch members here (after the team is known) since they aren't
+        # included in rtm.start
+        s = SlackRequest(team, 'conversations.members', {'channel': self.identifier}, channel=self)
+        self.eventrouter.receive(s)
+
+    def get_history(self, slow_queue=False):
+        # Get info for external users in the channel
+        for user in self.members - set(self.team.users.keys()):
+            s = SlackRequest(self.team, 'users.info', {'user': user}, channel=self)
+            self.eventrouter.receive(s)
+        super(SlackSharedChannel, self).get_history(slow_queue)
+
+    def set_name(self, slack_name):
+        self.name = config.shared_name_prefix + slack_name
+
+
+class SlackThreadChannel(SlackChannelCommon):
+    """
+    A thread channel is a virtual channel. We don't inherit from
+    SlackChannel, because most of how it operates will be different.
+    """
+
+    def __init__(self, eventrouter, parent_message):
+        self.eventrouter = eventrouter
+        self.parent_message = parent_message
+        self.hashed_messages = {}
+        self.channel_buffer = None
+        self.type = "thread"
+        self.got_history = False
+        self.label = None
+        self.members = self.parent_message.channel.members
+        self.team = self.parent_message.team
+        self.last_line_from = None
+
+    @property
+    def identifier(self):
+        return self.parent_message.channel.identifier
+
+    @property
+    def messages(self):
+        return self.parent_message.channel.messages
+
+    @property
+    def muted(self):
+        return self.parent_message.channel.muted
+
+    def formatted_name(self, style="default", **kwargs):
+        hash_or_ts = self.parent_message.hash or self.parent_message.ts
+        styles = {
+            "default": " +{}".format(hash_or_ts),
+            "long_default": "{}.{}".format(self.parent_message.channel.formatted_name(style="long_default"), hash_or_ts),
+            "sidebar": " +{}".format(hash_or_ts),
+        }
+        return styles[style]
+
+    def refresh(self):
+        self.rename()
+
+    def mark_read(self, ts=None, update_remote=True, force=False):
+        if self.channel_buffer:
+            w.buffer_set(self.channel_buffer, "unread", "")
+            w.buffer_set(self.channel_buffer, "hotlist", "-1")
+
+    def buffer_prnt(self, nick, text, timestamp, tag_nick=None):
+        data = "{}\t{}".format(format_nick(nick, self.last_line_from), text)
+        self.last_line_from = nick
+        ts = SlackTS(timestamp)
+        if self.channel_buffer:
+            if self.parent_message.channel.type in ["im", "mpim"]:
+                tagset = "dm"
+            else:
+                tagset = "channel"
+            self_msg = tag_nick == self.team.nick
+            tags = tag(tagset, user=tag_nick, self_msg=self_msg)
+
+            w.prnt_date_tags(self.channel_buffer, ts.major, tags, data)
+            modify_last_print_time(self.channel_buffer, ts.minor)
+            if self_msg:
+                self.mark_read(ts, update_remote=False, force=True)
+
+    def get_history(self):
+        self.got_history = True
+        for message in chain([self.parent_message], self.parent_message.submessages):
+            text = self.render(message)
+            self.buffer_prnt(message.sender, text, message.ts, tag_nick=message.sender_plain)
+        if len(self.parent_message.submessages) < self.parent_message.number_of_replies():
+            s = SlackRequest(self.team, "conversations.replies",
+                    {"channel": self.identifier, "ts": self.parent_message.ts},
+                    channel=self.parent_message.channel)
+            self.eventrouter.receive(s)
+
+    def main_message_keys_reversed(self):
+        return (message.ts for message in reversed(self.parent_message.submessages))
+
+    def send_message(self, message, subtype=None, request_dict_ext={}):
+        if subtype == 'me_message':
+            w.prnt("", "ERROR: /me is not supported in threads")
+            return w.WEECHAT_RC_ERROR
+        message = linkify_text(message, self.team)
+        dbg(message)
+        request = {"type": "message", "text": message,
+                "channel": self.parent_message.channel.identifier,
+                "thread_ts": str(self.parent_message.ts),
+                "user": self.team.myidentifier}
+        request.update(request_dict_ext)
+        self.team.send_to_websocket(request)
+
+    def open(self, update_remote=True):
+        self.create_buffer()
+        self.active = True
+        self.get_history()
+
+    def rename(self):
+        if self.channel_buffer and not self.label:
+            w.buffer_set(self.channel_buffer, "short_name", self.formatted_name(style="sidebar", enable_color=True))
+
+    def set_highlights(self, highlight_string=None):
+        if self.channel_buffer:
+            if highlight_string is None:
+                highlight_string = ",".join(self.parent_message.channel.highlights())
+            w.buffer_set(self.channel_buffer, "highlight_words", highlight_string)
+
+    def create_buffer(self):
+        """
+        Creates the weechat buffer where the thread magic happens.
+        """
+        if not self.channel_buffer:
+            self.channel_buffer = w.buffer_new(self.formatted_name(style="long_default"), "buffer_input_callback", "EVENTROUTER", "", "")
+            self.eventrouter.weechat_controller.register_buffer(self.channel_buffer, self)
+            w.buffer_set(self.channel_buffer, "localvar_set_type", 'channel')
+            w.buffer_set(self.channel_buffer, "localvar_set_nick", self.team.nick)
+            w.buffer_set(self.channel_buffer, "localvar_set_channel", self.formatted_name())
+            w.buffer_set(self.channel_buffer, "localvar_set_server", self.team.preferred_name)
+            w.buffer_set(self.channel_buffer, "short_name", self.formatted_name(style="sidebar", enable_color=True))
+            self.set_highlights()
+            time_format = w.config_string(w.config_get("weechat.look.buffer_time_format"))
+            parent_time = time.localtime(SlackTS(self.parent_message.ts).major)
+            topic = '{} {} | {}'.format(time.strftime(time_format, parent_time), self.parent_message.sender, self.render(self.parent_message)	)
+            w.buffer_set(self.channel_buffer, "title", topic)
+
+            # self.eventrouter.weechat_controller.set_refresh_buffer_list(True)
+
+    def destroy_buffer(self, update_remote):
+        self.channel_buffer = None
+        self.got_history = False
+        self.active = False
+
+    def render(self, message, force=False):
+        return message.render(force)
+
+
+class SlackUser(object):
+    """
+    Represends an individual slack user. Also where you set their name formatting.
+    """
+
+    def __init__(self, originating_team_id, **kwargs):
+        self.identifier = kwargs["id"]
+        # These attributes may be missing in the response, so we have to make
+        # sure they're set
+        self.profile = {}
+        self.presence = kwargs.get("presence", "unknown")
+        self.deleted = kwargs.get("deleted", False)
+        self.is_external = (not kwargs.get("is_bot") and
+                kwargs.get("team_id") != originating_team_id)
+        for key, value in kwargs.items():
+            setattr(self, key, value)
+
+        self.name = nick_from_profile(self.profile, kwargs["name"])
+        self.username = kwargs["name"]
+        self.update_color()
+
+    def __repr__(self):
+        return "Name:{} Identifier:{}".format(self.name, self.identifier)
+
+    def force_color(self, color_name):
+        self.color_name = color_name
+
+    def update_color(self):
+        # This will automatically be none/"" if the user has disabled nick
+        # colourization.
+        self.color_name = get_nick_color(self.name)
+
+    def update_status(self, status_emoji, status_text):
+        self.profile["status_emoji"] = status_emoji
+        self.profile["status_text"] = status_text
+
+    def formatted_name(self, prepend="", enable_color=True):
+        name = prepend + self.name
+        if enable_color:
+            return colorize_string(self.color_name, name)
+        else:
+            return name
+
+
+class SlackBot(SlackUser):
+    """
+    Basically the same as a user, but split out to identify and for future
+    needs
+    """
+    def __init__(self, originating_team_id, **kwargs):
+        super(SlackBot, self).__init__(originating_team_id, is_bot=True, **kwargs)
+
+
+class SlackMessage(object):
+    """
+    Represents a single slack message and associated context/metadata.
+    These are modifiable and can be rerendered to change a message,
+    delete a message, add a reaction, add a thread.
+    Note: these can't be tied to a SlackUser object because users
+    can be deleted, so we have to store sender in each one.
+    """
+    def __init__(self, message_json, team, channel, override_sender=None):
+        self.team = team
+        self.channel = channel
+        self.message_json = message_json
+        self.submessages = []
+        self.hash = None
+        if override_sender:
+            self.sender = override_sender
+            self.sender_plain = override_sender
+        else:
+            senders = self.get_sender()
+            self.sender, self.sender_plain = senders[0], senders[1]
+        self.ts = SlackTS(message_json['ts'])
+
+    def __hash__(self):
+        return hash(self.ts)
+
+    @property
+    def thread_channel(self):
+        return self.channel.thread_channels.get(self.ts)
+
+    def open_thread(self, switch=False):
+        if not self.thread_channel or not self.thread_channel.active:
+            self.channel.thread_channels[self.ts] = SlackThreadChannel(EVENTROUTER, self)
+            self.thread_channel.open()
+        if switch:
+            w.buffer_set(self.thread_channel.channel_buffer, "display", "1")
+
+    def render(self, force=False):
+        # If we already have a rendered version in the object, just return that.
+        if not force and self.message_json.get("_rendered_text"):
+            return self.message_json["_rendered_text"]
+
+        if "fallback" in self.message_json:
+            text = self.message_json["fallback"]
+        elif self.message_json.get("text"):
+            text = self.message_json["text"]
+        else:
+            text = ""
+
+        if self.message_json.get('mrkdwn', True):
+            text = render_formatting(text)
+
+        if (self.message_json.get('subtype') in ('channel_join', 'group_join') and
+                self.message_json.get('inviter')):
+            inviter_id = self.message_json.get('inviter')
+            text += " by invitation from <@{}>".format(inviter_id)
+
+        if "blocks" in self.message_json:
+            text += unfurl_blocks(self.message_json)
+
+        text = unfurl_refs(text)
+
+        if (self.message_json.get('subtype') == 'me_message' and
+                not self.message_json['text'].startswith(self.sender)):
+            text = "{} {}".format(self.sender, text)
+
+        if "edited" in self.message_json:
+            text += " " + colorize_string(config.color_edited_suffix, '(edited)')
+
+        text += unfurl_refs(unwrap_attachments(self.message_json, text))
+        text += unfurl_refs(unwrap_files(self.message_json, text))
+        text = unhtmlescape(text.lstrip().replace("\t", "    "))
+
+        text += create_reactions_string(
+                self.message_json.get("reactions", ""), self.team.myidentifier)
+
+        if self.number_of_replies():
+            self.channel.hash_message(self.ts)
+            text += " " + colorize_string(get_thread_color(self.hash), "[ Thread: {} Replies: {} ]".format(
+                    self.hash, self.number_of_replies()))
+
+        text = replace_string_with_emoji(text)
+
+        self.message_json["_rendered_text"] = text
+        return text
+
+    def change_text(self, new_text):
+        self.message_json["text"] = new_text
+        dbg(self.message_json)
+
+    def get_sender(self):
+        name = ""
+        name_plain = ""
+        user = self.team.users.get(self.message_json.get('user'))
+        if user:
+            name = "{}".format(user.formatted_name())
+            name_plain = "{}".format(user.formatted_name(enable_color=False))
+            if user.is_external:
+                name += config.external_user_suffix
+                name_plain += config.external_user_suffix
+        elif 'username' in self.message_json:
+            username = self.message_json["username"]
+            if self.message_json.get("subtype") == "bot_message":
+                name = "{} :]".format(username)
+                name_plain = "{}".format(username)
+            else:
+                name = "-{}-".format(username)
+                name_plain = "{}".format(username)
+        elif 'service_name' in self.message_json:
+            name = "-{}-".format(self.message_json["service_name"])
+            name_plain = "{}".format(self.message_json["service_name"])
+        elif self.message_json.get('bot_id') in self.team.bots:
+            name = "{} :]".format(self.team.bots[self.message_json["bot_id"]].formatted_name())
+            name_plain = "{}".format(self.team.bots[self.message_json["bot_id"]].formatted_name(enable_color=False))
+        return (name, name_plain)
+
+    def add_reaction(self, reaction, user):
+        m = self.message_json.get('reactions')
+        if m:
+            found = False
+            for r in m:
+                if r["name"] == reaction and user not in r["users"]:
+                    r["users"].append(user)
+                    found = True
+            if not found:
+                self.message_json["reactions"].append({"name": reaction, "users": [user]})
+        else:
+            self.message_json["reactions"] = [{"name": reaction, "users": [user]}]
+
+    def remove_reaction(self, reaction, user):
+        m = self.message_json.get('reactions')
+        if m:
+            for r in m:
+                if r["name"] == reaction and user in r["users"]:
+                    r["users"].remove(user)
+
+    def has_mention(self):
+        return w.string_has_highlight(unfurl_refs(self.message_json.get('text')),
+                ",".join(self.channel.highlights()))
+
+    def number_of_replies(self):
+        return max(len(self.submessages), len(self.message_json.get("replies", [])))
+
+    def notify_thread(self, action=None, sender_id=None):
+        if config.auto_open_threads:
+            self.open_thread()
+        elif sender_id != self.team.myidentifier:
+            if action == "mention":
+                template = "You were mentioned in thread {hash}, channel {channel}"
+            elif action == "participant":
+                template = "New message in thread {hash}, channel {channel} in which you participated"
+            elif action == "response":
+                template = "New message in thread {hash} in response to own message in {channel}"
+            else:
+                template = "Notification for message in thread {hash}, channel {channel}"
+            message = template.format(hash=self.hash, channel=self.channel.formatted_name())
+
+            self.team.buffer_prnt(message, message=True)
+
+class SlackThreadMessage(SlackMessage):
+
+    def __init__(self, parent_message, *args):
+        super(SlackThreadMessage, self).__init__(*args)
+        self.parent_message = parent_message
+
+
+class Hdata(object):
+    def __init__(self, w):
+        self.buffer = w.hdata_get('buffer')
+        self.line = w.hdata_get('line')
+        self.line_data = w.hdata_get('line_data')
+        self.lines = w.hdata_get('lines')
+
+
+class SlackTS(object):
+
+    def __init__(self, ts=None):
+        if ts:
+            self.major, self.minor = [int(x) for x in ts.split('.', 1)]
+        else:
+            self.major = int(time.time())
+            self.minor = 0
+
+    def __cmp__(self, other):
+        if isinstance(other, SlackTS):
+            if self.major < other.major:
+                return -1
+            elif self.major > other.major:
+                return 1
+            elif self.major == other.major:
+                if self.minor < other.minor:
+                    return -1
+                elif self.minor > other.minor:
+                    return 1
+                else:
+                    return 0
+        elif isinstance(other, str):
+            s = self.__str__()
+            if s < other:
+                return -1
+            elif s > other:
+                return 1
+            elif s == other:
+                return 0
+
+    def __lt__(self, other):
+        return self.__cmp__(other) < 0
+
+    def __le__(self, other):
+        return self.__cmp__(other) <= 0
+
+    def __eq__(self, other):
+        return self.__cmp__(other) == 0
+
+    def __ge__(self, other):
+        return self.__cmp__(other) >= 0
+
+    def __gt__(self, other):
+        return self.__cmp__(other) > 0
+
+    def __hash__(self):
+        return hash("{}.{}".format(self.major, self.minor))
+
+    def __repr__(self):
+        return str("{0}.{1:06d}".format(self.major, self.minor))
+
+    def split(self, *args, **kwargs):
+        return [self.major, self.minor]
+
+    def majorstr(self):
+        return str(self.major)
+
+    def minorstr(self):
+        return str(self.minor)
+
+###### New handlers
+
+
+def handle_rtmstart(login_data, eventrouter, team, channel, metadata):
+    """
+    This handles the main entry call to slack, rtm.start
+    """
+    metadata = login_data["wee_slack_request_metadata"]
+
+    if not login_data["ok"]:
+        w.prnt("", "ERROR: Failed connecting to Slack with token starting with {}: {}"
+               .format(metadata.token[:15], login_data["error"]))
+        if not re.match(r"^xo\w\w(-\d+){3}-[0-9a-f]+$", metadata.token):
+            w.prnt("", "ERROR: Token does not look like a valid Slack token. "
+                   "Ensure it is a valid token and not just a OAuth code.")
+
+        return
+
+    # Let's reuse a team if we have it already.
+    th = SlackTeam.generate_team_hash(login_data['self']['name'], login_data['team']['domain'])
+    if not eventrouter.teams.get(th):
+
+        users = {}
+        for item in login_data["users"]:
+            users[item["id"]] = SlackUser(login_data['team']['id'], **item)
+
+        bots = {}
+        for item in login_data["bots"]:
+            bots[item["id"]] = SlackBot(login_data['team']['id'], **item)
+
+        subteams = {}
+        for item in login_data["subteams"]["all"]:
+            is_member = item['id'] in login_data["subteams"]["self"]
+            subteams[item['id']] = SlackSubteam(
+                    login_data['team']['id'], is_member=is_member, **item)
+
+        channels = {}
+        for item in login_data["channels"]:
+            if item["is_shared"]:
+                channels[item["id"]] = SlackSharedChannel(eventrouter, **item)
+            elif item["is_private"]:
+                channels[item["id"]] = SlackPrivateChannel(eventrouter, **item)
+            else:
+                channels[item["id"]] = SlackChannel(eventrouter, **item)
+
+        for item in login_data["ims"]:
+            channels[item["id"]] = SlackDMChannel(eventrouter, users, **item)
+
+        for item in login_data["groups"]:
+            if item["is_mpim"]:
+                channels[item["id"]] = SlackMPDMChannel(eventrouter, users, login_data["self"]["id"], **item)
+            else:
+                channels[item["id"]] = SlackGroupChannel(eventrouter, **item)
+
+        self_profile = next(
+            user["profile"]
+            for user in login_data["users"]
+            if user["id"] == login_data["self"]["id"]
+        )
+        self_nick = nick_from_profile(self_profile, login_data["self"]["name"])
+
+        t = SlackTeam(
+            eventrouter,
+            metadata.token,
+            login_data['url'],
+            login_data["team"],
+            subteams,
+            self_nick,
+            login_data["self"]["id"],
+            login_data["self"]["manual_presence"],
+            users,
+            bots,
+            channels,
+            muted_channels=login_data["self"]["prefs"]["muted_channels"],
+            highlight_words=login_data["self"]["prefs"]["highlight_words"],
+        )
+        eventrouter.register_team(t)
+
+    else:
+        t = eventrouter.teams.get(th)
+        t.set_reconnect_url(login_data['url'])
+        t.connecting_rtm = False
+
+    t.connect()
+
+def handle_rtmconnect(login_data, eventrouter, team, channel, metadata):
+    metadata = login_data["wee_slack_request_metadata"]
+    team = metadata.team
+    team.connecting_rtm = False
+
+    if not login_data["ok"]:
+        w.prnt("", "ERROR: Failed reconnecting to Slack with token starting with {}: {}"
+               .format(metadata.token[:15], login_data["error"]))
+        return
+
+    team.set_reconnect_url(login_data['url'])
+    team.connect()
+
+
+def handle_emojilist(emoji_json, eventrouter, team, channel, metadata):
+    if emoji_json["ok"]:
+        team.emoji_completions.extend(emoji_json["emoji"].keys())
+
+
+def handle_channelsinfo(channel_json, eventrouter, team, channel, metadata):
+    channel.set_unread_count_display(channel_json['channel'].get('unread_count_display', 0))
+    channel.set_members(channel_json['channel']['members'])
+
+
+def handle_groupsinfo(group_json, eventrouter, team, channel, metadatas):
+    channel.set_unread_count_display(group_json['group'].get('unread_count_display', 0))
+
+
+def handle_conversationsopen(conversation_json, eventrouter, team, channel, metadata, object_name='channel'):
+    # Set unread count if the channel isn't new
+    if channel:
+        unread_count_display = conversation_json[object_name].get('unread_count_display', 0)
+        channel.set_unread_count_display(unread_count_display)
+
+
+def handle_mpimopen(mpim_json, eventrouter, team, channel, metadata, object_name='group'):
+    handle_conversationsopen(mpim_json, eventrouter, team, channel, metadata, object_name)
+
+
+def handle_history(message_json, eventrouter, team, channel, metadata):
+    if metadata['clear']:
+        channel.clear_messages()
+    channel.got_history = True
+    for message in reversed(message_json["messages"]):
+        process_message(message, eventrouter, team, channel, metadata, history_message=True)
+
+
+handle_channelshistory = handle_history
+handle_conversationshistory = handle_history
+handle_groupshistory = handle_history
+handle_imhistory = handle_history
+handle_mpimhistory = handle_history
+
+
+def handle_conversationsreplies(message_json, eventrouter, team, channel, metadata):
+    for message in message_json['messages']:
+        process_message(message, eventrouter, team, channel, metadata)
+
+
+def handle_conversationsmembers(members_json, eventrouter, team, channel, metadata):
+    if members_json['ok']:
+        channel.set_members(members_json['members'])
+    else:
+        w.prnt(team.channel_buffer, '{}Couldn\'t load members for channel {}. Error: {}'
+                .format(w.prefix('error'), channel.name, members_json['error']))
+
+
+def handle_usersinfo(user_json, eventrouter, team, channel, metadata):
+    user_info = user_json['user']
+    if not metadata.get('user'):
+        user = SlackUser(team.identifier, **user_info)
+        team.users[user_info['id']] = user
+
+    if channel.type == 'shared':
+        channel.update_nicklist(user_info['id'])
+    elif channel.type == 'im':
+        channel.slack_name = user.name
+        channel.set_topic(create_user_status_string(user.profile))
+
+
+def handle_usergroupsuserslist(users_json, eventrouter, team, channel, metadata):
+    header = 'Users in {}'.format(metadata['usergroup_handle'])
+    users = [team.users[key] for key in users_json['users']]
+    return print_users_info(team, header, users)
+
+
+def handle_usersprofileset(json, eventrouter, team, channel, metadata):
+    if not json['ok']:
+        w.prnt('', 'ERROR: Failed to set profile: {}'.format(json['error']))
+
+
+def handle_conversationsinvite(json, eventrouter, team, channel, metadata):
+    nicks = ', '.join(metadata['nicks'])
+    if json['ok']:
+        w.prnt(team.channel_buffer, 'Invited {} to {}'.format(nicks, channel.name))
+    else:
+        w.prnt(team.channel_buffer, 'ERROR: Couldn\'t invite {} to {}. Error: {}'
+                .format(nicks, channel.name, json['error']))
+
+
+def handle_chatcommand(json, eventrouter, team, channel, metadata):
+    command = '{} {}'.format(metadata['command'], metadata['command_args']).rstrip()
+    response = unfurl_refs(json['response']) if 'response' in json else ''
+    if json['ok']:
+        response_text = 'Response: {}'.format(response) if response else 'No response'
+        w.prnt(team.channel_buffer, 'Ran command "{}". {}' .format(command, response_text))
+    else:
+        response_text = '. Response: {}'.format(response) if response else ''
+        w.prnt(team.channel_buffer, 'ERROR: Couldn\'t run command "{}". Error: {}{}'
+                .format(command, json['error'], response_text))
+
+
+def handle_reactionsadd(json, eventrouter, team, channel, metadata):
+    if not json['ok']:
+        print_error("Couldn't add reaction {}: {}".format(metadata['reaction'], json['error']))
+
+
+def handle_reactionsremove(json, eventrouter, team, channel, metadata):
+    if not json['ok']:
+        print_error("Couldn't remove reaction {}: {}".format(metadata['reaction'], json['error']))
+
+
+###### New/converted process_ and subprocess_ methods
+def process_hello(message_json, eventrouter, team, channel, metadata):
+    team.subscribe_users_presence()
+
+
+def process_reconnect_url(message_json, eventrouter, team, channel, metadata):
+    team.set_reconnect_url(message_json['url'])
+
+
+def process_presence_change(message_json, eventrouter, team, channel, metadata):
+    users = [team.users[user_id] for user_id in message_json.get("users", [])]
+    if "user" in metadata:
+        users.append(metadata["user"])
+    for user in users:
+        team.update_member_presence(user, message_json["presence"])
+    if team.myidentifier in users:
+        w.bar_item_update("away")
+        w.bar_item_update("slack_away")
+
+
+def process_manual_presence_change(message_json, eventrouter, team, channel, metadata):
+    team.my_manual_presence = message_json["presence"]
+    w.bar_item_update("away")
+    w.bar_item_update("slack_away")
+
+
+def process_pref_change(message_json, eventrouter, team, channel, metadata):
+    if message_json['name'] == 'muted_channels':
+        team.set_muted_channels(message_json['value'])
+    elif message_json['name'] == 'highlight_words':
+        team.set_highlight_words(message_json['value'])
+    else:
+        dbg("Preference change not implemented: {}\n".format(message_json['name']))
+
+
+def process_user_change(message_json, eventrouter, team, channel, metadata):
+    """
+    Currently only used to update status, but lots here we could do.
+    """
+    user = metadata['user']
+    profile = message_json['user']['profile']
+    if user:
+        user.update_status(profile.get('status_emoji'), profile.get('status_text'))
+        dmchannel = team.find_channel_by_members({user.identifier}, channel_type='im')
+        if dmchannel:
+            dmchannel.set_topic(create_user_status_string(profile))
+
+
+def process_user_typing(message_json, eventrouter, team, channel, metadata):
+    if channel:
+        channel.set_typing(metadata["user"].name)
+        w.bar_item_update("slack_typing_notice")
+
+
+def process_team_join(message_json, eventrouter, team, channel, metadata):
+    user = message_json['user']
+    team.users[user["id"]] = SlackUser(team.identifier, **user)
+
+
+def process_pong(message_json, eventrouter, team, channel, metadata):
+    team.last_pong_time = time.time()
+
+
+def process_message(message_json, eventrouter, team, channel, metadata, history_message=False):
+    if SlackTS(message_json["ts"]) in channel.messages:
+        return
+
+    if "thread_ts" in message_json and "reply_count" not in message_json and "subtype" not in message_json:
+        if message_json.get("reply_broadcast"):
+            message_json["subtype"] = "thread_broadcast"
+        else:
+            message_json["subtype"] = "thread_message"
+
+    subtype = message_json.get("subtype")
+    subtype_functions = get_functions_with_prefix("subprocess_")
+
+    if subtype in subtype_functions:
+        subtype_functions[subtype](message_json, eventrouter, team, channel, history_message)
+    else:
+        message = SlackMessage(message_json, team, channel)
+        channel.store_message(message, team)
+
+        text = channel.render(message)
+        dbg("Rendered message: %s" % text)
+        dbg("Sender: %s (%s)" % (message.sender, message.sender_plain))
+
+        if subtype == 'me_message':
+            prefix = w.prefix("action").rstrip()
+        else:
+            prefix = message.sender
+
+        channel.buffer_prnt(prefix, text, message.ts, tag_nick=message.sender_plain, history_message=history_message)
+        channel.unread_count_display += 1
+        dbg("NORMAL REPLY {}".format(message_json))
+
+    if not history_message:
+        download_files(message_json, team)
+
+
+def download_files(message_json, team):
+    download_location = config.files_download_location
+    if not download_location:
+        return
+    download_location = w.string_eval_path_home(download_location, {}, {}, {})
+
+    if not os.path.exists(download_location):
+        try:
+            os.makedirs(download_location)
+        except:
+            w.prnt('', 'ERROR: Failed to create directory at files_download_location: {}'
+                    .format(format_exc_only()))
+
+    def fileout_iter(path):
+        yield path
+        main, ext = os.path.splitext(path)
+        for i in count(start=1):
+            yield main + "-{}".format(i) + ext
+
+    for f in message_json.get('files', []):
+        if f.get('mode') == 'tombstone':
+            continue
+
+        filetype = '' if f['title'].endswith(f['filetype']) else '.' + f['filetype']
+        filename = '{}_{}{}'.format(team.preferred_name, f['title'], filetype)
+        for fileout in fileout_iter(os.path.join(download_location, filename)):
+            if os.path.isfile(fileout):
+                continue
+            w.hook_process_hashtable(
+                "url:" + f['url_private'],
+                {
+                    'file_out': fileout,
+                    'httpheader': 'Authorization: Bearer ' + team.token
+                },
+                config.slack_timeout, "", "")
+            break
+
+
+def subprocess_thread_message(message_json, eventrouter, team, channel, history_message):
+    parent_ts = message_json.get('thread_ts')
+    if parent_ts:
+        parent_message = channel.messages.get(SlackTS(parent_ts))
+        if parent_message:
+            message = SlackThreadMessage(
+                parent_message, message_json, team, channel)
+            parent_message.submessages.append(message)
+            channel.hash_message(parent_ts)
+            channel.store_message(message, team)
+            channel.change_message(parent_ts)
+
+            if parent_message.thread_channel and parent_message.thread_channel.active:
+                parent_message.thread_channel.buffer_prnt(message.sender, parent_message.thread_channel.render(message), message.ts, tag_nick=message.sender_plain)
+            elif message.ts > channel.last_read and message.has_mention():
+                parent_message.notify_thread(action="mention", sender_id=message_json["user"])
+
+            if config.thread_messages_in_channel or message_json["subtype"] == "thread_broadcast":
+                thread_tag = "thread_broadcast" if message_json["subtype"] == "thread_broadcast" else "thread_message"
+                channel.buffer_prnt(
+                    message.sender,
+                    channel.render(message),
+                    message.ts,
+                    tag_nick=message.sender_plain,
+                    history_message=history_message,
+                    extra_tags=[thread_tag],
+                )
+
+
+subprocess_thread_broadcast = subprocess_thread_message
+
+
+def subprocess_channel_join(message_json, eventrouter, team, channel, history_message):
+    prefix_join = w.prefix("join").strip()
+    message = SlackMessage(message_json, team, channel, override_sender=prefix_join)
+    channel.buffer_prnt(prefix_join, channel.render(message), message_json["ts"], tagset='join', tag_nick=message.get_sender()[1], history_message=history_message)
+    channel.user_joined(message_json['user'])
+    channel.store_message(message, team)
+
+
+def subprocess_channel_leave(message_json, eventrouter, team, channel, history_message):
+    prefix_leave = w.prefix("quit").strip()
+    message = SlackMessage(message_json, team, channel, override_sender=prefix_leave)
+    channel.buffer_prnt(prefix_leave, channel.render(message), message_json["ts"], tagset='leave', tag_nick=message.get_sender()[1], history_message=history_message)
+    channel.user_left(message_json['user'])
+    channel.store_message(message, team)
+
+
+def subprocess_channel_topic(message_json, eventrouter, team, channel, history_message):
+    prefix_topic = w.prefix("network").strip()
+    message = SlackMessage(message_json, team, channel, override_sender=prefix_topic)
+    channel.buffer_prnt(prefix_topic, channel.render(message), message_json["ts"], tagset="topic", tag_nick=message.get_sender()[1], history_message=history_message)
+    channel.set_topic(message_json["topic"])
+    channel.store_message(message, team)
+
+
+subprocess_group_join = subprocess_channel_join
+subprocess_group_leave = subprocess_channel_leave
+subprocess_group_topic = subprocess_channel_topic
+
+
+def subprocess_message_replied(message_json, eventrouter, team, channel, history_message):
+    parent_ts = message_json["message"].get("thread_ts")
+    parent_message = channel.messages.get(SlackTS(parent_ts))
+    # Thread exists but is not open yet
+    if parent_message is not None \
+            and not (parent_message.thread_channel and parent_message.thread_channel.active):
+        channel.hash_message(parent_ts)
+        last_message = max(message_json["message"]["replies"], key=lambda x: x["ts"])
+        if message_json["message"].get("user") == team.myidentifier:
+            parent_message.notify_thread(action="response", sender_id=last_message["user"])
+        elif any(team.myidentifier == r["user"] for r in message_json["message"]["replies"]):
+            parent_message.notify_thread(action="participant", sender_id=last_message["user"])
+
+
+def subprocess_message_changed(message_json, eventrouter, team, channel, history_message):
+    new_message = message_json.get("message")
+    channel.change_message(new_message["ts"], message_json=new_message)
+
+
+def subprocess_message_deleted(message_json, eventrouter, team, channel, history_message):
+    message = colorize_string(config.color_deleted, '(deleted)')
+    channel.change_message(message_json["deleted_ts"], text=message)
+
+
+def process_reply(message_json, eventrouter, team, channel, metadata):
+    reply_to = int(message_json["reply_to"])
+    original_message_json = team.ws_replies.pop(reply_to, None)
+    if original_message_json:
+        original_message_json.update(message_json)
+        channel = team.channels[original_message_json.get('channel')]
+        process_message(original_message_json, eventrouter, team=team, channel=channel, metadata={})
+        dbg("REPLY {}".format(message_json))
+    else:
+        dbg("Unexpected reply {}".format(message_json))
+
+
+def process_channel_marked(message_json, eventrouter, team, channel, metadata):
+    ts = message_json.get("ts")
+    if ts:
+        channel.mark_read(ts=ts, force=True, update_remote=False)
+    else:
+        dbg("tried to mark something weird {}".format(message_json))
+
+
+process_group_marked = process_channel_marked
+process_im_marked = process_channel_marked
+process_mpim_marked = process_channel_marked
+
+
+def process_channel_joined(message_json, eventrouter, team, channel, metadata):
+    channel.update_from_message_json(message_json["channel"])
+    channel.open()
+
+
+def process_channel_created(message_json, eventrouter, team, channel, metadata):
+    item = message_json["channel"]
+    item['is_member'] = False
+    channel = SlackChannel(eventrouter, team=team, **item)
+    team.channels[item["id"]] = channel
+    team.buffer_prnt('Channel created: {}'.format(channel.slack_name))
+
+
+def process_channel_rename(message_json, eventrouter, team, channel, metadata):
+    channel.slack_name = message_json['channel']['name']
+
+
+def process_im_created(message_json, eventrouter, team, channel, metadata):
+    item = message_json["channel"]
+    channel = SlackDMChannel(eventrouter, team=team, users=team.users, **item)
+    team.channels[item["id"]] = channel
+    team.buffer_prnt('IM channel created: {}'.format(channel.name))
+
+
+def process_im_open(message_json, eventrouter, team, channel, metadata):
+    channel.check_should_open(True)
+    w.buffer_set(channel.channel_buffer, "hotlist", "2")
+
+
+def process_im_close(message_json, eventrouter, team, channel, metadata):
+    if channel.channel_buffer:
+        w.prnt(team.channel_buffer,
+                'IM {} closed by another client or the server'.format(channel.name))
+    eventrouter.weechat_controller.unregister_buffer(channel.channel_buffer, False, True)
+
+
+def process_group_joined(message_json, eventrouter, team, channel, metadata):
+    item = message_json["channel"]
+    if item["name"].startswith("mpdm-"):
+        channel = SlackMPDMChannel(eventrouter, team.users, team.myidentifier, team=team, **item)
+    else:
+        channel = SlackGroupChannel(eventrouter, team=team, **item)
+    team.channels[item["id"]] = channel
+    channel.open()
+
+
+def process_reaction_added(message_json, eventrouter, team, channel, metadata):
+    channel = team.channels.get(message_json["item"].get("channel"))
+    if message_json["item"].get("type") == "message":
+        ts = SlackTS(message_json['item']["ts"])
+
+        message = channel.messages.get(ts)
+        if message:
+            message.add_reaction(message_json["reaction"], message_json["user"])
+            channel.change_message(ts)
+    else:
+        dbg("reaction to item type not supported: " + str(message_json))
+
+
+def process_reaction_removed(message_json, eventrouter, team, channel, metadata):
+    channel = team.channels.get(message_json["item"].get("channel"))
+    if message_json["item"].get("type") == "message":
+        ts = SlackTS(message_json['item']["ts"])
+
+        message = channel.messages.get(ts)
+        if message:
+            message.remove_reaction(message_json["reaction"], message_json["user"])
+            channel.change_message(ts)
+    else:
+        dbg("Reaction to item type not supported: " + str(message_json))
+
+
+def process_subteam_created(subteam_json, eventrouter, team, channel, metadata):
+    subteam_json_info = subteam_json['subteam']
+    is_member = team.myidentifier in subteam_json_info.get('users', [])
+    subteam = SlackSubteam(team.identifier, is_member=is_member, **subteam_json_info)
+    team.subteams[subteam_json_info['id']] = subteam
+
+
+def process_subteam_updated(subteam_json, eventrouter, team, channel, metadata):
+    current_subteam_info = team.subteams[subteam_json['subteam']['id']]
+    is_member = team.myidentifier in subteam_json['subteam'].get('users', [])
+    new_subteam_info = SlackSubteam(team.identifier, is_member=is_member, **subteam_json['subteam'])
+    team.subteams[subteam_json['subteam']['id']] = new_subteam_info
+
+    if current_subteam_info.is_member != new_subteam_info.is_member:
+        for channel in team.channels.values():
+            channel.set_highlights()
+
+    if config.notify_usergroup_handle_updated and current_subteam_info.handle != new_subteam_info.handle:
+        message = 'User group {old_handle} has updated its handle to {new_handle} in team {team}.'.format(
+            name=current_subteam_info.handle, handle=new_subteam_info.handle, team=team.preferred_name)
+        team.buffer_prnt(message, message=True)
+
+
+def process_emoji_changed(message_json, eventrouter, team, channel, metadata):
+    team.load_emoji_completions()
+
+
+###### New module/global methods
+def render_formatting(text):
+    text = re.sub(r'(^| )\*([^*\n`]+)\*(?=[^\w]|$)',
+                  r'\1{}*\2*{}'.format(w.color(config.render_bold_as),
+                                       w.color('-' + config.render_bold_as)),
+                  text,
+                  flags=re.UNICODE)
+    text = re.sub(r'(^| )_([^_\n`]+)_(?=[^\w]|$)',
+                  r'\1{}_\2_{}'.format(w.color(config.render_italic_as),
+                                       w.color('-' + config.render_italic_as)),
+                  text,
+                  flags=re.UNICODE)
+    return text
+
+
+def linkify_text(message, team, only_users=False):
+    # The get_username_map function is a bit heavy, but this whole
+    # function is only called on message send..
+    usernames = team.get_username_map()
+    channels = team.get_channel_map()
+    usergroups = team.generate_usergroup_map()
+    message_escaped = (message
+        # Replace IRC formatting chars with Slack formatting chars.
+        .replace('\x02', '*')
+        .replace('\x1D', '_')
+        .replace('\x1F', config.map_underline_to)
+        # Escape chars that have special meaning to Slack. Note that we do not
+        # (and should not) perform full HTML entity-encoding here.
+        # See https://api.slack.com/docs/message-formatting for details.
+        .replace('&', '&amp;')
+        .replace('<', '&lt;')
+        .replace('>', '&gt;'))
+
+    def linkify_word(match):
+        word = match.group(0)
+        prefix, name = match.groups()
+        if prefix == "@":
+            if name in ["channel", "everyone", "group", "here"]:
+                return "<!{}>".format(name)
+            elif name in usernames:
+                return "<@{}>".format(usernames[name])
+            elif word in usergroups.keys():
+                return "<!subteam^{}|{}>".format(usergroups[word], word)
+        elif prefix == "#" and not only_users:
+            if word in channels:
+                return "<#{}|{}>".format(channels[word], name)
+        return word
+
+    linkify_regex = r'(?:^|(?<=\s))([@#])([\w\(\)\'.-]+)'
+    return re.sub(linkify_regex, linkify_word, message_escaped, flags=re.UNICODE)
+
+
+def unfurl_blocks(message_json):
+    block_text = [""]
+    for block in message_json["blocks"]:
+        try:
+            if block["type"] == "section":
+                fields = block.get("fields", [])
+                if "text" in block:
+                    fields.insert(0, block["text"])
+                block_text.extend(unfurl_block_element(field) for field in fields)
+            elif block["type"] == "actions":
+                elements = []
+                for element in block["elements"]:
+                    if element["type"] == "button":
+                        elements.append(unfurl_block_element(element["text"]))
+                    else:
+                        elements.append(colorize_string(config.color_deleted,
+                            '<<Unsupported block action type "{}">>'.format(element["type"])))
+                block_text.append(" | ".join(elements))
+            elif block["type"] == "call":
+                block_text.append("Join via " + block["call"]["v1"]["join_url"])
+            elif block["type"] == "divider":
+                block_text.append("---")
+            elif block["type"] == "context":
+                block_text.append(" | ".join(unfurl_block_element(el) for el in block["elements"]))
+            elif block["type"] == "image":
+                if "title" in block:
+                    block_text.append(unfurl_block_element(block["title"]))
+                block_text.append(unfurl_block_element(block))
+            elif block["type"] == "rich_text":
+                continue
+            else:
+                block_text.append(colorize_string(config.color_deleted,
+                    '<<Unsupported block type "{}">>'.format(block["type"])))
+                dbg('Unsupported block: "{}"'.format(json.dumps(block)), level=4)
+        except Exception as e:
+            dbg("Failed to unfurl block ({}): {}".format(repr(e), json.dumps(block)), level=4)
+    return "\n".join(block_text)
+
+
+def unfurl_block_element(text):
+    if text["type"] == "mrkdwn":
+        return render_formatting(text["text"])
+    elif text["type"] == "plain_text":
+        return text["text"]
+    elif text["type"] == "image":
+        return "{} ({})".format(text["image_url"], text["alt_text"])
+
+
+def unfurl_refs(text):
+    """
+    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>
+    #  - <!subteam^U2147483697|@group>
+    # Test patterns lives in ./_pytest/test_unfurl.py
+
+    def unfurl_ref(match):
+        ref, fallback = match.groups()
+
+        resolved_ref = resolve_ref(ref)
+        if resolved_ref != ref:
+            return resolved_ref
+
+        if fallback and not config.unfurl_ignore_alt_text:
+            if ref.startswith("#"):
+                return "#{}".format(fallback)
+            elif ref.startswith("@"):
+                return fallback
+            elif ref.startswith("!subteam"):
+                prefix = "@" if not fallback.startswith("@") else ""
+                return prefix + fallback
+            elif ref.startswith("!date"):
+                return fallback
+            else:
+                match_url = r"^\w+:(//)?{}$".format(re.escape(fallback))
+                url_matches_desc = re.match(match_url, ref)
+                if url_matches_desc and config.unfurl_auto_link_display == "text":
+                    return fallback
+                elif url_matches_desc and config.unfurl_auto_link_display == "url":
+                    return ref
+                else:
+                    return "{} ({})".format(ref, fallback)
+        return ref
+
+    return re.sub(r"<([^|>]*)(?:\|([^>]*))?>", unfurl_ref, text)
+
+
+def unhtmlescape(text):
+    return text.replace("&lt;", "<") \
+               .replace("&gt;", ">") \
+               .replace("&amp;", "&")
+
+
+def unwrap_attachments(message_json, text_before):
+    text_before_unescaped = unhtmlescape(text_before)
+    attachment_texts = []
+    a = message_json.get("attachments")
+    if a:
+        if text_before:
+            attachment_texts.append('')
+        for attachment in a:
+            # 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'])
+            title = attachment.get('title')
+            title_link = attachment.get('title_link', '')
+            if title_link in text_before_unescaped:
+                title_link = ''
+            if title and title_link:
+                t.append('%s%s (%s)' % (prepend_title_text, title, title_link,))
+                prepend_title_text = ''
+            elif title and not title_link:
+                t.append('%s%s' % (prepend_title_text, title,))
+                prepend_title_text = ''
+            from_url = attachment.get('from_url', '')
+            if from_url not in text_before_unescaped and from_url != title_link:
+                t.append(from_url)
+
+            atext = attachment.get("text")
+            if atext:
+                tx = re.sub(r' *\n[\n ]+', '\n', atext)
+                t.append(prepend_title_text + tx)
+                prepend_title_text = ''
+
+            image_url = attachment.get('image_url', '')
+            if image_url not in text_before_unescaped and image_url != title_link:
+                t.append(image_url)
+
+            fields = attachment.get("fields")
+            if fields:
+                for f in fields:
+                    if f.get('title'):
+                        t.append('%s %s' % (f['title'], f['value'],))
+                    else:
+                        t.append(f['value'])
+            fallback = attachment.get("fallback")
+            if t == [] and fallback:
+                t.append(fallback)
+            attachment_texts.append("\n".join([x.strip() for x in t if x]))
+    return "\n".join(attachment_texts)
+
+
+def unwrap_files(message_json, text_before):
+    files_texts = []
+    for f in message_json.get('files', []):
+        if f.get('mode', '') != 'tombstone':
+            text = '{} ({})'.format(f['url_private'], f['title'])
+        else:
+            text = colorize_string(config.color_deleted, '(This file was deleted.)')
+        files_texts.append(text)
+
+    if text_before:
+        files_texts.insert(0, '')
+    return "\n".join(files_texts)
+
+
+def resolve_ref(ref):
+    if ref in ['!channel', '!everyone', '!group', '!here']:
+        return ref.replace('!', '@')
+    for team in EVENTROUTER.teams.values():
+        if ref.startswith('@'):
+            user = team.users.get(ref[1:])
+            if user:
+                suffix = config.external_user_suffix if user.is_external else ''
+                return '@{}{}'.format(user.name, suffix)
+        elif ref.startswith('#'):
+            channel = team.channels.get(ref[1:])
+            if channel:
+                return channel.name
+        elif ref.startswith('!subteam'):
+            _, subteam_id = ref.split('^')
+            subteam = team.subteams.get(subteam_id)
+            if subteam:
+                return subteam.handle
+        elif ref.startswith("!date"):
+            parts = ref.split('^')
+            ref_datetime = datetime.fromtimestamp(int(parts[1]))
+            link_suffix = ' ({})'.format(parts[3]) if len(parts) > 3 else ''
+            token_to_format = {
+                    'date_num': '%Y-%m-%d',
+                    'date': '%B %d, %Y',
+                    'date_short': '%b %d, %Y',
+                    'date_long': '%A, %B %d, %Y',
+                    'time': '%H:%M',
+                    'time_secs': '%H:%M:%S'
+            }
+
+            def replace_token(match):
+                token = match.group(1)
+                if token.startswith('date_') and token.endswith('_pretty'):
+                    if ref_datetime.date() == date.today():
+                        return 'today'
+                    elif ref_datetime.date() == date.today() - timedelta(days=1):
+                        return 'yesterday'
+                    elif ref_datetime.date() == date.today() + timedelta(days=1):
+                        return 'tomorrow'
+                    else:
+                        token = token.replace('_pretty', '')
+                if token in token_to_format:
+                    return ref_datetime.strftime(token_to_format[token])
+                else:
+                    return match.group(0)
+
+            return re.sub(r"{([^}]+)}", replace_token, parts[2]) + link_suffix
+
+    # Something else, just return as-is
+    return ref
+
+
+def create_user_status_string(profile):
+    real_name = profile.get("real_name")
+    status_emoji = replace_string_with_emoji(profile.get("status_emoji", ""))
+    status_text = profile.get("status_text")
+    if status_emoji or status_text:
+        return "{} | {} {}".format(real_name, status_emoji, status_text)
+    else:
+        return real_name
+
+
+def create_reaction_string(reaction, myidentifier):
+    if config.show_reaction_nicks:
+        nicks = [resolve_ref('@{}'.format(user)) for user in reaction['users']]
+        users = '({})'.format(','.join(nicks))
+    else:
+        users = len(reaction['users'])
+    reaction_string = ':{}:{}'.format(reaction['name'], users)
+    if myidentifier in reaction['users']:
+        return colorize_string(config.color_reaction_suffix_added_by_you, reaction_string,
+                reset_color=config.color_reaction_suffix)
+    else:
+        return reaction_string
+
+
+def create_reactions_string(reactions, myidentifier):
+    reactions_with_users = [r for r in reactions if len(r['users']) > 0]
+    reactions_string = ' '.join(create_reaction_string(r, myidentifier) for r in reactions_with_users)
+    if reactions_string:
+        return ' ' + colorize_string(config.color_reaction_suffix, '[{}]'.format(reactions_string))
+    else:
+        return ''
+
+
+def hdata_line_ts(line_pointer):
+    data = w.hdata_pointer(hdata.line, line_pointer, 'data')
+    ts_major = w.hdata_time(hdata.line_data, data, 'date')
+    ts_minor = w.hdata_time(hdata.line_data, data, 'date_printed')
+    return (ts_major, ts_minor)
+
+
+def modify_buffer_line(buffer_pointer, ts, new_text):
+    own_lines = w.hdata_pointer(hdata.buffer, buffer_pointer, 'own_lines')
+    line_pointer = w.hdata_pointer(hdata.lines, own_lines, 'last_line')
+
+    # Find the last line with this ts
+    while line_pointer and hdata_line_ts(line_pointer) != (ts.major, ts.minor):
+        line_pointer = w.hdata_move(hdata.line, line_pointer, -1)
+
+    # Find all lines for the message
+    pointers = []
+    while line_pointer and hdata_line_ts(line_pointer) == (ts.major, ts.minor):
+        pointers.append(line_pointer)
+        line_pointer = w.hdata_move(hdata.line, line_pointer, -1)
+    pointers.reverse()
+
+    # Split the message into at most the number of existing lines as we can't insert new lines
+    lines = new_text.split('\n', len(pointers) - 1)
+    # Replace newlines to prevent garbled lines in bare display mode
+    lines = [line.replace('\n', ' | ') for line in lines]
+    # Extend lines in case the new message is shorter than the old as we can't delete lines
+    lines += [''] * (len(pointers) - len(lines))
+
+    for pointer, line in zip(pointers, lines):
+        data = w.hdata_pointer(hdata.line, pointer, 'data')
+        w.hdata_update(hdata.line_data, data, {"message": line})
+
+    return w.WEECHAT_RC_OK
+
+
+def modify_last_print_time(buffer_pointer, ts_minor):
+    """
+    This overloads the time printed field to let us store the slack
+    per message unique id that comes after the "." in a slack ts
+    """
+    own_lines = w.hdata_pointer(hdata.buffer, buffer_pointer, 'own_lines')
+    line_pointer = w.hdata_pointer(hdata.lines, own_lines, 'last_line')
+
+    while line_pointer:
+        data = w.hdata_pointer(hdata.line, line_pointer, 'data')
+        w.hdata_update(hdata.line_data, data, {"date_printed": str(ts_minor)})
+
+        if w.hdata_string(hdata.line_data, data, 'prefix'):
+            # Reached the first line of the message, so stop here
+            break
+
+        # Move one line backwards so all lines of the message are set
+        line_pointer = w.hdata_move(hdata.line, line_pointer, -1)
+
+    return w.WEECHAT_RC_OK
+
+
+def nick_from_profile(profile, username):
+    full_name = profile.get('real_name') or username
+    if config.use_full_names:
+        nick = full_name
+    else:
+        nick = profile.get('display_name') or full_name
+    return nick.replace(' ', '')
+
+
+def format_nick(nick, previous_nick=None):
+    if nick == previous_nick:
+        nick = w.config_string(w.config_get('weechat.look.prefix_same_nick')) or nick
+    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_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'))
+    return colorize_string(nick_prefix_color_name, nick_prefix) + nick + colorize_string(nick_suffix_color_name, nick_suffix)
+
+
+def tag(tagset=None, user=None, self_msg=False, backlog=False, no_log=False, extra_tags=None):
+    tagsets = {
+        "team_info": {"no_highlight", "log3"},
+        "team_message": {"irc_privmsg", "notify_message", "log1"},
+        "dm": {"irc_privmsg", "notify_private", "log1"},
+        "join": {"irc_join", "no_highlight", "log4"},
+        "leave": {"irc_part", "no_highlight", "log4"},
+        "topic": {"irc_topic", "no_highlight", "log3"},
+        "channel": {"irc_privmsg", "notify_message", "log1"},
+    }
+    nick_tag = {"nick_{}".format(user).replace(" ", "_")} if user else set()
+    slack_tag = {"slack_{}".format(tagset or "default")}
+    tags = nick_tag | slack_tag | tagsets.get(tagset, set())
+    if self_msg or backlog:
+        tags -= {"notify_highlight", "notify_message", "notify_private"}
+        tags |= {"notify_none", "no_highlight"}
+        if self_msg:
+            tags |= {"self_msg"}
+        if backlog:
+            tags |= {"logger_backlog"}
+    if no_log:
+        tags |= {"no_log"}
+        tags = {tag for tag in tags if not tag.startswith("log") or tag == "logger_backlog"}
+    if extra_tags:
+        tags |= set(extra_tags)
+    return ",".join(tags)
+
+
+def set_own_presence_active(team):
+    slackbot = team.get_channel_map()['Slackbot']
+    channel = team.channels[slackbot]
+    request = {"type": "typing", "channel": channel.identifier}
+    channel.team.send_to_websocket(request, expect_reply=False)
+
+
+###### New/converted command_ commands
+
+
+@slack_buffer_or_ignore
+@utf8_decode
+def invite_command_cb(data, current_buffer, args):
+    team = EVENTROUTER.weechat_controller.buffers[current_buffer].team
+    split_args = args.split()[1:]
+    if not split_args:
+        w.prnt('', 'Too few arguments for command "/invite" (help on command: /help invite)')
+        return w.WEECHAT_RC_OK_EAT
+
+    if split_args[-1].startswith("#") or split_args[-1].startswith(config.group_name_prefix):
+        nicks = split_args[:-1]
+        channel = team.channels.get(team.get_channel_map().get(split_args[-1]))
+        if not nicks or not channel:
+            w.prnt('', '{}: No such nick/channel'.format(split_args[-1]))
+            return w.WEECHAT_RC_OK_EAT
+    else:
+        nicks = split_args
+        channel = EVENTROUTER.weechat_controller.buffers[current_buffer]
+
+    all_users = team.get_username_map()
+    users = set()
+    for nick in nicks:
+        user = all_users.get(nick.lstrip('@'))
+        if not user:
+            w.prnt('', 'ERROR: Unknown user: {}'.format(nick))
+            return w.WEECHAT_RC_OK_EAT
+        users.add(user)
+
+    s = SlackRequest(team, "conversations.invite", {"channel": channel.identifier, "users": ",".join(users)},
+            channel=channel, metadata={"nicks": nicks})
+    EVENTROUTER.receive(s)
+    return w.WEECHAT_RC_OK_EAT
+
+
+@slack_buffer_or_ignore
+@utf8_decode
+def part_command_cb(data, current_buffer, args):
+    e = EVENTROUTER
+    args = args.split()
+    if len(args) > 1:
+        team = e.weechat_controller.buffers[current_buffer].team
+        cmap = team.get_channel_map()
+        channel = "".join(args[1:])
+        if channel in cmap:
+            buffer_ptr = team.channels[cmap[channel]].channel_buffer
+            e.weechat_controller.unregister_buffer(buffer_ptr, update_remote=True, close_buffer=True)
+        else:
+            w.prnt(team.channel_buffer, "{}: No such channel".format(channel))
+    else:
+        e.weechat_controller.unregister_buffer(current_buffer, update_remote=True, close_buffer=True)
+    return w.WEECHAT_RC_OK_EAT
+
+
+def parse_topic_command(command):
+    args = command.split()[1:]
+    channel_name = None
+    topic = None
+
+    if args:
+        if args[0].startswith('#'):
+            channel_name = args[0]
+            topic = args[1:]
+        else:
+            topic = args
+
+    if topic == []:
+        topic = None
+    if topic:
+        topic = ' '.join(topic)
+    if topic == '-delete':
+        topic = ''
+
+    return channel_name, topic
+
+
+@slack_buffer_or_ignore
+@utf8_decode
+def topic_command_cb(data, current_buffer, command):
+    """
+    Change the topic of a channel
+    /topic [<channel>] [<topic>|-delete]
+    """
+    channel_name, topic = parse_topic_command(command)
+    team = EVENTROUTER.weechat_controller.buffers[current_buffer].team
+
+    if channel_name:
+        channel = team.channels.get(team.get_channel_map().get(channel_name))
+    else:
+        channel = EVENTROUTER.weechat_controller.buffers[current_buffer]
+
+    if not channel:
+        w.prnt(team.channel_buffer, "{}: No such channel".format(channel_name))
+        return w.WEECHAT_RC_OK_EAT
+
+    if topic is None:
+        w.prnt(channel.channel_buffer,
+                'Topic for {} is "{}"'.format(channel.name, channel.render_topic()))
+    else:
+        s = SlackRequest(team, "conversations.setTopic",
+                {"channel": channel.identifier, "topic": linkify_text(topic, team)}, channel=channel)
+        EVENTROUTER.receive(s)
+    return w.WEECHAT_RC_OK_EAT
+
+
+@slack_buffer_or_ignore
+@utf8_decode
+def whois_command_cb(data, current_buffer, command):
+    """
+    Get real name of user
+    /whois <nick>
+    """
+    args = command.split()
+    if len(args) < 2:
+        w.prnt(current_buffer, "Not enough arguments")
+        return w.WEECHAT_RC_OK_EAT
+    user = args[1]
+    if (user.startswith('@')):
+        user = user[1:]
+    team = EVENTROUTER.weechat_controller.buffers[current_buffer].team
+    u = team.users.get(team.get_username_map().get(user))
+    if u:
+        def print_profile(field):
+            value = u.profile.get(field)
+            if value:
+                team.buffer_prnt("[{}]: {}: {}".format(user, field, value))
+
+        team.buffer_prnt("[{}]: {}".format(user, u.real_name))
+        status_emoji = replace_string_with_emoji(u.profile.get("status_emoji", ""))
+        status_text = u.profile.get("status_text", "")
+        if status_emoji or status_text:
+            team.buffer_prnt("[{}]: {} {}".format(user, status_emoji, status_text))
+
+        team.buffer_prnt("[{}]: username: {}".format(user, u.username))
+        team.buffer_prnt("[{}]: id: {}".format(user, u.identifier))
+
+        print_profile('title')
+        print_profile('email')
+        print_profile('phone')
+        print_profile('skype')
+    else:
+        team.buffer_prnt("[{}]: No such user".format(user))
+    return w.WEECHAT_RC_OK_EAT
+
+
+@slack_buffer_or_ignore
+@utf8_decode
+def me_command_cb(data, current_buffer, args):
+    channel = EVENTROUTER.weechat_controller.buffers[current_buffer]
+    message = args.split(' ', 1)[1]
+    channel.send_message(message, subtype='me_message')
+    return w.WEECHAT_RC_OK_EAT
+
+
+@utf8_decode
+def command_register(data, current_buffer, args):
+    """
+    /slack register [code]
+    Register a Slack team in wee-slack.
+    """
+    CLIENT_ID = "2468770254.51917335286"
+    CLIENT_SECRET = "dcb7fe380a000cba0cca3169a5fe8d70"  # Not really a secret.
+    REDIRECT_URI = "https%3A%2F%2Fwee-slack.github.io%2Fwee-slack%2Foauth%23"
+    if not args:
+        message = textwrap.dedent("""
+            ### Connecting to a Slack team with OAuth ###
+            1) Paste this link into a browser: https://slack.com/oauth/authorize?client_id={}&scope=client&redirect_uri={}
+            2) Select the team you wish to access from wee-slack in your browser. If you want to add multiple teams, you will have to repeat this whole process for each team.
+            3) Click "Authorize" in the browser.
+               If you get a message saying you are not authorized to install wee-slack, the team has restricted Slack app installation and you will have to request it from an admin. To do that, go to https://my.slack.com/apps/A1HSZ9V8E-wee-slack and click "Request to Install".
+            4) The web page will show a command in the form `/slack register <code>`. Run this command in weechat.
+        """).strip().format(CLIENT_ID, REDIRECT_URI)
+        w.prnt("", message)
+        return w.WEECHAT_RC_OK_EAT
+
+    uri = (
+        "https://slack.com/api/oauth.access?"
+        "client_id={}&client_secret={}&redirect_uri={}&code={}"
+    ).format(CLIENT_ID, CLIENT_SECRET, REDIRECT_URI, args)
+    params = {'useragent': 'wee_slack {}'.format(SCRIPT_VERSION)}
+    w.hook_process_hashtable('url:', params, config.slack_timeout, "", "")
+    w.hook_process_hashtable("url:{}".format(uri), params, config.slack_timeout, "register_callback", "")
+    return w.WEECHAT_RC_OK_EAT
+
+
+@utf8_decode
+def register_callback(data, command, return_code, out, err):
+    if return_code != 0:
+        w.prnt("", "ERROR: problem when trying to get Slack OAuth token. Got return code {}. Err: {}".format(return_code, err))
+        w.prnt("", "Check the network or proxy settings")
+        return w.WEECHAT_RC_OK_EAT
+
+    if len(out) <= 0:
+        w.prnt("", "ERROR: problem when trying to get Slack OAuth token. Got 0 length answer. Err: {}".format(err))
+        w.prnt("", "Check the network or proxy settings")
+        return w.WEECHAT_RC_OK_EAT
+
+    d = json.loads(out)
+    if not d["ok"]:
+        w.prnt("",
+               "ERROR: Couldn't get Slack OAuth token: {}".format(d['error']))
+        return w.WEECHAT_RC_OK_EAT
+
+    if config.is_default('slack_api_token'):
+        w.config_set_plugin('slack_api_token', d['access_token'])
+    else:
+        # Add new token to existing set, joined by comma.
+        tok = config.get_string('slack_api_token')
+        w.config_set_plugin('slack_api_token',
+                            ','.join([tok, d['access_token']]))
+
+    w.prnt("", "Success! Added team \"%s\"" % (d['team_name'],))
+    w.prnt("", "Please reload wee-slack with: /python reload slack")
+    w.prnt("", "If you want to add another team you can repeat this process from step 1 before reloading wee-slack.")
+    return w.WEECHAT_RC_OK_EAT
+
+
+@slack_buffer_or_ignore
+@utf8_decode
+def msg_command_cb(data, current_buffer, args):
+    aargs = args.split(None, 2)
+    who = aargs[1].lstrip('@')
+    if who == "*":
+        who = EVENTROUTER.weechat_controller.buffers[current_buffer].name
+    else:
+        join_query_command_cb(data, current_buffer, '/query ' + who)
+
+    if len(aargs) > 2:
+        message = aargs[2]
+        team = EVENTROUTER.weechat_controller.buffers[current_buffer].team
+        cmap = team.get_channel_map()
+        if who in cmap:
+            channel = team.channels[cmap[who]]
+            channel.send_message(message)
+    return w.WEECHAT_RC_OK_EAT
+
+
+def print_team_items_info(team, header, items, extra_info_function):
+    team.buffer_prnt("{}:".format(header))
+    if items:
+        max_name_length = max(len(item.name) for item in items)
+        for item in sorted(items, key=lambda item: item.name.lower()):
+            extra_info = extra_info_function(item)
+            team.buffer_prnt("    {:<{}}({})".format(item.name, max_name_length + 2, extra_info))
+    return w.WEECHAT_RC_OK_EAT
+
+
+def print_users_info(team, header, users):
+    def extra_info_function(user):
+        external_text = ", external" if user.is_external else ""
+        return user.presence + external_text
+    return print_team_items_info(team, header, users, extra_info_function)
+
+
+@slack_buffer_required
+@utf8_decode
+def command_teams(data, current_buffer, args):
+    """
+    /slack teams
+    List the connected Slack teams.
+    """
+    team = EVENTROUTER.weechat_controller.buffers[current_buffer].team
+    teams = EVENTROUTER.teams.values()
+    extra_info_function = lambda team: "token: {}...".format(team.token[:15])
+    return print_team_items_info(team, "Slack teams", teams, extra_info_function)
+
+
+@slack_buffer_required
+@utf8_decode
+def command_channels(data, current_buffer, args):
+    """
+    /slack channels
+    List the channels in the current team.
+    """
+    team = EVENTROUTER.weechat_controller.buffers[current_buffer].team
+    channels = [channel for channel in team.channels.values() if channel.type not in ['im', 'mpim']]
+    def extra_info_function(channel):
+        if channel.active:
+            return "member"
+        elif getattr(channel, "is_archived", None):
+            return "archived"
+        else:
+            return "not a member"
+    return print_team_items_info(team, "Channels", channels, extra_info_function)
+
+
+@slack_buffer_required
+@utf8_decode
+def command_users(data, current_buffer, args):
+    """
+    /slack users
+    List the users in the current team.
+    """
+    team = EVENTROUTER.weechat_controller.buffers[current_buffer].team
+    return print_users_info(team, "Users", team.users.values())
+
+
+@slack_buffer_required
+@utf8_decode
+def command_usergroups(data, current_buffer, args):
+    """
+    /slack usergroups [handle]
+    List the usergroups in the current team
+    If handle is given show the members in the usergroup
+    """
+    team = EVENTROUTER.weechat_controller.buffers[current_buffer].team
+    usergroups = team.generate_usergroup_map()
+    usergroup_key = usergroups.get(args)
+
+    if usergroup_key:
+        s = SlackRequest(team, "usergroups.users.list", {"usergroup": usergroup_key},
+                metadata={'usergroup_handle': args})
+        EVENTROUTER.receive(s)
+    elif args:
+        w.prnt('', 'ERROR: Unknown usergroup handle: {}'.format(args))
+        return w.WEECHAT_RC_ERROR
+    else:
+        def extra_info_function(subteam):
+            is_member = 'member' if subteam.is_member else 'not a member'
+            return '{}, {}'.format(subteam.handle, is_member)
+        return print_team_items_info(team, "Usergroups", team.subteams.values(), extra_info_function)
+    return w.WEECHAT_RC_OK_EAT
+
+command_usergroups.completion = '%(usergroups)'
+
+
+@slack_buffer_required
+@utf8_decode
+def command_talk(data, current_buffer, args):
+    """
+    /slack talk <user>[,<user2>[,<user3>...]]
+    Open a chat with the specified user(s).
+    """
+    if not args:
+        w.prnt('', 'Usage: /slack talk <user>[,<user2>[,<user3>...]]')
+        return w.WEECHAT_RC_ERROR
+    return join_query_command_cb(data, current_buffer, '/query ' + args)
+
+command_talk.completion = '%(nicks)'
+
+
+@slack_buffer_or_ignore
+@utf8_decode
+def join_query_command_cb(data, current_buffer, args):
+    team = EVENTROUTER.weechat_controller.buffers[current_buffer].team
+    split_args = args.split(' ', 1)
+    if len(split_args) < 2 or not split_args[1]:
+        w.prnt('', 'Too few arguments for command "{}" (help on command: /help {})'
+                .format(split_args[0], split_args[0].lstrip('/')))
+        return w.WEECHAT_RC_OK_EAT
+    query = split_args[1]
+
+    # Try finding the channel by name
+    channel = team.channels.get(team.get_channel_map().get(query))
+
+    # If the channel doesn't exist, try finding a DM or MPDM instead
+    if not channel:
+        if query.startswith('#'):
+            w.prnt('', 'ERROR: Unknown channel: {}'.format(query))
+            return w.WEECHAT_RC_OK_EAT
+
+        # Get the IDs of the users
+        all_users = team.get_username_map()
+        users = set()
+        for username in query.split(','):
+            user = all_users.get(username.lstrip('@'))
+            if not user:
+                w.prnt('', 'ERROR: Unknown user: {}'.format(username))
+                return w.WEECHAT_RC_OK_EAT
+            users.add(user)
+
+        if users:
+            if len(users) > 1:
+                channel_type = 'mpim'
+                # Add the current user since MPDMs include them as a member
+                users.add(team.myidentifier)
+            else:
+                channel_type = 'im'
+
+            channel = team.find_channel_by_members(users, channel_type=channel_type)
+
+            # If the DM or MPDM doesn't exist, create it
+            if not channel:
+                s = SlackRequest(team, SLACK_API_TRANSLATOR[channel_type]['join'],
+                        {'users': ','.join(users)})
+                EVENTROUTER.receive(s)
+
+    if channel:
+        channel.open()
+        if config.switch_buffer_on_join:
+            w.buffer_set(channel.channel_buffer, "display", "1")
+    return w.WEECHAT_RC_OK_EAT
+
+
+@slack_buffer_required
+@utf8_decode
+def command_showmuted(data, current_buffer, args):
+    """
+    /slack showmuted
+    List the muted channels in the current team.
+    """
+    team = EVENTROUTER.weechat_controller.buffers[current_buffer].team
+    muted_channels = [team.channels[key].name
+            for key in team.muted_channels if key in team.channels]
+    team.buffer_prnt("Muted channels: {}".format(', '.join(muted_channels)))
+    return w.WEECHAT_RC_OK_EAT
+
+
+def get_msg_from_id(channel, msg_id):
+    if msg_id[0] == '$':
+        msg_id = msg_id[1:]
+    ts = channel.hashed_messages.get(msg_id)
+    return channel.messages.get(ts)
+
+
+@slack_buffer_required
+@utf8_decode
+def command_thread(data, current_buffer, args):
+    """
+    /thread [message_id]
+    Open the thread for the message.
+    If no message id is specified the last thread in channel will be opened.
+    """
+    channel = EVENTROUTER.weechat_controller.buffers[current_buffer]
+
+    if not isinstance(channel, SlackChannelCommon):
+        print_error('/thread can not be used in the team buffer, only in a channel')
+        return w.WEECHAT_RC_ERROR
+
+    if args:
+        msg = get_msg_from_id(channel, args)
+        if not msg:
+            w.prnt('', 'ERROR: Invalid id given, must be an existing id')
+            return w.WEECHAT_RC_OK_EAT
+    else:
+        for message in reversed(channel.messages.values()):
+            if type(message) == SlackMessage and message.number_of_replies():
+                msg = message
+                break
+        else:
+            w.prnt('', 'ERROR: No threads found in channel')
+            return w.WEECHAT_RC_OK_EAT
+
+    msg.open_thread(switch=config.switch_buffer_on_join)
+    return w.WEECHAT_RC_OK_EAT
+
+command_thread.completion = '%(threads)'
+
+
+@slack_buffer_required
+@utf8_decode
+def command_reply(data, current_buffer, args):
+    """
+    /reply [-alsochannel] [<count/message_id>] <message>
+
+    When in a channel buffer:
+    /reply [-alsochannel] <count/message_id> <message>
+    Reply in a thread on the message. Specify either the message id or a count
+    upwards to the message from the last message.
+
+    When in a thread buffer:
+    /reply [-alsochannel] <message>
+    Reply to the current thread.  This can be used to send the reply to the
+    rest of the channel.
+
+    In either case, -alsochannel also sends the reply to the parent channel.
+    """
+    channel = EVENTROUTER.weechat_controller.buffers[current_buffer]
+    parts = args.split(None, 1)
+    if parts[0] == "-alsochannel":
+        args = parts[1]
+        broadcast = True
+    else:
+        broadcast = False
+
+    if isinstance(channel, SlackThreadChannel):
+        text = args
+        msg = channel.parent_message
+    else:
+        try:
+            msg_id, text = args.split(None, 1)
+        except ValueError:
+            w.prnt('', 'Usage (when in a channel buffer): /reply [-alsochannel] <count/message_id> <message>')
+            return w.WEECHAT_RC_OK_EAT
+        msg = get_msg_from_id(channel, msg_id)
+
+    if msg:
+        if isinstance(msg, SlackThreadMessage):
+            parent_id = str(msg.parent_message.ts)
+        else:
+            parent_id = str(msg.ts)
+    elif msg_id.isdigit() and int(msg_id) >= 1:
+        mkeys = channel.main_message_keys_reversed()
+        parent_id = str(next(islice(mkeys, int(msg_id) - 1, None)))
+    else:
+        w.prnt('', 'ERROR: Invalid id given, must be a number greater than 0 or an existing id')
+        return w.WEECHAT_RC_OK_EAT
+
+    channel.send_message(text, request_dict_ext={'thread_ts': parent_id, 'reply_broadcast': broadcast})
+    return w.WEECHAT_RC_OK_EAT
+
+command_reply.completion = '-alsochannel %(threads)||%(threads)'
+
+
+@slack_buffer_required
+@utf8_decode
+def command_rehistory(data, current_buffer, args):
+    """
+    /rehistory
+    Reload the history in the current channel.
+    """
+    channel = EVENTROUTER.weechat_controller.buffers[current_buffer]
+    channel.clear_messages()
+    channel.get_history()
+    return w.WEECHAT_RC_OK_EAT
+
+
+@slack_buffer_required
+@utf8_decode
+def command_hide(data, current_buffer, args):
+    """
+    /hide
+    Hide the current channel if it is marked as distracting.
+    """
+    channel = EVENTROUTER.weechat_controller.buffers[current_buffer]
+    name = channel.formatted_name(style='long_default')
+    if name in config.distracting_channels:
+        w.buffer_set(channel.channel_buffer, "hidden", "1")
+    return w.WEECHAT_RC_OK_EAT
+
+
+@utf8_decode
+def slack_command_cb(data, current_buffer, args):
+    split_args = args.split(' ', 1)
+    cmd_name = split_args[0]
+    cmd_args = split_args[1] if len(split_args) > 1 else ''
+    cmd = EVENTROUTER.cmds.get(cmd_name or 'help')
+    if not cmd:
+        w.prnt('', 'Command not found: ' + cmd_name)
+        return w.WEECHAT_RC_OK
+    return cmd(data, current_buffer, cmd_args)
+
+
+@utf8_decode
+def command_help(data, current_buffer, args):
+    """
+    /slack help [command]
+    Print help for /slack commands.
+    """
+    if args:
+        cmd = EVENTROUTER.cmds.get(args)
+        if cmd:
+            cmds = {args: cmd}
+        else:
+            w.prnt('', 'Command not found: ' + args)
+            return w.WEECHAT_RC_OK
+    else:
+        cmds = EVENTROUTER.cmds
+        w.prnt('', '\n{}'.format(colorize_string('bold', 'Slack commands:')))
+
+    script_prefix = '{0}[{1}python{0}/{1}slack{0}]{1}'.format(w.color('green'), w.color('reset'))
+
+    for _, cmd in sorted(cmds.items()):
+        name, cmd_args, description = parse_help_docstring(cmd)
+        w.prnt('', '\n{}  {} {}\n\n{}'.format(
+            script_prefix, colorize_string('white', name), cmd_args, description))
+    return w.WEECHAT_RC_OK
+
+
+@slack_buffer_required
+@utf8_decode
+def command_distracting(data, current_buffer, args):
+    """
+    /slack distracting
+    Add or remove the current channel from distracting channels. You can hide
+    or unhide these channels with /slack nodistractions.
+    """
+    channel = EVENTROUTER.weechat_controller.buffers[current_buffer]
+    fullname = channel.formatted_name(style="long_default")
+    if fullname in config.distracting_channels:
+        config.distracting_channels.remove(fullname)
+    else:
+        config.distracting_channels.append(fullname)
+    w.config_set_plugin('distracting_channels', ','.join(config.distracting_channels))
+    return w.WEECHAT_RC_OK_EAT
+
+
+@slack_buffer_required
+@utf8_decode
+def command_slash(data, current_buffer, args):
+    """
+    /slack slash /customcommand arg1 arg2 arg3
+    Run a custom slack command.
+    """
+    channel = EVENTROUTER.weechat_controller.buffers[current_buffer]
+    team = channel.team
+
+    split_args = args.split(' ', 1)
+    command = split_args[0]
+    text = split_args[1] if len(split_args) > 1 else ""
+    text_linkified = linkify_text(text, team, only_users=True)
+
+    s = SlackRequest(team, "chat.command",
+            {"command": command, "text": text_linkified, 'channel': channel.identifier},
+            channel=channel, metadata={'command': command, 'command_args': text})
+    EVENTROUTER.receive(s)
+    return w.WEECHAT_RC_OK_EAT
+
+
+@slack_buffer_required
+@utf8_decode
+def command_mute(data, current_buffer, args):
+    """
+    /slack mute
+    Toggle mute on the current channel.
+    """
+    channel = EVENTROUTER.weechat_controller.buffers[current_buffer]
+    team = channel.team
+    team.muted_channels ^= {channel.identifier}
+    muted_str = "Muted" if channel.identifier in team.muted_channels else "Unmuted"
+    team.buffer_prnt("{} channel {}".format(muted_str, channel.name))
+    s = SlackRequest(team, "users.prefs.set",
+            {"name": "muted_channels", "value": ",".join(team.muted_channels)}, channel=channel)
+    EVENTROUTER.receive(s)
+    return w.WEECHAT_RC_OK_EAT
+
+
+@slack_buffer_required
+@utf8_decode
+def command_linkarchive(data, current_buffer, args):
+    """
+    /slack linkarchive [message_id]
+    Place a link to the channel or message in the input bar.
+    Use cursor or mouse mode to get the id.
+    """
+    channel = EVENTROUTER.weechat_controller.buffers[current_buffer]
+    url = 'https://{}/'.format(channel.team.domain)
+
+    if isinstance(channel, SlackChannelCommon):
+        url += 'archives/{}/'.format(channel.identifier)
+        if args:
+            if args[0] == '$':
+                message_id = args[1:]
+            else:
+                message_id = args
+            ts = channel.hashed_messages.get(message_id)
+            message = channel.messages.get(ts)
+            if message:
+                url += 'p{}{:0>6}'.format(message.ts.majorstr(), message.ts.minorstr())
+                if isinstance(message, SlackThreadMessage):
+                    url += "?thread_ts={}&cid={}".format(message.parent_message.ts, channel.identifier)
+            else:
+                w.prnt('', 'ERROR: Invalid id given, must be an existing id')
+                return w.WEECHAT_RC_OK_EAT
+
+    w.command(current_buffer, "/input insert {}".format(url))
+    return w.WEECHAT_RC_OK_EAT
+
+command_linkarchive.completion = '%(threads)'
+
+
+@utf8_decode
+def command_nodistractions(data, current_buffer, args):
+    """
+    /slack nodistractions
+    Hide or unhide all channels marked as distracting.
+    """
+    global hide_distractions
+    hide_distractions = not hide_distractions
+    channels = [channel for channel in EVENTROUTER.weechat_controller.buffers.values()
+            if channel in config.distracting_channels]
+    for channel in channels:
+        w.buffer_set(channel.channel_buffer, "hidden", str(int(hide_distractions)))
+    return w.WEECHAT_RC_OK_EAT
+
+
+@slack_buffer_required
+@utf8_decode
+def command_upload(data, current_buffer, args):
+    """
+    /slack upload <filename>
+    Uploads a file to the current buffer.
+    """
+    channel = EVENTROUTER.weechat_controller.buffers[current_buffer]
+    weechat_dir = w.info_get("weechat_dir", "")
+    file_path = os.path.join(weechat_dir, os.path.expanduser(args))
+
+    if channel.type == 'team':
+        w.prnt('', "ERROR: Can't upload a file to the team buffer")
+        return w.WEECHAT_RC_ERROR
+
+    if not os.path.isfile(file_path):
+        unescaped_file_path = file_path.replace(r'\ ', ' ')
+        if os.path.isfile(unescaped_file_path):
+            file_path = unescaped_file_path
+        else:
+            w.prnt('', 'ERROR: Could not find file: {}'.format(file_path))
+            return w.WEECHAT_RC_ERROR
+
+    post_data = {
+        'channels': channel.identifier,
+    }
+    if isinstance(channel, SlackThreadChannel):
+        post_data['thread_ts'] = channel.parent_message.ts
+
+    url = SlackRequest(channel.team, 'files.upload', post_data, channel=channel).request_string()
+    options = [
+        '-s',
+        '-Ffile=@{}'.format(file_path),
+        url
+    ]
+
+    proxy_string = ProxyWrapper().curl()
+    if proxy_string:
+        options.append(proxy_string)
+
+    options_hashtable = {'arg{}'.format(i + 1): arg for i, arg in enumerate(options)}
+    w.hook_process_hashtable('curl', options_hashtable, config.slack_timeout, 'upload_callback', '')
+    return w.WEECHAT_RC_OK_EAT
+
+command_upload.completion = '%(filename)'
+
+
+@utf8_decode
+def upload_callback(data, command, return_code, out, err):
+    if return_code != 0:
+        w.prnt("", "ERROR: Couldn't upload file. Got return code {}. Error: {}".format(return_code, err))
+        return w.WEECHAT_RC_OK_EAT
+
+    try:
+        response = json.loads(out)
+    except JSONDecodeError:
+        w.prnt("", "ERROR: Couldn't process response from file upload. Got: {}".format(out))
+        return w.WEECHAT_RC_OK_EAT
+
+    if not response["ok"]:
+        w.prnt("", "ERROR: Couldn't upload file. Error: {}".format(response["error"]))
+    return w.WEECHAT_RC_OK_EAT
+
+
+@utf8_decode
+def away_command_cb(data, current_buffer, args):
+    all_servers, message = re.match('^/away( -all)? ?(.*)', args).groups()
+    if all_servers:
+        team_buffers = [team.channel_buffer for team in EVENTROUTER.teams.values()]
+    elif current_buffer in EVENTROUTER.weechat_controller.buffers:
+        team_buffers = [current_buffer]
+    else:
+        return w.WEECHAT_RC_OK
+
+    for team_buffer in team_buffers:
+        if message:
+            command_away(data, team_buffer, args)
+        else:
+            command_back(data, team_buffer, args)
+    return w.WEECHAT_RC_OK
+
+
+@slack_buffer_required
+@utf8_decode
+def command_away(data, current_buffer, args):
+    """
+    /slack away
+    Sets your status as 'away'.
+    """
+    team = EVENTROUTER.weechat_controller.buffers[current_buffer].team
+    s = SlackRequest(team, "users.setPresence", {"presence": "away"})
+    EVENTROUTER.receive(s)
+    return w.WEECHAT_RC_OK
+
+
+@slack_buffer_required
+@utf8_decode
+def command_status(data, current_buffer, args):
+    """
+    /slack status [<emoji> [<status_message>]|-delete]
+    Lets you set your Slack Status (not to be confused with away/here).
+    Prints current status if no arguments are given, unsets the status if -delete is given.
+    """
+    team = EVENTROUTER.weechat_controller.buffers[current_buffer].team
+
+    split_args = args.split(" ", 1)
+    if not split_args[0]:
+        profile = team.users[team.myidentifier].profile
+        team.buffer_prnt("Status: {} {}".format(
+            replace_string_with_emoji(profile.get("status_emoji", "")),
+            profile.get("status_text", "")))
+        return w.WEECHAT_RC_OK
+
+    emoji = "" if split_args[0] == "-delete" else split_args[0]
+    text = split_args[1] if len(split_args) > 1 else ""
+    new_profile = {"status_text": text, "status_emoji": emoji}
+
+    s = SlackRequest(team, "users.profile.set", {"profile": new_profile})
+    EVENTROUTER.receive(s)
+    return w.WEECHAT_RC_OK
+
+command_status.completion = "-delete|%(emoji)"
+
+
+@utf8_decode
+def line_event_cb(data, signal, hashtable):
+    buffer_pointer = hashtable["_buffer"]
+    line_timestamp = hashtable["_chat_line_date"]
+    line_time_id = hashtable["_chat_line_date_printed"]
+    channel = EVENTROUTER.weechat_controller.buffers.get(buffer_pointer)
+
+    if line_timestamp and line_time_id and isinstance(channel, SlackChannelCommon):
+        ts = SlackTS("{}.{}".format(line_timestamp, line_time_id))
+
+        message_hash = channel.hash_message(ts)
+        if message_hash is None:
+            return w.WEECHAT_RC_OK
+        message_hash = "$" + message_hash
+
+        if data == "message":
+            w.command(buffer_pointer, "/cursor stop")
+            w.command(buffer_pointer, "/input insert {}".format(message_hash))
+        elif data == "delete":
+            w.command(buffer_pointer, "/input send {}s///".format(message_hash))
+        elif data == "linkarchive":
+            w.command(buffer_pointer, "/cursor stop")
+            w.command(buffer_pointer, "/slack linkarchive {}".format(message_hash[1:]))
+        elif data == "reply":
+            w.command(buffer_pointer, "/cursor stop")
+            w.command(buffer_pointer, "/input insert /reply {}\\x20".format(message_hash))
+        elif data == "thread":
+            w.command(buffer_pointer, "/cursor stop")
+            w.command(buffer_pointer, "/thread {}".format(message_hash))
+    return w.WEECHAT_RC_OK
+
+
+@slack_buffer_required
+@utf8_decode
+def command_back(data, current_buffer, args):
+    """
+    /slack back
+    Sets your status as 'back'.
+    """
+    team = EVENTROUTER.weechat_controller.buffers[current_buffer].team
+    s = SlackRequest(team, "users.setPresence", {"presence": "auto"})
+    EVENTROUTER.receive(s)
+    set_own_presence_active(team)
+    return w.WEECHAT_RC_OK
+
+
+@slack_buffer_required
+@utf8_decode
+def command_label(data, current_buffer, args):
+    """
+    /label <name>
+    Rename a thread buffer. Note that this is not permanent. It will only last
+    as long as you keep the buffer and wee-slack open.
+    """
+    channel = EVENTROUTER.weechat_controller.buffers[current_buffer]
+    if channel.type == 'thread':
+        new_name = " +" + args
+        channel.label = new_name
+        w.buffer_set(channel.channel_buffer, "short_name", new_name)
+    return w.WEECHAT_RC_OK
+
+
+@utf8_decode
+def set_unread_cb(data, current_buffer, command):
+    for channel in EVENTROUTER.weechat_controller.buffers.values():
+        channel.mark_read()
+    return w.WEECHAT_RC_OK
+
+
+@slack_buffer_or_ignore
+@utf8_decode
+def set_unread_current_buffer_cb(data, current_buffer, command):
+    channel = EVENTROUTER.weechat_controller.buffers[current_buffer]
+    channel.mark_read()
+    return w.WEECHAT_RC_OK
+
+
+###### NEW EXCEPTIONS
+
+
+class InvalidType(Exception):
+    """
+    Raised when we do type checking to ensure objects of the wrong
+    type are not used improperly.
+    """
+    def __init__(self, type_str):
+        super(InvalidType, self).__init__(type_str)
+
+###### New but probably old and need to migrate
+
+
+def closed_slack_debug_buffer_cb(data, buffer):
+    global slack_debug
+    slack_debug = None
+    return w.WEECHAT_RC_OK
+
+
+def create_slack_debug_buffer():
+    global slack_debug, debug_string
+    if slack_debug is None:
+        debug_string = None
+        slack_debug = w.buffer_new("slack-debug", "", "", "closed_slack_debug_buffer_cb", "")
+        w.buffer_set(slack_debug, "notify", "0")
+        w.buffer_set(slack_debug, "highlight_tags_restrict", "highlight_force")
+
+
+def load_emoji():
+    try:
+        DIR = w.info_get('weechat_dir', '')
+        with open('{}/weemoji.json'.format(DIR), 'r') as ef:
+            emojis = json.loads(ef.read())
+            if 'emoji' in emojis:
+                print_error('The weemoji.json file is in an old format. Please update it.')
+            else:
+                emoji_unicode = {key: value['unicode'] for key, value in emojis.items()}
+
+                emoji_skin_tones = {skin_tone['name']: skin_tone['unicode']
+                        for emoji in emojis.values()
+                        for skin_tone in emoji.get('skinVariations', {}).values()}
+
+                emoji_with_skin_tones = chain(emoji_unicode.items(), emoji_skin_tones.items())
+                emoji_with_skin_tones_reverse = {v: k for k, v in emoji_with_skin_tones}
+                return emoji_unicode, emoji_with_skin_tones_reverse
+    except:
+        dbg("Couldn't load emoji list: {}".format(format_exc_only()), 5)
+    return {}, {}
+
+
+def parse_help_docstring(cmd):
+    doc = textwrap.dedent(cmd.__doc__).strip().split('\n', 1)
+    cmd_line = doc[0].split(None, 1)
+    args = ''.join(cmd_line[1:])
+    return cmd_line[0], args, doc[1].strip()
+
+
+def setup_hooks():
+    w.bar_item_new('slack_typing_notice', '(extra)typing_bar_item_cb', '')
+    w.bar_item_new('away', '(extra)away_bar_item_cb', '')
+    w.bar_item_new('slack_away', '(extra)away_bar_item_cb', '')
+
+    w.hook_timer(5000, 0, 0, "ws_ping_cb", "")
+    w.hook_timer(1000, 0, 0, "typing_update_cb", "")
+    w.hook_timer(1000, 0, 0, "buffer_list_update_callback", "EVENTROUTER")
+    w.hook_timer(3000, 0, 0, "reconnect_callback", "EVENTROUTER")
+    w.hook_timer(1000 * 60 * 5, 0, 0, "slack_never_away_cb", "")
+
+    w.hook_signal('buffer_closing', "buffer_closing_callback", "")
+    w.hook_signal('buffer_switch', "buffer_switch_callback", "EVENTROUTER")
+    w.hook_signal('window_switch', "buffer_switch_callback", "EVENTROUTER")
+    w.hook_signal('quit', "quit_notification_callback", "")
+    if config.send_typing_notice:
+        w.hook_signal('input_text_changed', "typing_notification_cb", "")
+
+    command_help.completion = '|'.join(EVENTROUTER.cmds.keys())
+    completions = '||'.join(
+            '{} {}'.format(name, getattr(cmd, 'completion', ''))
+            for name, cmd in EVENTROUTER.cmds.items())
+
+    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(sorted(EVENTROUTER.cmds.keys())) +
+        '\nUse /slack help <command> to find out more\n',
+        # Completions
+        completions,
+        # Function name
+        'slack_command_cb', '')
+
+    w.hook_command_run('/me', 'me_command_cb', '')
+    w.hook_command_run('/query', 'join_query_command_cb', '')
+    w.hook_command_run('/join', 'join_query_command_cb', '')
+    w.hook_command_run('/part', 'part_command_cb', '')
+    w.hook_command_run('/topic', 'topic_command_cb', '')
+    w.hook_command_run('/msg', 'msg_command_cb', '')
+    w.hook_command_run('/invite', 'invite_command_cb', '')
+    w.hook_command_run("/input complete_next", "complete_next_cb", "")
+    w.hook_command_run("/input set_unread", "set_unread_cb", "")
+    w.hook_command_run("/input set_unread_current_buffer", "set_unread_current_buffer_cb", "")
+    w.hook_command_run('/away', 'away_command_cb', '')
+    w.hook_command_run('/whois', 'whois_command_cb', '')
+
+    for cmd_name in ['hide', 'label', 'rehistory', 'reply', 'thread']:
+        cmd = EVENTROUTER.cmds[cmd_name]
+        _, args, description = parse_help_docstring(cmd)
+        completion = getattr(cmd, 'completion', '')
+        w.hook_command(cmd_name, description, args, '', completion, 'command_' + cmd_name, '')
+
+    w.hook_completion("irc_channel_topic", "complete topic for slack", "topic_completion_cb", "")
+    w.hook_completion("irc_channels", "complete channels for slack", "channel_completion_cb", "")
+    w.hook_completion("irc_privates", "complete dms/mpdms for slack", "dm_completion_cb", "")
+    w.hook_completion("nicks", "complete @-nicks for slack", "nick_completion_cb", "")
+    w.hook_completion("threads", "complete thread ids for slack", "thread_completion_cb", "")
+    w.hook_completion("usergroups", "complete @-usergroups for slack", "usergroups_completion_cb", "")
+    w.hook_completion("emoji", "complete :emoji: for slack", "emoji_completion_cb", "")
+
+    w.key_bind("mouse", {
+        "@chat(python.*):button2": "hsignal:slack_mouse",
+        })
+    w.key_bind("cursor", {
+        "@chat(python.*):D": "hsignal:slack_cursor_delete",
+        "@chat(python.*):L": "hsignal:slack_cursor_linkarchive",
+        "@chat(python.*):M": "hsignal:slack_cursor_message",
+        "@chat(python.*):R": "hsignal:slack_cursor_reply",
+        "@chat(python.*):T": "hsignal:slack_cursor_thread",
+        })
+
+    w.hook_hsignal("slack_mouse", "line_event_cb", "message")
+    w.hook_hsignal("slack_cursor_delete", "line_event_cb", "delete")
+    w.hook_hsignal("slack_cursor_linkarchive", "line_event_cb", "linkarchive")
+    w.hook_hsignal("slack_cursor_message", "line_event_cb", "message")
+    w.hook_hsignal("slack_cursor_reply", "line_event_cb", "reply")
+    w.hook_hsignal("slack_cursor_thread", "line_event_cb", "thread")
+
+    # Hooks to fix/implement
+    # w.hook_signal('buffer_opened', "buffer_opened_cb", "")
+    # w.hook_signal('window_scrolled', "scrolled_cb", "")
+    # w.hook_timer(3000, 0, 0, "slack_connection_persistence_cb", "")
+
+##### END NEW
+
+
+def dbg(message, level=0, main_buffer=False, fout=False):
+    """
+    send debug output to the slack-debug buffer and optionally write to a file.
+    """
+    # TODO: do this smarter
+    if level >= config.debug_level:
+        global debug_string
+        message = "DEBUG: {}".format(message)
+        if fout:
+            with open('/tmp/debug.log', 'a+') as log_file:
+                log_file.writelines(message + '\n')
+        if main_buffer:
+                w.prnt("", "slack: " + message)
+        else:
+            if slack_debug and (not debug_string or debug_string in message):
+                w.prnt(slack_debug, message)
+
+
+###### Config code
+class PluginConfig(object):
+    Setting = collections.namedtuple('Setting', ['default', 'desc'])
+    # Default settings.
+    # These are, initially, each a (default, desc) tuple; the former is the
+    # default value of the setting, in the (string) format that weechat
+    # expects, and the latter is the user-friendly description of the setting.
+    # At __init__ time these values are extracted, the description is used to
+    # set or update the setting description for use with /help, and the default
+    # value is used to set the default for any settings not already defined.
+    # Following this procedure, the keys remain the same, but the values are
+    # the real (python) values of the settings.
+    default_settings = {
+        'auto_open_threads': Setting(
+            default='false',
+            desc='Automatically open threads when mentioned or in'
+            'response to own messages.'),
+        'background_load_all_history': Setting(
+            default='false',
+            desc='Load history for each channel in the background as soon as it'
+            ' opens, rather than waiting for the user to look at it.'),
+        'channel_name_typing_indicator': Setting(
+            default='true',
+            desc='Change the prefix of a channel from # to > when someone is'
+            ' typing in it. Note that this will (temporarily) affect the sort'
+            ' order if you sort buffers by name rather than by number.'),
+        'color_buflist_muted_channels': Setting(
+            default='darkgray',
+            desc='Color to use for muted channels in the buflist'),
+        'color_deleted': Setting(
+            default='red',
+            desc='Color to use for deleted messages and files.'),
+        'color_edited_suffix': Setting(
+            default='095',
+            desc='Color to use for (edited) suffix on messages that have been edited.'),
+        'color_reaction_suffix': Setting(
+            default='darkgray',
+            desc='Color to use for the [:wave:(@user)] suffix on messages that'
+            ' have reactions attached to them.'),
+        'color_reaction_suffix_added_by_you': Setting(
+            default='blue',
+            desc='Color to use for reactions that you have added.'),
+        'color_thread_suffix': Setting(
+            default='lightcyan',
+            desc='Color to use for the [thread: XXX] suffix on messages that'
+            ' have threads attached to them. The special value "multiple" can'
+            ' be used to use a different color for each thread.'),
+        'color_typing_notice': Setting(
+            default='yellow',
+            desc='Color to use for the typing notice.'),
+        'colorize_private_chats': Setting(
+            default='false',
+            desc='Whether to use nick-colors in DM windows.'),
+        'debug_mode': Setting(
+            default='false',
+            desc='Open a dedicated buffer for debug messages and start logging'
+            ' to it. How verbose the logging is depends on log_level.'),
+        'debug_level': Setting(
+            default='3',
+            desc='Show only this level of debug info (or higher) when'
+            ' debug_mode is on. Lower levels -> more messages.'),
+        'distracting_channels': Setting(
+            default='',
+            desc='List of channels to hide.'),
+        'external_user_suffix': Setting(
+            default='*',
+            desc='The suffix appended to nicks to indicate external users.'),
+        'files_download_location': Setting(
+            default='',
+            desc='If set, file attachments will be automatically downloaded'
+            ' to this location. "%h" will be replaced by WeeChat home,'
+            ' "~/.weechat" by default.'),
+        'group_name_prefix': Setting(
+            default='&',
+            desc='The prefix of buffer names for groups (private channels).'),
+        'map_underline_to': Setting(
+            default='_',
+            desc='When sending underlined text to slack, use this formatting'
+            ' character for it. The default ("_") sends it as italics. Use'
+            ' "*" to send bold instead.'),
+        'muted_channels_activity': Setting(
+            default='personal_highlights',
+            desc="Control which activity you see from muted channels, either"
+            " none, personal_highlights, all_highlights or all. none: Don't"
+            " show any activity. personal_highlights: Only show personal"
+            " highlights, i.e. not @channel and @here. all_highlights: Show"
+            " all highlights, but not other messages. all: Show all activity,"
+            " like other channels."),
+        'notify_usergroup_handle_updated': Setting(
+            default='false',
+            desc="Control if you want to see notification when a usergroup's"
+            " handle has changed, either true or false."),
+        'never_away': Setting(
+            default='false',
+            desc='Poke Slack every five minutes so that it never marks you "away".'),
+        'record_events': Setting(
+            default='false',
+            desc='Log all traffic from Slack to disk as JSON.'),
+        'render_bold_as': Setting(
+            default='bold',
+            desc='When receiving bold text from Slack, render it as this in weechat.'),
+        'render_emoji_as_string': Setting(
+            default='false',
+            desc="Render emojis as :emoji_name: instead of emoji characters. Enable this"
+            " if your terminal doesn't support emojis, or set to 'both' if you want to"
+            " see both renderings. Note that even though this is"
+            " disabled by default, you need to place {}/blob/master/weemoji.json in your"
+            " weechat directory to enable rendering emojis as emoji characters."
+            .format(REPO_URL)),
+        'render_italic_as': Setting(
+            default='italic',
+            desc='When receiving bold text from Slack, render it as this in weechat.'
+            ' If your terminal lacks italic support, consider using "underline" instead.'),
+        'send_typing_notice': Setting(
+            default='true',
+            desc='Alert Slack users when you are typing a message in the input bar '
+            '(Requires reload)'),
+        'server_aliases': Setting(
+            default='',
+            desc='A comma separated list of `subdomain:alias` pairs. The alias'
+            ' will be used instead of the actual name of the slack (in buffer'
+            ' names, logging, etc). E.g `work:no_fun_allowed` would make your'
+            ' work slack show up as `no_fun_allowed` rather than `work.slack.com`.'),
+        'shared_name_prefix': Setting(
+            default='%',
+            desc='The prefix of buffer names for shared channels.'),
+        'short_buffer_names': Setting(
+            default='false',
+            desc='Use `foo.#channel` rather than `foo.slack.com.#channel` as the'
+            ' internal name for Slack buffers.'),
+        'show_buflist_presence': Setting(
+            default='true',
+            desc='Display a `+` character in the buffer list for present users.'),
+        'show_reaction_nicks': Setting(
+            default='false',
+            desc='Display the name of the reacting user(s) alongside each reactji.'),
+        'slack_api_token': Setting(
+            default='INSERT VALID KEY HERE!',
+            desc='List of Slack API tokens, one per Slack instance you want to'
+            ' connect to. See the README for details on how to get these.'),
+        'slack_timeout': Setting(
+            default='20000',
+            desc='How long (ms) to wait when communicating with Slack.'),
+        'switch_buffer_on_join': Setting(
+            default='true',
+            desc='When /joining a channel, automatically switch to it as well.'),
+        'thread_messages_in_channel': Setting(
+            default='false',
+            desc='When enabled shows thread messages in the parent channel.'),
+        'unfurl_ignore_alt_text': Setting(
+            default='false',
+            desc='When displaying ("unfurling") links to channels/users/etc,'
+            ' ignore the "alt text" present in the message and instead use the'
+            ' canonical name of the thing being linked to.'),
+        'unfurl_auto_link_display': Setting(
+            default='both',
+            desc='When displaying ("unfurling") links to channels/users/etc,'
+            ' determine what is displayed when the text matches the url'
+            ' without the protocol. This happens when Slack automatically'
+            ' creates links, e.g. from words separated by dots or email'
+            ' addresses. Set it to "text" to only display the text written by'
+            ' the user, "url" to only display the url or "both" (the default)'
+            ' to display both.'),
+        'unhide_buffers_with_activity': Setting(
+            default='false',
+            desc='When activity occurs on a buffer, unhide it even if it was'
+            ' previously hidden (whether by the user or by the'
+            ' distracting_channels setting).'),
+        'use_full_names': Setting(
+            default='false',
+            desc='Use full names as the nicks for all users. When this is'
+            ' false (the default), display names will be used if set, with a'
+            ' fallback to the full name if display name is not set.'),
+    }
+
+    # Set missing settings to their defaults. Load non-missing settings from
+    # weechat configs.
+    def __init__(self):
+        self.settings = {}
+        # Set all descriptions, replace the values in the dict with the
+        # default setting value rather than the (setting,desc) tuple.
+        for key, (default, desc) in self.default_settings.items():
+            w.config_set_desc_plugin(key, desc)
+            self.settings[key] = default
+
+        # Migrate settings from old versions of Weeslack...
+        self.migrate()
+        # ...and then set anything left over from the defaults.
+        for key, default in self.settings.items():
+            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):
+        try:
+            return getattr(self, 'get_' + key)(key)
+        except AttributeError:
+            # Most settings are on/off, so make get_boolean the default
+            return self.get_boolean(key)
+        except:
+            # There was setting-specific getter, but it failed.
+            return self.settings[key]
+
+    def __getattr__(self, key):
+        try:
+            return self.settings[key]
+        except KeyError:
+            raise AttributeError(key)
+
+    def get_boolean(self, key):
+        return w.config_string_to_boolean(w.config_get_plugin(key))
+
+    def get_string(self, key):
+        return w.config_get_plugin(key)
+
+    def get_int(self, key):
+        return int(w.config_get_plugin(key))
+
+    def is_default(self, key):
+        default = self.default_settings.get(key).default
+        return w.config_get_plugin(key) == default
+
+    get_color_buflist_muted_channels = get_string
+    get_color_deleted = get_string
+    get_color_edited_suffix = get_string
+    get_color_reaction_suffix = get_string
+    get_color_reaction_suffix_added_by_you = get_string
+    get_color_thread_suffix = get_string
+    get_color_typing_notice = get_string
+    get_debug_level = get_int
+    get_external_user_suffix = get_string
+    get_files_download_location = get_string
+    get_group_name_prefix = get_string
+    get_map_underline_to = get_string
+    get_muted_channels_activity = get_string
+    get_render_bold_as = get_string
+    get_render_italic_as = get_string
+    get_shared_name_prefix = get_string
+    get_slack_timeout = get_int
+    get_unfurl_auto_link_display = get_string
+
+    def get_distracting_channels(self, key):
+        return [x.strip() for x in w.config_get_plugin(key).split(',') if x]
+
+    def get_server_aliases(self, key):
+        alias_list = w.config_get_plugin(key)
+        return dict(item.split(":") for item in alias_list.split(",") if ':' in item)
+
+    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_render_emoji_as_string(self, key):
+        s = w.config_get_plugin(key)
+        if s == 'both':
+            return s
+        return w.config_string_to_boolean(s)
+
+    def migrate(self):
+        """
+        This is to migrate the extension name from slack_extension to slack
+        """
+        if not w.config_get_plugin("migrated"):
+            for k in self.settings.keys():
+                if not w.config_is_set_plugin(k):
+                    p = w.config_get("plugins.var.python.slack_extension.{}".format(k))
+                    data = w.config_string(p)
+                    if data != "":
+                        w.config_set_plugin(k, data)
+            w.config_set_plugin("migrated", "true")
+
+        old_thread_color_config = w.config_get_plugin("thread_suffix_color")
+        new_thread_color_config = w.config_get_plugin("color_thread_suffix")
+        if old_thread_color_config and not new_thread_color_config:
+            w.config_set_plugin("color_thread_suffix", old_thread_color_config)
+
+
+def config_server_buffer_cb(data, key, value):
+    for team in EVENTROUTER.teams.values():
+        team.buffer_merge(value)
+    return w.WEECHAT_RC_OK
+
+
+# to Trace execution, add `setup_trace()` to startup
+# and  to a function and sys.settrace(trace_calls)  to a function
+def setup_trace():
+    global f
+    now = time.time()
+    f = open('{}/{}-trace.json'.format(RECORD_DIR, now), 'w')
+
+
+def trace_calls(frame, event, arg):
+    global f
+    if event != 'call':
+        return
+    co = frame.f_code
+    func_name = co.co_name
+    if func_name == 'write':
+        # Ignore write() calls from print statements
+        return
+    func_line_no = frame.f_lineno
+    func_filename = co.co_filename
+    caller = frame.f_back
+    caller_line_no = caller.f_lineno
+    caller_filename = caller.f_code.co_filename
+    print('Call to %s on line %s of %s from line %s of %s' % \
+        (func_name, func_line_no, func_filename,
+         caller_line_no, caller_filename), file=f)
+    f.flush()
+    return
+
+
+def initiate_connection(token, retries=3, team=None):
+    return SlackRequest(team,
+                        'rtm.{}'.format('connect' if team else 'start'),
+                        {"batch_presence_aware": 1},
+                        retries=retries,
+                        token=token)
+
+
+if __name__ == "__main__":
+
+    w = WeechatWrapper(weechat)
+
+    if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE,
+                  SCRIPT_DESC, "script_unloaded", ""):
+
+        weechat_version = w.info_get("version_number", "") or 0
+        if int(weechat_version) < 0x1030000:
+            w.prnt("", "\nERROR: Weechat version 1.3+ is required to use {}.\n\n".format(SCRIPT_NAME))
+        else:
+
+            global EVENTROUTER
+            EVENTROUTER = EventRouter()
+
+            receive_httprequest_callback = EVENTROUTER.receive_httprequest_callback
+            receive_ws_callback = EVENTROUTER.receive_ws_callback
+
+            # Global var section
+            slack_debug = None
+            config = PluginConfig()
+            config_changed_cb = config.config_changed
+
+            typing_timer = time.time()
+
+            hide_distractions = False
+
+            w.hook_config("plugins.var.python." + SCRIPT_NAME + ".*", "config_changed_cb", "")
+            w.hook_config("irc.look.server_buffer", "config_server_buffer_cb", "")
+            w.hook_modifier("input_text_for_buffer", "input_text_for_buffer_cb", "")
+
+            EMOJI, EMOJI_WITH_SKIN_TONES_REVERSE = load_emoji()
+            setup_hooks()
+
+            # attach to the weechat hooks we need
+
+            tokens = [token.strip() for token in config.slack_api_token.split(',')]
+            w.prnt('', 'Connecting to {} slack team{}.'
+                    .format(len(tokens), '' if len(tokens) == 1 else 's'))
+            for t in tokens:
+                s = initiate_connection(t)
+                EVENTROUTER.receive(s)
+            if config.record_events:
+                EVENTROUTER.record()
+            EVENTROUTER.handle_next()
+            # END attach to the weechat hooks we need
+
+            hdata = Hdata(w)