b89b95f1cb1d

Oh god
[view raw] [browse files]
author Steve Losh <steve@stevelosh.com>
date Tue, 04 Jul 2017 15:25:03 +0000
parents 73adae8ca49d
children af2b6e2d27f3
branches/tags (none)
files .hgsub .hgsubstate bin/sbcl-vlime hgignore lispwords vim/bundle/vlime-vim vim/vimrc weechat/python/autoload/wee_slack.py

Changes

--- a/.hgsub	Fri Jun 23 13:41:39 2017 +0000
+++ b/.hgsub	Tue Jul 04 15:25:03 2017 +0000
@@ -11,8 +11,6 @@
 vim/bundle/commentary            = [git]git://github.com/tpope/vim-commentary.git
 vim/bundle/ctrlp                 = [git]git://github.com/kien/ctrlp.vim.git
 vim/bundle/delimitmate           = [git]git://github.com/Raimondi/delimitMate.git
-vim/bundle/deoplete              = [git]git://github.com/Shougo/deoplete.nvim
-vim/bundle/deoplete-omnisharp    = [git]git://github.com/Robzz/deoplete-omnisharp/
 vim/bundle/dispatch              = [git]git://github.com/tpope/vim-dispatch.git
 vim/bundle/fireplace             = [git]git://github.com/tpope/vim-fireplace.git
 vim/bundle/fugitive              = [git]git://github.com/tpope/vim-fugitive.git
@@ -42,6 +40,7 @@
 vim/bundle/swig                  = [git]git://github.com/vim-scripts/SWIG-syntax.git
 vim/bundle/syntastic             = [git]git://github.com/scrooloose/syntastic.git
 vim/bundle/targets               = [git]git://github.com/wellle/targets.git
-vim/bundle/vim-completes-me      = [git]git://github.com/ajh17/VimCompletesMe.git
 vim/bundle/vimtex                = [git]git://github.com/lervag/vimtex.git
 vim/bundle/vitality              = [hg]https://bitbucket.org/sjl/vitality.vim
+vim/bundle/vlime                 = [git]git://github.com/l04m33/vlime.git
+vim/bundle/windowswap            = [git]git://github.com/wesQ3/vim-windowswap.git
--- a/.hgsubstate	Fri Jun 23 13:41:39 2017 +0000
+++ b/.hgsubstate	Tue Jul 04 15:25:03 2017 +0000
@@ -11,15 +11,13 @@
 dc349bb7d30f713d770fc1fa0fe209e6aab82dc8 vim/bundle/commentary
 c6d1fc5e58d689bfb104ff336aeb89d9ef1b48e2 vim/bundle/ctrlp
 38487bbec8ba50834e257940b357de03991fa8f9 vim/bundle/delimitmate
-16de9153fc2112129e1e2b3e4adcb1258c469159 vim/bundle/deoplete
-b4a82052ac8ab50623d09da96b01d5e9c3629159 vim/bundle/deoplete-omnisharp
 ffbd5eb50c9daf67657b87fd767d1801ac9a15a7 vim/bundle/dispatch
 1c75b56ceb96a6e7fb6708ae96ab63b3023bab2f vim/bundle/fireplace
 935a2cccd3065b1322fb2235285d42728600afdf vim/bundle/fugitive
 127d706f2def96876605e6bd5d366c973cb8e406 vim/bundle/gdl
 6ea4e1983b18cf440c8f800a3e94b57338a3e99f vim/bundle/glsl
 0d57b080f9fae8573c688b6679b31eb1666edc4c vim/bundle/gnuplot
-9dd2d48255fcc3ac5122f6028dc238fabcccd861 vim/bundle/gundo
+1d84591fff04caebab75cba2294fc3843f0a4a29 vim/bundle/gundo
 fccd580f5f11d576169ee347907c9fbd77af410a vim/bundle/html5
 78fffa609b3e6b84ef01ee4c9aba6d7435d7b18e vim/bundle/indent-object
 395f8901b34cc871c9576886938a6efda0eb7268 vim/bundle/javascript
@@ -42,6 +40,7 @@
 19c3d966440b6cfe8d74251881a48e961ddb8648 vim/bundle/swig
 cc6b92afa640db4342dc9ab9fd4215316888d6fa vim/bundle/syntastic
 f6f2d6618a321f5b0065586a7bc934325fec81ab vim/bundle/targets
-4367cf0727c8c7de9f7f056825e0dc04f8981f35 vim/bundle/vim-completes-me
 5d5c71044880443035e07009497962feacb56b20 vim/bundle/vimtex
 bf3fd7f67e730f93765bd3c1cfcdb18fd4043521 vim/bundle/vitality
+ae313e04ebd1bb9d0434fb2e7c9e25d418aa5f0a vim/bundle/vlime
+6876fe38b33732cb124d415ffc4156f16da5e118 vim/bundle/windowswap
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/sbcl-vlime	Tue Jul 04 15:25:03 2017 +0000
@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+
+set -e
+sbcl-rlwrap --eval "(load \"~/lib/dotfiles/vim/bundle/vlime/lisp/start-vlime.lisp\")" "$@"
--- a/hgignore	Fri Jun 23 13:41:39 2017 +0000
+++ b/hgignore	Tue Jul 04 15:25:03 2017 +0000
@@ -4,3 +4,4 @@
 *.pyc
 tags
 tags.bak
+*.fasl
--- a/lispwords	Fri Jun 23 13:41:39 2017 +0000
+++ b/lispwords	Tue Jul 04 15:25:03 2017 +0000
@@ -91,3 +91,6 @@
 
 ; temperance
 (1 push-logic-frame-with)
+
+; blt
+(1 key-case)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vim/bundle/vlime-vim	Tue Jul 04 15:25:03 2017 +0000
@@ -0,0 +1,1 @@
+vlime/vim/
\ No newline at end of file
--- a/vim/vimrc	Fri Jun 23 13:41:39 2017 +0000
+++ b/vim/vimrc	Tue Jul 04 15:25:03 2017 +0000
@@ -164,6 +164,8 @@
 
 set wildignore+=*.orig                           " Merge resolution files
 
+set wildignore+=*.fasl                           " Lisp FASLs
+
 " Clojure/Leiningen
 set wildignore+=classes
 set wildignore+=lib
@@ -460,9 +462,6 @@
 " Great for pasting Python lines into REPLs.
 nnoremap vv ^vg_
 
-" Sudo to write
-" cnoremap w!! w !sudo tee % >/dev/null
-
 " Typos
 command! -bang E e<bang>
 command! -bang Q q<bang>
@@ -474,18 +473,12 @@
 command! -bang Wq wq<bang>
 command! -bang WQ wq<bang>
 
-" I suck at typing.
-vnoremap - =
-
 " Toggle paste
 " For some reason pastetoggle doesn't redraw the screen (thus the status bar
 " doesn't change) while :set paste! does, so I use that instead.
 " set pastetoggle=<F6>
 nnoremap <F6> :set paste!<cr>
 
-" Toggle [i]nvisible characters
-nnoremap <leader>i :set list!<cr>
-
 " Unfuck my screen
 nnoremap U :syntax sync fromstart<cr>:redraw!<cr>
 
@@ -493,6 +486,9 @@
 nnoremap <leader>Go :Start! git push origin<cr>
 nnoremap <leader>Gu :Start! git push upstream<cr>
 
+" Open current directory in Finder
+nnoremap <leader>O :!open .<cr>
+
 " Zip Right
 "
 " Moves the character under the cursor to the end of the line.  Handy when you
@@ -510,13 +506,6 @@
 " This should preserve your last yank/delete as well.
 nnoremap zl :let @z=@"<cr>x$p:let @"=@z<cr>
 
-" Ranger
-nnoremap <leader>r :silent !ranger %:h<cr>:redraw!<cr>
-nnoremap <leader>R :silent !ranger<cr>:redraw!<cr>
-
-" Jump (see the J mini-plugin later on)
-nnoremap <leader>J :J<space>
-
 " Indent from insert mode
 " has to be imap because we want to be able to use the "go-indent" mapping
 imap <c-l> <c-o>gi
@@ -861,22 +850,30 @@
     hi def link replResult Debug
     hi def link replComment Comment
 endfunction "}}}
+function! MapLispReplKeys() "{{{
+    nnoremap <buffer> <silent> <localleader>q :call QuickloadLispSystem()<cr>
+    nnoremap <buffer> <silent> <localleader>Q :call QuickloadLispPrompt()<cr>
+endfunction "}}}
+function! InitializeLispRepl() "{{{
+    call MapLispReplKeys()
+    call HighlightLispRepl()
+endfunction "}}}
 
 function! OpenLispReplSBCL() "{{{
-    NeoRepl sbcl-nrepl
-    call HighlightLispRepl()
+    NeoRepl sbcl-vlime
+    call InitializeLispRepl()
 endfunction "}}}
 function! OpenLispReplCCL() "{{{
     NeoRepl ccl-nrepl
-    call HighlightLispRepl()
+    call InitializeLispRepl()
 endfunction "}}}
 function! OpenLispReplECL() "{{{
     NeoRepl ecl-nrepl
-    call HighlightLispRepl()
+    call InitializeLispRepl()
 endfunction "}}}
 function! OpenLispReplABCL() "{{{
     NeoRepl abcl-nrepl
-    call HighlightLispRepl()
+    call InitializeLispRepl()
 endfunction "}}}
 
 function! SetLispWords() "{{{
@@ -1012,6 +1009,7 @@
 function! QuickloadLispPrompt() " {{{
     call NeoReplSendRaw("(ql:quickload :" . input("? ") . ")\n")
 endfunction " }}}
+
 " }}}
 " Folding {{{
 let g:lisp_fold_extra = [ 
@@ -1042,38 +1040,27 @@
     au FileType lisp RainbowParenthesesActivate
     au syntax lisp RainbowParenthesesLoadRound
 
+    " Force omnicompletion (vlime's)
+    au FileType lisp inoremap <c-n> <c-x><c-o>
+
     au FileType lisp setlocal iskeyword+=!,?,%
     au FileType lisp setlocal equalprg=lispindent
 
     " scratch buffer
     au FileType lisp nnoremap <buffer> <leader><tab> :e scratch.lisp<cr>
 
-    " Handy stuff
+    " Open REPLs
+    au FileType lisp nnoremap <buffer> <silent> <localleader>Os :call OpenLispReplSBCL()<cr>
+    au FileType lisp nnoremap <buffer> <silent> <localleader>Oc :call OpenLispReplCCL()<cr>
+    au FileType lisp nnoremap <buffer> <silent> <localleader>Oe :call OpenLispReplECL()<cr>
+    au FileType lisp nnoremap <buffer> <silent> <localleader>Oa :call OpenLispReplABCL()<cr>
+
+    " Misc mappings
+    au FileType lisp nnoremap <buffer> gi :call IndentToplevelLispForm()<cr>
+    au FileType lisp nnoremap <buffer> <silent> <localleader>q :call QuickloadLispSystem()<cr>
+    au FileType lisp nnoremap <buffer> <silent> <localleader>Q :call QuickloadLispPrompt()<cr>
     au FileType lisp nnoremap <buffer> [] :call DuplicateLispForm()<cr>
 
-    " s/it/happening/
-    "
-    " Map the Ooze keys, then add a few extra on top to work with the REPL.
-    "
-    " [o]pen repl
-    " [s]end to repl
-    " [c]lear repl
-    au FileType lisp silent! call OozeMapKeys()
-    au FileType lisp nnoremap <buffer> <silent> <localleader>os :call OpenLispReplSBCL()<cr>
-    au FileType lisp nnoremap <buffer> <silent> <localleader>oc :call OpenLispReplCCL()<cr>
-    au FileType lisp nnoremap <buffer> <silent> <localleader>oe :call OpenLispReplECL()<cr>
-    au FileType lisp nnoremap <buffer> <silent> <localleader>oa :call OpenLispReplABCL()<cr>
-    au FileType lisp nnoremap <buffer> <silent> <localleader>s :call SendToplevelLispForm()<cr>
-    au FileType lisp nnoremap <buffer> <silent> <localleader>c :call NeoReplSendRaw("nil\n")<cr>:sleep 20m<cr>:call NeoReplSendRaw("")<cr>
-    au FileType lisp nnoremap <buffer> <silent> <localleader>Q :call NeoReplSendRaw("(load \"vendor/quickutils.lisp\")\n")<cr>
-    au FileType lisp nnoremap <buffer> <silent> <localleader>0 :call NeoReplSendRaw("0\n")<cr>
-    au FileType lisp nnoremap <buffer> <silent> <localleader>1 :call NeoReplSendRaw("1\n")<cr>
-    au FileType lisp nnoremap <buffer> <silent> <localleader>2 :call NeoReplSendRaw("2\n")<cr>
-    au FileType lisp nnoremap <buffer> <silent> <localleader>3 :call NeoReplSendRaw("3\n")<cr>
-    au FileType lisp nnoremap <buffer> gi :call IndentToplevelLispForm()<cr>
-    au FileType lisp nnoremap <buffer> <silent> <localleader>d :call DisassembleLispSymbol()<cr>
-    au FileType lisp nnoremap <buffer> <silent> <localleader>q :call QuickloadLispSystem()<cr>
-
     " Navigate trees of sexps with arrows
     au FileType lisp call s:vim_sexp_mappings()
     au FileType lisp noremap <buffer> <left>  :<c-u>call SexpBack()<cr>
@@ -1879,6 +1866,8 @@
     au FileType vim setlocal foldmethod=marker keywordprg=:help
     au FileType help setlocal textwidth=78
     au BufWinEnter *.txt if &ft == 'help' | wincmd L | endif
+
+    au FileType vim inoremap <c-n> <c-x><c-n>
 augroup END
 
 " }}}
@@ -2012,47 +2001,6 @@
 let delimitMate_expand_cr = 1
 
 " }}}
-" Deoplete {{{
-
-let g:deoplete#enable_at_startup = 1
-
-if !exists('g:deoplete#omni#input_patterns')
-    let g:deoplete#omni#input_patterns = {}
-endif
-
-let g:deoplete#sources = {}
-let g:deoplete#sources._ = []
-let g:deoplete#sources.lisp = ['buffer', 'tag']
-let g:deoplete#sources.cs = ['cs']
-
-" Unfuck deoplete's autocomplete menu
-" I hate computers so much
-inoremap <silent> <CR> <C-r>=<SID>unfuck_deoplete_enter()<CR>
-" inoremap <silent> ) <C-r>=<SID>unfuck_deoplete_rparen()<CR>
-
-function! s:unfuck_deoplete_enter() abort
-    return deoplete#close_popup() . "\<CR>"
-endfunction
-
-" function! s:unfuck_deoplete_rparen() abort
-"     return deoplete#close_popup() . ")"
-" endfunction
-
-" let g:deoplete#disable_auto_complete = 1
-
-" inoremap <silent><expr> <c-n>
-" \ pumvisible() ? "\<c-n>" :
-" \ <SID>check_back_space() ? "\<tab>" :
-" \ deoplete#mappings#manual_complete()
-
-" function! s:check_back_space() abort " {{{
-"     let col = col('.') - 1
-"     return !col || getline('.')[col - 1]  =~ '\s'
-" endfunction " }}}
-
-" autocmd InsertLeave,CompleteDone * if pumvisible() == 0 | pclose | endif
-
-" }}}
 " Dispatch {{{
 
 nnoremap <leader>d :Dispatch<cr>
@@ -2173,6 +2121,7 @@
                     \ '.*\.o$', 'db.db', 'tags.bak', '.*\.pdf$', '.*\.mid$',
                     \ '^tags$',
                     \ '^.*\.meta$',
+                    \ '^.*\.fasl$',
                     \ '.*\.bcf$', '.*\.blg$', '.*\.fdb_latexmk$', '.*\.bbl$', '.*\.aux$', '.*\.run.xml$', '.*\.fls$',
                     \ '.*\.midi$']
 
@@ -2202,6 +2151,10 @@
     " Also quit fucking with my save file mapping.
     nunmap <buffer> s
 
+    " Please just stop
+    nunmap <buffer> <leader>W
+    nunmap <buffer> <leader>O
+
     " Oh my god will you fuck off already
     " nnoremap <buffer> dp :diffput<cr>
     " nnoremap <buffer> do :diffobtain<cr>
@@ -2504,6 +2457,91 @@
 let g:targets_pairs = '()b {}B []r <>'
 
 " }}}
+" Vlime {{{
+
+let g:vlime_window_settings = {
+        \ "sldb": {
+            \ "pos": "topleft",
+            \ "vertical": v:true
+        \ },
+        \ "xref": {
+            \ "pos": "belowright",
+            \ "size": 5,
+            \ "vertical": v:false
+        \ },
+        \ "repl": {
+            \ "pos": "belowright",
+            \ "size": 80,
+            \ "vertical": v:true
+        \ },
+        \ "arglist": {
+            \ "pos": "topleft",
+            \ "size": 2,
+            \ "vertical": v:false
+        \ }
+    \ }
+
+let g:vlime_compiler_policy = {
+            \ "DEBUG": 3,
+            \ "SPEED": 1
+            \ }
+
+function! CleanVlimeWindows()
+    call vlime#plugin#CloseWindow("preview")
+    call vlime#plugin#CloseWindow("notes")
+    call vlime#plugin#CloseWindow("xref")
+endfunction
+
+function! MapVlimeKeys()
+    nnoremap <silent> <buffer> <c-]> :call vlime#plugin#FindDefinition(vlime#ui#CurAtom())<cr>
+    nnoremap <silent> <buffer> -     :call CleanVlimeWindows()<cr>
+endfunction
+
+augroup CustomVlimeInputBuffer
+    autocmd!
+    " autocmd FileType vlime_input inoremap <silent> <buffer> <tab> <c-r>=VlimeKey("tab")<cr>
+    autocmd FileType vlime_input setlocal omnifunc=VlimeCompleteFunc
+    " autocmd FileType vlime_input setlocal indentexpr=VlimeCalcCurIndent()
+    autocmd FileType vlime_input inoremap <c-n> <c-x><c-o>
+augroup end
+
+augroup LocalVlime
+    autocmd!
+
+    " Settings
+    au FileType vlime_sldb setlocal nowrap
+    au FileType vlime_repl setlocal nowrap
+
+    " Keys for Lisp files
+    au FileType lisp nnoremap <buffer> <localleader>e :call vlime#plugin#Compile(vlime#ui#CurTopExpr(v:true))<cr>
+    au FileType lisp nnoremap <buffer> <localleader>f :call vlime#plugin#CompileFile(expand("%:p"))<cr>
+    au FileType lisp nnoremap <buffer> <localleader>S :call vlime#plugin#SendToREPL(vlime#ui#CurTopExpr())<cr>
+    au FileType lisp nnoremap <buffer> <localleader>i :call vlime#plugin#Inspect(vlime#ui#CurExprOrAtom())<cr>
+    au FileType lisp nnoremap <buffer> M :call vlime#plugin#DocumentationSymbol(vlime#ui#CurOperator())<cr>
+
+    " Universal keys, for all kinds of Vlime windows
+    au FileType lisp,vlime_repl,vlime_inspector,vlime_sldb,vlime_notes,vlime_xref,vlime_preview call MapVlimeKeys()
+
+    " Fix <cr>
+    au FileType vlime_xref      nnoremap <buffer> <cr> :call vlime#ui#xref#OpenCurXref()<cr>
+    au FileType vlime_notes     nnoremap <buffer> <cr> :call vlime#ui#compiler_notes#OpenCurNote()<cr>
+    au FileType vlime_sldb      nnoremap <buffer> <cr> :call vlime#ui#sldb#ChooseCurRestart()<cr>
+    au FileType vlime_inspector nnoremap <buffer> <cr> :call vlime#ui#inspector#InspectorSelect()<cr>
+
+    " Fix d
+    au FileType vlime_sldb      nnoremap <buffer> <nowait> d :call vlime#ui#sldb#ShowFrameDetails()<cr>
+
+    " Fix p
+    au FileType vlime_inspector nnoremap <buffer> p :call vlime#ui#inspector#InspectorPop()<cr>
+augroup end
+
+" }}}
+" Windowswap {{{
+
+let g:windowswap_map_keys = 0 "prevent default bindings
+nnoremap <silent> <leader>W :call WindowSwap#EasyWindowSwap()<CR>
+
+" }}}
 
 " }}}
 " Text objects ------------------------------------------------------------ {{{
@@ -2599,7 +2637,7 @@
 endfunc
 
 " TODO: Figure out the diffexpr shit necessary to make this buffer-local.
-nnoremap <leader>W :call ToggleDiffWhitespace()<CR>
+" nnoremap <leader>W :call ToggleDiffWhitespace()<CR>
 
 " }}}
 " Error Toggles {{{
--- a/weechat/python/autoload/wee_slack.py	Fri Jun 23 13:41:39 2017 +0000
+++ b/weechat/python/autoload/wee_slack.py	Tue Jul 04 15:25:03 2017 +0000
@@ -1,39 +1,41 @@
 # -*- coding: utf-8 -*-
-#
+
+from __future__ import unicode_literals
 
 from functools import wraps
 
 import time
 import json
-import os
 import pickle
 import sha
+import os
 import re
 import urllib
-import HTMLParser
 import sys
 import traceback
 import collections
 import ssl
+import random
+import string
 
 from websocket import create_connection, WebSocketConnectionClosedException
 
 # hack to make tests possible.. better way?
 try:
-    import weechat as w
+    import weechat
 except:
     pass
 
-SCRIPT_NAME = "slack_extension"
+SCRIPT_NAME = "slack"
 SCRIPT_AUTHOR = "Ryan Huber <rhuber@gmail.com>"
-SCRIPT_VERSION = "0.99.9"
+SCRIPT_VERSION = "1.99"
 SCRIPT_LICENSE = "MIT"
 SCRIPT_DESC = "Extends weechat for typing notification/search/etc on slack.com"
 
 BACKLOG_SIZE = 200
 SCROLLBACK_SIZE = 500
 
-CACHE_VERSION = "4"
+RECORD_DIR = "/tmp/weeslack-debug"
 
 SLACK_API_TRANSLATOR = {
     "channel": {
@@ -54,10 +56,44 @@
         "join": "channels.join",
         "leave": "groups.leave",
         "mark": "groups.mark",
+    },
+    "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:
+            return w.WEECHAT_RC_ERROR
+        return f(data, current_buffer, *args, **kwargs)
+    return wrapper
+
+
 NICK_GROUP_HERE = "0|Here"
 NICK_GROUP_AWAY = "1|Away"
 
@@ -67,334 +103,507 @@
     if ssl_defaults.cafile is not None:
         sslopt_ca_certs = {'ca_certs': ssl_defaults.cafile}
 
-
-def dbg(message, fout=False, main_buffer=False):
-    """
-    send debug output to the slack-debug buffer and optionally write to a file.
-    """
-    message = "DEBUG: {}".format(message)
-    # message = message.encode('utf-8', 'replace')
-    if fout:
-        file('/tmp/debug.log', 'a+').writelines(message + '\n')
-    if main_buffer:
-            w.prnt("", "slack: " + message)
+###### Unicode handling
+
+
+def encode_to_utf8(data):
+    if isinstance(data, unicode):
+        return data.encode('utf-8')
+    if isinstance(data, bytes):
+        return data
+    elif isinstance(data, collections.Mapping):
+        return dict(map(encode_to_utf8, data.iteritems()))
+    elif isinstance(data, collections.Iterable):
+        return type(data)(map(encode_to_utf8, data))
     else:
-        if slack_debug is not None:
-            w.prnt(slack_debug, message)
-
-
-class SearchList(list):
-    """
-    A normal python list with some syntactic sugar for searchability
-    """
-    def __init__(self):
-        self.hashtable = {}
-        super(SearchList, self).__init__(self)
-
-    def find(self, name):
-        if name in self.hashtable:
-            return self.hashtable[name]
-        # this is a fallback to __eq__ if the item isn't in the hashtable already
-        if name in self:
-            self.update_hashtable()
-            return self[self.index(name)]
-
-    def append(self, item, aliases=[]):
-        super(SearchList, self).append(item)
-        self.update_hashtable(item)
-
-    def update_hashtable(self, item=None):
-        if item is not None:
-            try:
-                for alias in item.get_aliases():
-                    if alias is not None:
-                        self.hashtable[alias] = item
-            except AttributeError:
-                pass
+        return data
+
+
+def decode_from_utf8(data):
+    if isinstance(data, bytes):
+        return data.decode('utf-8')
+    if isinstance(data, unicode):
+        return data
+    elif isinstance(data, collections.Mapping):
+        return dict(map(decode_from_utf8, data.iteritems()))
+    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
+
+    def __getattr__(self, attr):
+        orig_attr = self.wrapped_class.__getattribute__(attr)
+        if callable(orig_attr):
+            def hooked(*args, **kwargs):
+                result = orig_attr(*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
         else:
-            for child in self:
-                try:
-                    for alias in child.get_aliases():
-                        if alias is not None:
-                            self.hashtable[alias] = child
-                except AttributeError:
-                    pass
-
-    def find_by_class(self, class_name):
-        items = []
-        for child in self:
-            if child.__class__ == class_name:
-                items.append(child)
-        return items
-
-    def find_by_class_deep(self, class_name, attribute):
-        items = []
-        for child in self:
-            if child.__class__ == self.__class__:
-                items += child.find_by_class_deep(class_name, attribute)
-            else:
-                items += (eval('child.' + attribute).find_by_class(class_name))
-        return items
-
-
-class SlackServer(object):
-    """
-    Root object used to represent connection and state of the connection to a slack group.
-    """
-    def __init__(self, token):
-        self.nick = None
-        self.name = None
-        self.team = None
-        self.domain = None
-        self.server_buffer_name = None
-        self.login_data = None
-        self.buffer = None
-        self.token = token
-        self.ws = None
-        self.ws_hook = None
-        self.users = SearchList()
-        self.bots = SearchList()
-        self.channels = SearchList()
-        self.connecting = False
-        self.connected = False
-        self.connection_attempt_time = 0
-        self.communication_counter = 0
-        self.message_buffer = {}
-        self.ping_hook = None
-        self.alias = None
-        self.got_history = False
-
-        self.identifier = None
-        self.connect_to_slack()
-
-    def __eq__(self, compare_str):
-        if compare_str == self.identifier or compare_str == self.token or compare_str == self.buffer:
-            return True
+            return decode_from_utf8(orig_attr)
+
+
+##### BEGIN NEW
+
+IGNORED_EVENTS = [
+    "hello",
+    # "pref_change",
+    # "reconnect_url",
+]
+
+###### 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.context = {}
+        self.weechat_controller = WeechatController(self)
+        self.previous_buffer = ""
+        self.reply_buffer = {}
+        self.cmds = {k[8:]: v for k, v in globals().items() if k.startswith("command_")}
+        self.proc = {k[8:]: v for k, v in globals().items() if k.startswith("process_")}
+        self.handlers = {k[7:]: v for k, v in globals().items() if k.startswith("handle_")}
+        self.local_proc = {k[14:]: v for k, v in globals().items() if k.startswith("local_process_")}
+        self.shutting_down = False
+        self.recording = False
+        self.recording_path = "/tmp"
+
+    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:
-            return False
-
-    def __str__(self):
-        return "{}".format(self.identifier)
-
-    def __repr__(self):
-        return "{}".format(self.identifier)
-
-    def add_user(self, user):
-        self.users.append(user, user.get_aliases())
-        users.append(user, user.get_aliases())
-
-    def add_bot(self, bot):
-        self.bots.append(bot)
-
-    def add_channel(self, channel):
-        self.channels.append(channel, channel.get_aliases())
-        channels.append(channel, channel.get_aliases())
-
-    def get_aliases(self):
-        aliases = filter(None, [self.identifier, self.token, self.buffer, self.alias])
-        return aliases
-
-    def find(self, name, attribute):
-        attribute = eval("self." + attribute)
-        return attribute.find(name)
-
-    def get_communication_id(self):
-        if self.communication_counter > 999:
-            self.communication_counter = 0
-        self.communication_counter += 1
-        return self.communication_counter
-
-    def send_to_websocket(self, data, expect_reply=True):
-        data["id"] = self.get_communication_id()
-        message = json.dumps(data)
+            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.
+        """
+        data = self.context.get(identifier, None)
+        if data:
+            # dbg("retrieved context {} ".format(identifier))
+            return data
+
+    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:
+            # dbg("deleted eontext {} ".format(identifier))
+            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_id, team in self.teams.iteritems():
+            if not team.connected:
+                team.connect()
+                dbg("reconnecting {}".format(team))
+
+    def receive_ws_callback(self, team_hash):
+        """
+        incomplete (reconnect)
+        This is called by the global method of the same name.
+        It is triggered when we have incoming data on a websocket,
+        which needs to be read. Once it is read, we will ensure
+        the data is valid JSON, add metadata, and place it back
+        on the queue for processing as JSON.
+        """
         try:
-            if expect_reply:
-                self.message_buffer[data["id"]] = data
-            self.ws.send(message)
-            dbg("Sent {}...".format(message[:100]))
+            # Read the data from the websocket associated with this team.
+            data = decode_from_utf8(self.teams[team_hash].ws.recv())
+            message_json = json.loads(data)
+            metadata = WeeSlackMetadata({
+                "team": team_hash,
+            }).jsonify()
+            message_json["wee_slack_metadata"] = metadata
+            if self.recording:
+                self.record_event(message_json, 'type', 'websocket')
+            self.receive_json(json.dumps(message_json))
+        except WebSocketConnectionClosedException:
+            # TODO: handle reconnect here
+            self.teams[team_hash].set_disconnected()
+            return w.WEECHAT_RC_OK
+        except Exception:
+            dbg("socket issue: {}\n".format(traceback.format_exc()))
+            return w.WEECHAT_RC_OK
+
+    def receive_httprequest_callback(self, data, command, return_code, out, err):
+        """
+        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)
+        try:
+            dbg("RECEIVED CALLBACK with request of {} id of {} and  code {} of length {}".format(request_metadata.request, request_metadata.response_id, return_code, len(out)))
         except:
-            dbg("Unexpected error: {}\nSent: {}".format(sys.exc_info()[0], data))
-            self.connected = False
-
-    def ping(self):
-        request = {"type": "ping"}
-        self.send_to_websocket(request)
-
-    def should_connect(self):
-        """
-        If we haven't tried to connect OR we tried and never heard back and it
-        has been 125 seconds consider the attempt dead and try again
-        """
-        if self.connection_attempt_time == 0 or self.connection_attempt_time + 125 < int(time.time()):
-            return True
-        else:
-            return False
-
-    def connect_to_slack(self):
-        t = time.time()
-        # Double check that we haven't exceeded a long wait to connect and try again.
-        if self.connecting and self.should_connect():
-            self.connecting = False
-        if not self.connecting:
-            async_slack_api_request("slack.com", self.token, "rtm.start", {"ts": t})
-            self.connection_attempt_time = int(time.time())
-            self.connecting = True
-
-    def connected_to_slack(self, login_data):
-        if login_data["ok"]:
-            self.team = login_data["team"]["domain"]
-            self.domain = login_data["team"]["domain"] + ".slack.com"
-            dbg("connected to {}".format(self.domain))
-            self.identifier = self.domain
-
-            alias = w.config_get_plugin("server_alias.{}".format(login_data["team"]["domain"]))
-            if alias:
-                self.server_buffer_name = alias
-                self.alias = alias
+            dbg(request_metadata)
+            return
+        if return_code == 0:
+            if len(out) > 0:
+                if request_metadata.response_id in self.reply_buffer:
+                    # dbg("found response id in reply_buffer", True)
+                    self.reply_buffer[request_metadata.response_id] += out
+                else:
+                    # dbg("didn't find response id in reply_buffer", True)
+                    self.reply_buffer[request_metadata.response_id] = ""
+                    self.reply_buffer[request_metadata.response_id] += out
+                try:
+                    j = json.loads(self.reply_buffer[request_metadata.response_id])
+                except:
+                    pass
+                    # dbg("Incomplete json, awaiting more", True)
+                try:
+                    j["wee_slack_process_method"] = request_metadata.request_normalized
+                    j["wee_slack_request_metadata"] = pickle.dumps(request_metadata)
+                    self.reply_buffer.pop(request_metadata.response_id)
+                    if self.recording:
+                        self.record_event(j, 'wee_slack_process_method', 'http')
+                    self.receive_json(json.dumps(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:
-                self.server_buffer_name = self.domain
-
-            self.nick = login_data["self"]["name"]
-            self.create_local_buffer()
-
-            if self.create_slack_websocket(login_data):
-                if self.ping_hook:
-                    w.unhook(self.ping_hook)
-                    self.communication_counter = 0
-                self.ping_hook = w.hook_timer(1000 * 5, 0, 0, "slack_ping_cb", self.domain)
-                if len(self.users) == 0 or len(self.channels) == 0:
-                    self.create_slack_mappings(login_data)
-
-                self.connected = True
-                self.connecting = False
-
-                self.print_connection_info(login_data)
-                if len(self.message_buffer) > 0:
-                    for message_id in self.message_buffer.keys():
-                        if self.message_buffer[message_id]["type"] != 'ping':
-                            resend = self.message_buffer.pop(message_id)
-                            dbg("Resent failed message.")
-                            self.send_to_websocket(resend)
-                            # sleep to prevent being disconnected by websocket server
-                            time.sleep(1)
-                        else:
-                            self.message_buffer.pop(message_id)
-            for chan in self.channels:
-                # Set channel history back to false because we will miss messages that came
-                # while we were disconnected otherwise.
-                chan.got_history = False
-                if chan.channel_buffer and chan.muted:
-                    w.buffer_set(chan.channel_buffer, "hotlist", "-1")
-            return True
+                dbg("length was zero, probably a bug..")
+                self.delete_context(data)
+                self.receive(request_metadata)
+        elif return_code != -1:
+            self.reply_buffer.pop(request_metadata.response_id, None)
+            self.delete_context(data)
         else:
-            token_start = self.token[:10]
-            error = """
-!! slack.com login error: {}
- The problematic token starts with {}
- Please check your API token with
- "/set plugins.var.python.slack_extension.slack_api_token (token)"
-
-""".format(login_data["error"], token_start)
-            w.prnt("", error)
-            self.connected = False
-
-    def print_connection_info(self, login_data):
-        self.buffer_prnt('Connected to Slack', backlog=True)
-        self.buffer_prnt('{:<20} {}'.format(u"Websocket URL", login_data["url"]), backlog=True)
-        self.buffer_prnt('{:<20} {}'.format(u"User name", login_data["self"]["name"]), backlog=True)
-        self.buffer_prnt('{:<20} {}'.format(u"User ID", login_data["self"]["id"]), backlog=True)
-        self.buffer_prnt('{:<20} {}'.format(u"Team name", login_data["team"]["name"]), backlog=True)
-        self.buffer_prnt('{:<20} {}'.format(u"Team domain", login_data["team"]["domain"]), backlog=True)
-        self.buffer_prnt('{:<20} {}'.format(u"Team id", login_data["team"]["id"]), backlog=True)
-
-    def create_local_buffer(self):
-        if not w.buffer_search("", self.server_buffer_name):
-            self.buffer = w.buffer_new(self.server_buffer_name, "buffer_input_cb", "", "", "")
-            if w.config_string(w.config_get('irc.look.server_buffer')) == 'merge_with_core':
-                w.buffer_merge(self.buffer, w.buffer_search_main())
-            w.buffer_set(self.buffer, "nicklist", "1")
-
-    def create_slack_websocket(self, data):
-        web_socket_url = data['url']
-        try:
-            self.ws = create_connection(web_socket_url, sslopt=sslopt_ca_certs)
-            self.ws_hook = w.hook_fd(self.ws.sock._sock.fileno(), 1, 0, 0, "slack_websocket_cb", self.identifier)
-            self.ws.sock.setblocking(0)
-            return True
-        except Exception as e:
-            print("websocket connection error: {}".format(e))
-            return False
-
-    def create_slack_mappings(self, data):
-
-        for item in data["users"]:
-            self.add_user(User(self, item["name"], item["id"], item["presence"], item["deleted"], is_bot=item.get('is_bot', False)))
-
-        for item in data["bots"]:
-            self.add_bot(Bot(self, item["name"], item["id"], item["deleted"]))
-
-        for item in data["channels"]:
-            item["is_open"] = item["is_member"]
-            item["prepend_name"] = "#"
-            if not item["is_archived"]:
-                self.add_channel(Channel(self, **item))
-
-        for item in data["groups"]:
-            item["prepend_name"] = "#"
-            if not item["is_archived"]:
-                if item["name"].startswith("mpdm-"):
-                    self.add_channel(MpdmChannel(self, **item))
+            if request_metadata.response_id not in self.reply_buffer:
+                self.reply_buffer[request_metadata.response_id] = ""
+            self.reply_buffer[request_metadata.response_id] += out
+
+    def receive_json(self, data):
+        """
+        complete
+        Receives a raw JSON string from and unmarshals it
+        as dict, then places it back on the queue for processing.
+        """
+        dbg("RECEIVED JSON of len {}".format(len(data)))
+        message_json = json.loads(data)
+        self.queue.append(message_json)
+
+    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.
+        """
+        if len(self.slow_queue) > 0 and ((self.slow_queue_timer + 1) < time.time()):
+            # for q in self.slow_queue[0]:
+            dbg("from slow queue", 0)
+            self.queue.append(self.slow_queue.pop())
+            # self.slow_queue = []
+            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:
-                    self.add_channel(GroupChannel(self, **item))
-
-        for item in data["ims"]:
-            if item["unread_count"] > 0 or item["is_open"]:
-                item["is_open"] = True
-            item['name'] = self.users.find(item["user"]).name
-            self.add_channel(DmChannel(self, **item))
-
-        for item in data['self']['prefs']['muted_channels'].split(','):
-            if item == '':
-                continue
-            maybe_muted_chan = self.channels.find(item)
-            if maybe_muted_chan is not None:
-                maybe_muted_chan.muted = True
-
-        #for item in self.channels:
-        #    item.get_history()
-
-    def buffer_prnt(self, message='no message', user="SYSTEM", backlog=False):
-        message = message.encode('ascii', 'ignore')
-        if backlog:
-            tags = "no_highlight,notify_none,logger_backlog_end"
-        else:
-            tags = ""
-        if user == "SYSTEM":
-            user = w.config_string(w.config_get('weechat.look.prefix_network'))
-        if self.buffer:
-            w.prnt_date_tags(self.buffer, 0, tags, "{}\t{}".format(user, message))
+                    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"
+
+                # Here we are passing the actual objects. No more lookups.
+                meta = j.get("wee_slack_metadata", None)
+                if meta:
+                    try:
+                        if isinstance(meta, basestring):
+                            dbg("string of metadata")
+                        team = meta.get("team", None)
+                        if team:
+                            kwargs["team"] = self.teams[team]
+                            if "user" in j:
+                                kwargs["user"] = self.teams[team].users[j["user"]]
+                            if "channel" in j:
+                                kwargs["channel"] = self.teams[team].channels[j["channel"]]
+                    except:
+                        dbg("metadata failure")
+
+                if function_name not in IGNORED_EVENTS:
+                    dbg("running {}".format(function_name))
+                    if function_name.startswith("local_") and function_name in self.local_proc:
+                        self.local_proc[function_name](j, self, **kwargs)
+                    elif function_name in self.proc:
+                        self.proc[function_name](j, self, **kwargs)
+                    elif function_name in self.handlers:
+                        self.handlers[function_name](j, self, **kwargs)
+                    else:
+                        raise ProcessNotImplemented(function_name)
+
+
+def handle_next(*args):
+    """
+    complete
+    This is just a place to call the event router globally.
+    This is a dirty hack. There must be a better way.
+    """
+    try:
+        EVENTROUTER.handle_next()
+    except:
+        if config.debug_mode:
+            traceback.print_exc()
         else:
             pass
-            # w.prnt("", "%s\t%s" % (user, message))
-
-    def set_away(self, msg):
-        async_slack_api_request(self.domain, self.token, 'presence.set', {"presence": "away"})
-        for c in self.channels:
-            if c.channel_buffer is not None:
-                w.buffer_set(c.channel_buffer, "localvar_set_away", msg)
-
-    def set_active(self):
-        async_slack_api_request(self.domain, self.token, 'presence.set', {"presence": "active"})
-        for c in self.channels:
-            if c.channel_buffer is not None:
-                w.buffer_set(c.channel_buffer, "localvar_set_away", '')
-                w.buffer_set(c.channel_buffer, "localvar_del_away", '')
-
-
-def buffer_input_cb(b, buffer, data):
-    channel = channels.find(buffer)
+    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
+        """
+        if isinstance(buffer_ptr, basestring):
+            try:
+                self.buffers[buffer_ptr].destroy_buffer(update_remote)
+                if close_buffer:
+                    w.buffer_close(buffer_ptr)
+                del self.buffers[buffer_ptr]
+            except:
+                dbg("Tried to close unknown buffer")
+        else:
+            raise InvalidType(type(buffer_ptr))
+
+    def get_channel_from_buffer_ptr(self, buffer_ptr):
+        return self.buffers.get(buffer_ptr, None)
+
+    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
+
+
+def receive_httprequest_callback(data, command, return_code, out, err):
+    """
+    complete
+    This is a dirty hack. There must be a better way.
+    """
+    # def url_processor_cb(data, command, return_code, out, err):
+    data = decode_from_utf8(data)
+    EVENTROUTER.receive_httprequest_callback(data, command, return_code, out, err)
+    return w.WEECHAT_RC_OK
+
+
+def receive_ws_callback(*args):
+    """
+    complete
+    The first arg is all we want here. It contains the team
+    hash which is set when we _hook the descriptor.
+    This is a dirty hack. There must be a better way.
+    """
+    EVENTROUTER.receive_ws_callback(args[0])
+    return w.WEECHAT_RC_OK
+
+
+def reconnect_callback(*args):
+    EVENTROUTER.reconnect_if_disconnected()
+    return w.WEECHAT_RC_OK
+
+
+def buffer_closing_callback(signal, sig_type, data):
+    """
+    complete
+    Receives a callback from weechat when a buffer is being closed.
+    We pass the eventrouter variable name in as a string, as
+    that is the only way we can do dependency injection via weechat
+    callback, hence the eval.
+    """
+    data = decode_from_utf8(data)
+    eval(signal).weechat_controller.unregister_buffer(data, True, False)
+    return w.WEECHAT_RC_OK
+
+
+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.
+    """
+    data = decode_from_utf8(data)
+    eventrouter = eval(signal)
+    channel = eventrouter.weechat_controller.get_channel_from_buffer_ptr(buffer_ptr)
     if not channel:
         return w.WEECHAT_RC_OK_EAT
+
     reaction = re.match("^\s*(\d*)(\+|-):(.*):\s*$", data)
     if reaction:
         if reaction.group(2) == "+":
@@ -411,392 +620,747 @@
             # rid of escapes.
             new = new.replace(r'\/', '/')
             old = old.replace(r'\/', '/')
-            channel.change_previous_message(old.decode("utf-8"), new.decode("utf-8"), flags)
+            channel.edit_previous_message(old, new, flags)
     else:
         channel.send_message(data)
-        # channel.buffer_prnt(channel.server.nick, data)
-    channel.mark_read(True)
+        # this is probably wrong channel.mark_read(update_remote=True, force=True)
     return w.WEECHAT_RC_ERROR
 
 
-class Channel(object):
+def buffer_switch_callback(signal, sig_type, data):
+    """
+    incomplete
+    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
+    """
+    data = decode_from_utf8(data)
+    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()
+
+    eventrouter.weechat_controller.set_previous_buffer(data)
+    return w.WEECHAT_RC_OK
+
+
+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".
     """
-    Represents a single channel and is the source of truth
-    for channel <> weechat buffer
+    data = decode_from_utf8(data)
+    eventrouter = eval(data)
+    # global buffer_list_update
+
+    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()
+
+
+def typing_notification_cb(signal, sig_type, data):
+    data = decode_from_utf8(data)
+    msg = w.buffer_get_string(data, "input")
+    if len(msg) > 8 and msg[:1] != "/":
+        global typing_timer
+        now = time.time()
+        if typing_timer + 4 < now:
+            current_buffer = w.current_buffer()
+            channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer, None)
+            if channel:
+                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
+
+
+def typing_update_cb(data, remaining_calls):
+    data = decode_from_utf8(data)
+    w.bar_item_update("slack_typing_notice")
+    return w.WEECHAT_RC_OK
+
+
+def slack_never_away_cb(data, remaining_calls):
+    data = decode_from_utf8(data)
+    if config.never_away:
+        for t in EVENTROUTER.teams.values():
+            slackbot = t.get_channel_map()['slackbot']
+            channel = t.channels[slackbot]
+            request = {"type": "typing", "channel": channel.identifier}
+            channel.team.send_to_websocket(request, expect_reply=False)
+    return w.WEECHAT_RC_OK
+
+
+def typing_bar_item_cb(data, current_buffer, args):
+    """
+    Privides a bar item indicating who is typing in the current channel AND
+    why is typing a DM to you globally.
+    """
+    typers = []
+    current_buffer = w.current_buffer()
+    current_channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer, None)
+
+    # 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 t in EVENTROUTER.teams.values():
+        for channel in t.channels.values():
+            if channel.type == "im":
+                if channel.is_someone_typing():
+                    typers.append("D/" + channel.slack_name)
+                pass
+
+    typing = ", ".join(typers)
+    if typing != "":
+        typing = w.color('yellow') + "typing: " + typing
+
+    return typing
+
+
+def nick_completion_cb(data, completion_item, current_buffer, completion):
+    """
+    Adds all @-prefixed nicks to completion list
+    """
+
+    data = decode_from_utf8(data)
+    completion = decode_from_utf8(completion)
+    current_buffer = w.current_buffer()
+    current_channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer, None)
+
+    if current_channel is None or current_channel.members is None:
+        return w.WEECHAT_RC_OK
+    for m in current_channel.members:
+        u = current_channel.team.users.get(m, None)
+        if u:
+            w.hook_completion_list_add(completion, "@" + u.slack_name, 1, w.WEECHAT_LIST_POS_SORT)
+    return w.WEECHAT_RC_OK
+
+
+def emoji_completion_cb(data, completion_item, current_buffer, completion):
+    """
+    Adds all :-prefixed emoji to completion list
     """
-    #def __init__(self, server, name, identifier, active, last_read=0, prepend_name="", members=[], topic="", unread_count=0):
-    def __init__(self, server, **kwargs):
-
-        self.name = kwargs.get('prepend_name', "") + kwargs.get('name')
-        self.current_short_name = kwargs.get('prepend_name', "") + kwargs.get('name')
-        self.identifier = kwargs.get('id', 0)
-        self.active = kwargs.get('is_open', False)
-        self.last_read = float(kwargs.get('last_read', 0))
-        self.members = set(kwargs.get('members', []))
-        self.topic = kwargs.get('topic', {"value": ""})["value"]
-        self.unread_count = kwargs.get('unread_count_display', 0)
-
-        self.members_table = {}
+
+    data = decode_from_utf8(data)
+    completion = decode_from_utf8(completion)
+    current_buffer = w.current_buffer()
+    current_channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer, None)
+
+    if current_channel is None:
+        return w.WEECHAT_RC_OK
+    for e in EMOJI['emoji']:
+        w.hook_completion_list_add(completion, ":" + e + ":", 0, w.WEECHAT_LIST_POS_SORT)
+    return w.WEECHAT_RC_OK
+
+
+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
+
+    """
+
+    data = decode_from_utf8(data)
+    command = decode_from_utf8(data)
+    current_buffer = w.current_buffer()
+    current_channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer, None)
+
+    # channel = channels.find(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 m in current_channel.members:
+        u = current_channel.team.users.get(m, None)
+        if u and u.slack_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()
+    return w.WEECHAT_RC_OK
+
+##### New Classes
+
+
+class SlackRequest(object):
+    """
+    complete
+    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, token, request, post_data={}, **kwargs):
+        for key, value in kwargs.items():
+            setattr(self, key, value)
+        self.tries = 0
+        self.start_time = time.time()
+        self.domain = 'api.slack.com'
+        self.request = request
+        self.request_normalized = re.sub(r'\W+', '', request)
+        self.token = token
+        post_data["token"] = token
+        self.post_data = post_data
+        self.params = {'useragent': 'wee_slack {}'.format(SCRIPT_VERSION)}
+        self.url = 'https://{}/api/{}?{}'.format(self.domain, request, urllib.urlencode(encode_to_utf8(post_data)))
+        self.response_id = sha.sha("{}{}".format(self.url, self.start_time)).hexdigest()
+        self.retries = kwargs.get('retries', 3)
+#    def __repr__(self):
+#        return "URL: {} Tries: {} ID: {}".format(self.url, self.tries, self.response_id)
+
+    def request_string(self):
+        return "{}".format(self.url)
+
+    def tried(self):
+        self.tries += 1
+        self.response_id = sha.sha("{}{}".format(self.url, time.time())).hexdigest()
+
+    def should_try(self):
+        return self.tries < self.retries
+
+    def retry_ready(self):
+        return (self.start_time + (self.tries**2)) < time.time()
+
+
+class SlackTeam(object):
+    """
+    incomplete
+    Team object under which users and channels live.. Does lots.
+    """
+
+    def __init__(self, eventrouter, token, websocket_url, subdomain, nick, myidentifier, users, bots, channels, **kwargs):
+        self.ws_url = websocket_url
+        self.connected = False
+        self.connecting = False
+        # self.ws = None
+        self.ws_counter = 0
+        self.ws_replies = {}
+        self.eventrouter = eventrouter
+        self.token = token
+        self.team = self
+        self.subdomain = subdomain
+        self.domain = subdomain + ".slack.com"
+        self.preferred_name = self.domain
+        self.nick = nick
+        self.myidentifier = myidentifier
+        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.type = "channel"
-        self.server = server
-        self.typing = {}
-        self.last_received = None
-        self.messages = []
-        self.scrolling = False
-        self.last_active_user = None
-        self.muted = False
-        self.got_history = False
-        #w.prnt("", "unread: {}".format(self.unread_count))
-        if self.active:
-            self.create_buffer()
-            self.attach_buffer()
-            self.create_members_table()
-            self.update_nicklist()
-            self.set_topic(self.topic)
-            buffer_list_update_next()
-
-    def __str__(self):
-        return self.name
-
-    def __repr__(self):
-        return self.name
+        self.got_history = True
+        self.create_buffer()
+        self.set_muted_channels(kwargs.get('muted_channels', ""))
+        for c in self.channels.keys():
+            channels[c].set_related_server(self)
+            channels[c].check_should_open()
+        #    self.channel_set_related_server(c)
+        # 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.set_highlight_words(kwargs.get('highlight_words', ""))
 
     def __eq__(self, compare_str):
-        if compare_str == self.fullname() or compare_str == self.name or compare_str == self.identifier or compare_str == self.name[1:] or (compare_str == self.channel_buffer and self.channel_buffer is not None):
+        if compare_str == self.token or compare_str == self.domain or compare_str == self.subdomain:
             return True
         else:
             return False
 
-    def get_aliases(self):
-        aliases = [self.fullname(), self.name, self.identifier, self.name[1:], ]
-        if self.channel_buffer is not None:
-            aliases.append(self.channel_buffer)
-        return aliases
-
-    def create_members_table(self):
-        for user in self.members:
-            self.members_table[user] = self.server.users.find(user)
+    def add_channel(self, channel):
+        self.channels[channel["id"]] = channel
+        channel.set_related_server(self)
+
+    # def connect_request_generate(self):
+    #    return SlackRequest(self.token, 'rtm.start', {})
+
+    # def close_all_buffers(self):
+    #    for channel in self.channels:
+    #        self.eventrouter.weechat_controller.unregister_buffer(channel.channel_buffer, update_remote=False, close_buffer=True)
+    #    #also close this server buffer
+    #    self.eventrouter.weechat_controller.unregister_buffer(self.channel_buffer, update_remote=False, close_buffer=True)
 
     def create_buffer(self):
-        channel_buffer = w.buffer_search("", "{}.{}".format(self.server.server_buffer_name, self.name))
-        if channel_buffer:
-            self.channel_buffer = channel_buffer
+        if not self.channel_buffer:
+            if config.short_buffer_names:
+                self.preferred_name = self.subdomain
+            elif config.server_aliases not in ['', None]:
+                name = config.server_aliases.get(self.subdomain, None)
+                if name:
+                    self.preferred_name = name
+            else:
+                self.preferred_name = self.domain
+            self.channel_buffer = w.buffer_new("{}".format(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')
+            if w.config_string(w.config_get('irc.look.server_buffer')) == 'merge_with_core':
+                w.buffer_merge(self.channel_buffer, w.buffer_search_main())
+            w.buffer_set(self.channel_buffer, "nicklist", "1")
+
+    def set_muted_channels(self, muted_str):
+        self.muted_channels = {x for x in muted_str.split(',')}
+
+    def set_highlight_words(self, highlight_str):
+        self.highlight_words = {x for x in highlight_str.split(',')}
+        if len(self.highlight_words) > 0:
+            for v in self.channels.itervalues():
+                v.set_highlights()
+
+    def formatted_name(self, **kwargs):
+        return self.domain
+
+    def buffer_prnt(self, data):
+        w.prnt_date_tags(self.channel_buffer, SlackTS().major, tag("backlog"), data)
+
+    def get_channel_map(self):
+        return {v.slack_name: k for k, v in self.channels.iteritems()}
+
+    def get_username_map(self):
+        return {v.slack_name: k for k, v in self.users.iteritems()}
+
+    def get_team_hash(self):
+        return self.team_hash
+
+    @staticmethod
+    def generate_team_hash(nick, subdomain):
+        return str(sha.sha("{}{}".format(nick, subdomain)).hexdigest())
+
+    def refresh(self):
+        self.rename()
+
+    def rename(self):
+        pass
+
+    # def attach_websocket(self, ws):
+    #    self.ws = ws
+
+    def is_user_present(self, user_id):
+        user = self.users.get(user_id)
+        if user.presence == 'active':
+            return True
         else:
-            self.channel_buffer = w.buffer_new("{}.{}".format(self.server.server_buffer_name, self.name), "buffer_input_cb", self.name, "", "")
+            return False
+
+    def mark_read(self):
+        pass
+
+    def connect(self):
+        if not self.connected and not self.connecting:
+            self.connecting = True
+            if self.ws_url:
+                try:
+                    ws = create_connection(self.ws_url, sslopt=sslopt_ca_certs)
+                    w.hook_fd(ws.sock._sock.fileno(), 1, 0, 0, "receive_ws_callback", self.get_team_hash())
+                    ws.sock.setblocking(0)
+                    self.ws = ws
+                    # self.attach_websocket(ws)
+                    self.set_connected()
+                    self.connecting = False
+                except Exception as e:
+                    dbg("websocket connection error: {}".format(e))
+                    self.connecting = False
+                    return False
+            else:
+                # The fast reconnect failed, so start over-ish
+                for chan in self.channels:
+                    self.channels[chan].got_history = False
+                s = SlackRequest(self.token, 'rtm.start', {}, retries=999)
+                self.eventrouter.receive(s)
+                self.connecting = False
+                # del self.eventrouter.teams[self.get_team_hash()]
+            self.set_reconnect_url(None)
+
+    def set_connected(self):
+        self.connected = True
+
+    def set_disconnected(self):
+        self.connected = False
+
+    def set_reconnect_url(self, url):
+        self.ws_url = url
+
+    def next_ws_transaction_id(self):
+        if self.ws_counter > 999:
+            self.ws_counter = 0
+        self.ws_counter += 1
+        return self.ws_counter
+
+    def send_to_websocket(self, data, expect_reply=True):
+        data["id"] = self.next_ws_transaction_id()
+        message = json.dumps(data)
+        try:
+            if expect_reply:
+                self.ws_replies[data["id"]] = data
+            self.ws.send(encode_to_utf8(message))
+            dbg("Sent {}...".format(message[:100]))
+        except:
+            print "WS ERROR"
+            dbg("Unexpected error: {}\nSent: {}".format(sys.exc_info()[0], data))
+            self.set_connected()
+
+
+class SlackChannel(object):
+    """
+    Represents an individual slack channel.
+    """
+
+    def __init__(self, eventrouter, **kwargs):
+        # We require these two things for a vaid object,
+        # the rest we can just learn from slack
+        self.active = False
+        for key, value in kwargs.items():
+            setattr(self, key, value)
+        self.members = set(kwargs.get('members', set()))
+        self.eventrouter = eventrouter
+        self.slack_name = kwargs["name"]
+        self.slack_topic = kwargs.get("topic", {"value": ""})
+        self.slack_purpose = kwargs.get("purpose", {"value": ""})
+        self.identifier = kwargs["id"]
+        self.last_read = SlackTS(kwargs.get("last_read", SlackTS()))
+        self.channel_buffer = None
+        self.team = kwargs.get('team', None)
+        self.got_history = False
+        self.messages = {}
+        self.hashed_messages = {}
+        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.update_nicklist()
+
+    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)
+
+    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 formatted_name(self, style="default", typing=False, **kwargs):
+        if config.channel_name_typing_indicator:
+            if not typing:
+                prepend = "#"
+            else:
+                prepend = ">"
+        else:
+            prepend = "#"
+        select = {
+            "default": prepend + self.slack_name,
+            "sidebar": 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, topic=None):
+        if self.channel_buffer:
+            if not topic:
+                if self.slack_topic['value'] != "":
+                    topic = self.slack_topic['value']
+                else:
+                    topic = self.slack_purpose['value']
+            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.token, SLACK_API_TRANSLATOR[self.type]["join"], {"name": self.name}, team_hash=self.team.team_hash, channel_identifier=self.identifier)
+                self.eventrouter.receive(s)
+        self.create_buffer()
+        self.active = True
+        self.get_history()
+        if "info" in SLACK_API_TRANSLATOR[self.type]:
+            s = SlackRequest(self.team.token, SLACK_API_TRANSLATOR[self.type]["info"], {"name": self.identifier}, team_hash=self.team.team_hash, channel_identifier=self.identifier)
+            self.eventrouter.receive(s)
+        # self.create_buffer()
+
+    def check_should_open(self, force=False):
+        try:
+            if self.is_archived:
+                return
+        except:
+            pass
+        if force:
+            self.create_buffer()
+        else:
+            for reason in ["is_member", "is_open", "unread_count_display"]:
+                try:
+                    if eval("self." + reason):
+                        self.create_buffer()
+                        if config.background_load_all_history:
+                            self.get_history(slow_queue=True)
+                except:
+                    pass
+
+    def set_related_server(self, team):
+        self.team = team
+
+    def set_highlights(self):
+        # highlight my own name and any set highlights
+        if self.channel_buffer:
+            highlights = self.team.highlight_words.union({'@' + self.team.nick, "!here", "!channel", "!everyone"})
+            h_str = ",".join(highlights)
+            w.buffer_set(self.channel_buffer, "highlight_words", h_str)
+
+    def create_buffer(self):
+        """
+        incomplete (muted doesn't work)
+        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')
-            if self.server.alias:
-                w.buffer_set(self.channel_buffer, "localvar_set_server", self.server.alias)
-            else:
-                w.buffer_set(self.channel_buffer, "localvar_set_server", self.server.team)
-            w.buffer_set(self.channel_buffer, "localvar_set_channel", self.name)
-            w.buffer_set(self.channel_buffer, "short_name", self.name)
-            buffer_list_update_next()
-        if self.unread_count != 0 and not self.muted:
-            w.buffer_set(self.channel_buffer, "hotlist", "1")
-
-    def attach_buffer(self):
-        channel_buffer = w.buffer_search("", "{}.{}".format(self.server.server_buffer_name, self.name))
-        if channel_buffer != main_weechat_buffer:
-            self.channel_buffer = channel_buffer
-            w.buffer_set(self.channel_buffer, "localvar_set_nick", self.server.nick)
-            w.buffer_set(self.channel_buffer, "highlight_words", self.server.nick)
-        else:
-            self.channel_buffer = None
-        channels.update_hashtable()
-        self.server.channels.update_hashtable()
-
-    def detach_buffer(self):
-        if self.channel_buffer is not None:
-            w.buffer_close(self.channel_buffer)
-            self.channel_buffer = None
-        channels.update_hashtable()
-        self.server.channels.update_hashtable()
-
-    def update_nicklist(self, user=None):
-        if not self.channel_buffer:
-            return
-
-        w.buffer_set(self.channel_buffer, "nicklist", "1")
-
-        # create nicklists for the current channel if they don't exist
-        # if they do, use the existing pointer
-        here = w.nicklist_search_group(self.channel_buffer, '', NICK_GROUP_HERE)
-        if not here:
-            here = w.nicklist_add_group(self.channel_buffer, '', NICK_GROUP_HERE, "weechat.color.nicklist_group", 1)
-        afk = w.nicklist_search_group(self.channel_buffer, '', NICK_GROUP_AWAY)
-        if not afk:
-            afk = w.nicklist_add_group(self.channel_buffer, '', NICK_GROUP_AWAY, "weechat.color.nicklist_group", 1)
-
-        if user:
-            user = self.members_table[user]
-            nick = w.nicklist_search_nick(self.channel_buffer, "", user.name)
-            # since this is a change just remove it regardless of where it is
-            w.nicklist_remove_nick(self.channel_buffer, nick)
-            # now add it back in to whichever..
-            w.nicklist_add_nick(self.channel_buffer, here, user.name, user.color_name, "", "", 1)
-
-        # if we didn't get a user, build a complete list. this is expensive.
-        else:
+            w.buffer_set(self.channel_buffer, "localvar_set_channel", self.formatted_name())
+            w.buffer_set(self.channel_buffer, "short_name", self.formatted_name(style="sidebar", enable_color=True))
+            self.render_topic()
+            self.eventrouter.weechat_controller.set_refresh_buffer_list(True)
+            if self.channel_buffer:
+                # if self.team.server_alias:
+                #    w.buffer_set(self.channel_buffer, "localvar_set_server", self.team.server_alias)
+                # else:
+                w.buffer_set(self.channel_buffer, "localvar_set_server", self.team.preferred_name)
+        # else:
+        #    self.eventrouter.weechat_controller.register_buffer(self.channel_buffer, self)
             try:
-                for user in self.members:
-                    user = self.members_table[user]
-                    if user.deleted:
-                        continue
-                    w.nicklist_add_nick(self.channel_buffer, here, user.name, user.color_name, "", "", 1)
-            except Exception as e:
-                dbg("DEBUG: {} {} {}".format(self.identifier, self.name, e))
-
-    def fullname(self):
-        return "{}.{}".format(self.server.server_buffer_name, self.name)
-
-    def has_user(self, name):
-        return name in self.members
-
-    def user_join(self, name):
-        self.members.add(name)
-        self.create_members_table()
+                for c in range(self.unread_count_display):
+                    if self.type == "im":
+                        w.buffer_set(self.channel_buffer, "hotlist", "2")
+                    else:
+                        w.buffer_set(self.channel_buffer, "hotlist", "1")
+                else:
+                    pass
+                    # dbg("no unread in {}".format(self.name))
+            except:
+                pass
+
         self.update_nicklist()
-
-    def user_leave(self, name):
-        if name in self.members:
-            self.members.remove(name)
-        self.create_members_table()
-        self.update_nicklist()
-
-    def set_active(self):
-        self.active = True
-
-    def set_inactive(self):
+        # dbg("exception no unread count")
+        # if self.unread_count != 0 and not self.muted:
+        #    w.buffer_set(self.channel_buffer, "hotlist", "1")
+
+    def destroy_buffer(self, update_remote):
+        if self.channel_buffer is not None:
+            self.channel_buffer = None
+        self.messages = {}
+        self.hashed_messages = {}
+        self.got_history = False
+        # if update_remote and not eventrouter.shutting_down:
         self.active = False
-
-    def set_typing(self, user):
+        if update_remote and not self.eventrouter.shutting_down:
+            s = SlackRequest(self.team.token, SLACK_API_TRANSLATOR[self.type]["leave"], {"channel": self.identifier}, team_hash=self.team.team_hash, channel_identifier=self.identifier)
+            self.eventrouter.receive(s)
+
+    def buffer_prnt(self, nick, text, timestamp=str(time.time()), tagset=None, tag_nick=None, **kwargs):
+        data = "{}\t{}".format(nick, text)
+        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:
-            if w.buffer_get_integer(self.channel_buffer, "hidden") == 0:
-                self.typing[user] = time.time()
-                buffer_list_update_next()
-
-    def unset_typing(self, user):
-        if self.channel_buffer:
-            if w.buffer_get_integer(self.channel_buffer, "hidden") == 0:
-                try:
-                    del self.typing[user]
-                    buffer_list_update_next()
-                except:
-                    pass
-
-    def send_message(self, message):
-        message = self.linkify_text(message)
-        dbg(message)
-        request = {"type": "message", "channel": self.identifier, "text": message, "_server": self.server.domain}
-        self.server.send_to_websocket(request)
-
-    def linkify_text(self, message):
-        message = message.split(' ')
-        for item in enumerate(message):
-            targets = re.match('.*([@#])([\w.]+\w)(\W*)', item[1])
-            if targets and targets.groups()[0] == '@':
-                named = targets.groups()
-                if named[1] in ["group", "channel", "here"]:
-                    message[item[0]] = "<!{}>".format(named[1])
-                if self.server.users.find(named[1]):
-                    message[item[0]] = "<@{}>{}".format(self.server.users.find(named[1]).identifier, named[2])
-            if targets and targets.groups()[0] == '#':
-                named = targets.groups()
-                if self.server.channels.find(named[1]):
-                    message[item[0]] = "<#{}|{}>{}".format(self.server.channels.find(named[1]).identifier, named[1], named[2])
+            # backlog messages - we will update the read marker as we print these
+            backlog = True if ts <= last_read else False
+            if tagset:
+                tags = tag(tagset, user=tag_nick)
+                self.new_messages = True
+
+            # we have to infer the tagset because we weren't told
+            elif ts <= last_read:
+                tags = tag("backlog", user=tag_nick)
+            elif self.type in ["im", "mpdm"]:
+                if nick != self.team.nick:
+                    tags = tag("dm", user=tag_nick)
+                    self.new_messages = True
+                else:
+                    tags = tag("dmfromme")
+            else:
+                tags = tag("default", user=tag_nick)
+                self.new_messages = True
+
+            try:
+                if config.unhide_buffers_with_activity and not self.is_visible() and (self.identifier not in self.team.muted_channels):
+                    w.buffer_set(self.channel_buffer, "hidden", "0")
+
+                w.prnt_date_tags(self.channel_buffer, ts.major, tags, data)
+                modify_print_time(self.channel_buffer, ts.minorstr(), ts.major)
+                if backlog:
+                    self.mark_read(ts, update_remote=False, force=True)
+            except:
+                dbg("Problem processing buffer_prnt")
+
+    def send_message(self, message, request_dict_ext={}):
+        # team = self.eventrouter.teams[self.team]
+        message = linkify_text(message, self.team, self)
         dbg(message)
-        return " ".join(message)
-
-    def set_topic(self, topic):
-        self.topic = topic.encode('utf-8')
-        w.buffer_set(self.channel_buffer, "title", self.topic)
-
-    def open(self, update_remote=True):
-        self.create_buffer()
-        self.active = True
-        self.get_history()
-        if "info" in SLACK_API_TRANSLATOR[self.type]:
-            async_slack_api_request(self.server.domain, self.server.token, SLACK_API_TRANSLATOR[self.type]["info"], {"name": self.name.lstrip("#")})
-        if update_remote:
-            if "join" in SLACK_API_TRANSLATOR[self.type]:
-                async_slack_api_request(self.server.domain, self.server.token, SLACK_API_TRANSLATOR[self.type]["join"], {"name": self.name.lstrip("#")})
-
-    def close(self, update_remote=True):
-        # remove from cache so messages don't reappear when reconnecting
-        if self.active:
-            self.active = False
-            self.current_short_name = ""
-            self.detach_buffer()
-        if update_remote:
-            async_slack_api_request(self.server.domain, self.server.token, SLACK_API_TRANSLATOR[self.type]["leave"], {"channel": self.identifier})
-
-    def closed(self):
-        self.channel_buffer = None
-        self.last_received = None
-        self.close()
-
-    def is_someone_typing(self):
-        for user in self.typing.keys():
-            if self.typing[user] + 4 > time.time():
-                return True
-        if len(self.typing) > 0:
-            self.typing = {}
-            buffer_list_update_next()
-        return False
-
-    def get_typing_list(self):
-        typing = []
-        for user in self.typing.keys():
-            if self.typing[user] + 4 > time.time():
-                typing.append(user)
-        return typing
-
-    def mark_read(self, update_remote=True):
-        if self.channel_buffer:
-            w.buffer_set(self.channel_buffer, "unread", "")
-        if update_remote:
-            self.last_read = time.time()
-            self.update_read_marker(self.last_read)
-
-    def update_read_marker(self, time):
-        async_slack_api_request(self.server.domain, self.server.token, SLACK_API_TRANSLATOR[self.type]["mark"], {"channel": self.identifier, "ts": time})
-
-    def rename(self):
-        if self.is_someone_typing():
-            new_name = ">{}".format(self.name[1:])
-        else:
-            new_name = self.name
-        if self.channel_buffer:
-            if self.current_short_name != new_name:
-                self.current_short_name = new_name
-                w.buffer_set(self.channel_buffer, "short_name", new_name)
-
-    def buffer_prnt(self, user='unknown_user', message='no message', time=0):
-        """
-        writes output (message) to a buffer (channel)
-        """
-        set_read_marker = False
-        time_float = float(time)
-        tags = "nick_" + user
-        user_obj = self.server.users.find(user)
-        # XXX: we should not set log1 for robots.
-        if time_float != 0 and self.last_read >= time_float:
-            tags += ",no_highlight,notify_none,logger_backlog_end"
-            set_read_marker = True
-        elif message.find(self.server.nick.encode('utf-8')) > -1:
-            tags += ",notify_highlight,log1"
-        elif user != self.server.nick and self.name in self.server.users:
-            tags += ",notify_private,notify_message,log1,irc_privmsg"
-        elif self.muted:
-            tags += ",no_highlight,notify_none,logger_backlog_end"
-        elif user in [x.strip() for x in w.prefix("join"), w.prefix("quit")]:
-            tags += ",irc_smart_filter"
+        request = {"type": "message", "channel": self.identifier, "text": message, "_team": self.team.team_hash, "user": self.team.myidentifier}
+        request.update(request_dict_ext)
+        self.team.send_to_websocket(request)
+        self.mark_read(update_remote=False, force=True)
+
+    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
+        if len(self.messages.keys()) > SCROLLBACK_SIZE:
+            mk = self.messages.keys()
+            mk.sort()
+            for k in mk[:SCROLLBACK_SIZE]:
+                msg_to_delete = self.messages[k]
+                if msg_to_delete.hash:
+                    del self.hashed_messages[msg_to_delete.hash]
+                del self.messages[k]
+
+    def change_message(self, ts, text=None, suffix=None):
+        ts = SlackTS(ts)
+        if ts in self.messages:
+            m = self.messages[ts]
+            if text:
+                m.change_text(text)
+            if suffix:
+                m.change_suffix(suffix)
+            text = m.render(force=True)
+        modify_buffer_line(self.channel_buffer, text, ts.major, ts.minor)
+        return True
+
+    def edit_previous_message(self, old, new, flags):
+        message = self.my_last_message()
+        if new == "" and old == "":
+            s = SlackRequest(self.team.token, "chat.delete", {"channel": self.identifier, "ts": message['ts']}, team_hash=self.team.team_hash, channel_identifier=self.identifier)
+            self.eventrouter.receive(s)
         else:
-            tags += ",notify_message,log1,irc_privmsg"
-        # don't write these to local log files
-        # tags += ",no_log"
-        time_int = int(time_float)
-        if self.channel_buffer:
-            prefix_same_nick = w.config_string(w.config_get('weechat.look.prefix_same_nick'))
-            if user == self.last_active_user and prefix_same_nick != "":
-                if config.colorize_nicks and user_obj:
-                    name = user_obj.color + prefix_same_nick
-                else:
-                    name = prefix_same_nick
+            num_replace = 1
+            if 'g' in flags:
+                num_replace = 0
+            new_message = re.sub(old, new, message["text"], num_replace)
+            if new_message != message["text"]:
+                s = SlackRequest(self.team.token, "chat.update", {"channel": self.identifier, "ts": message['ts'], "text": new_message}, team_hash=self.team.team_hash, channel_identifier=self.identifier)
+                self.eventrouter.receive(s)
+
+    def my_last_message(self):
+        for message in reversed(self.sorted_message_keys()):
+            m = self.messages[message]
+            if "user" in m.message_json and "text" in m.message_json and m.message_json["user"] == self.team.myidentifier:
+                return m.message_json
+
+    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:
+                w.buffer_clear(self.channel_buffer)
+            self.buffer_prnt('', 'getting channel history...', tagset='backlog')
+            s = SlackRequest(self.team.token, SLACK_API_TRANSLATOR[self.type]["history"], {"channel": self.identifier, "count": BACKLOG_SIZE}, team_hash=self.team.team_hash, channel_identifier=self.identifier, clear=True)
+            if not slow_queue:
+                self.eventrouter.receive(s)
             else:
-                nick_prefix = w.config_string(w.config_get('weechat.look.nick_prefix'))
-                nick_prefix_color_name = w.config_string(w.config_get('weechat.color.chat_nick_prefix'))
-                nick_prefix_color = w.color(nick_prefix_color_name)
-
-                nick_suffix = w.config_string(w.config_get('weechat.look.nick_suffix'))
-                nick_suffix_color_name = w.config_string(w.config_get('weechat.color.chat_nick_prefix'))
-                nick_suffix_color = w.color(nick_suffix_color_name)
-
-                if user_obj:
-                    name = user_obj.formatted_name()
-                    self.last_active_user = user
-                    # XXX: handle bots properly here.
-                else:
-                    name = user
-                    self.last_active_user = None
-                name = nick_prefix_color + nick_prefix + w.color("reset") + name + nick_suffix_color + nick_suffix + w.color("reset")
-            name = name.decode('utf-8')
-            # colorize nicks in each line
-            chat_color = w.config_string(w.config_get('weechat.color.chat'))
-            if type(message) is not unicode:
-                message = message.decode('UTF-8', 'replace')
-            curr_color = w.color(chat_color)
-            if config.colorize_nicks and config.colorize_messages and user_obj:
-                curr_color = user_obj.color
-            message = curr_color + message
-            for user in self.server.users:
-                if user.name in message:
-                    message = user.name_regex.sub(
-                        r'\1\2{}\3'.format(user.formatted_name() + curr_color),
-                        message)
-
-            message = HTMLParser.HTMLParser().unescape(message)
-            data = u"{}\t{}".format(name, message).encode('utf-8')
-            w.prnt_date_tags(self.channel_buffer, time_int, tags, data)
-
-            if set_read_marker:
-                self.mark_read(False)
-        else:
-            self.open(False)
-        self.last_received = time
-        self.unset_typing(user)
-
-    def buffer_redraw(self):
-        if self.channel_buffer and not self.scrolling:
-            w.buffer_clear(self.channel_buffer)
-            self.messages.sort()
-            for message in self.messages:
-                process_message(message.message_json, False)
-
-    def set_scrolling(self):
-        self.scrolling = True
-
-    def unset_scrolling(self):
-        self.scrolling = False
-
-    def has_message(self, ts):
-        return self.messages.count(ts) > 0
-
-    def change_message(self, ts, text=None, suffix=''):
-        if self.has_message(ts):
-            message_index = self.messages.index(ts)
-
-            if text is not None:
-                self.messages[message_index].change_text(text)
-            text = render_message(self.messages[message_index].message_json, True)
-
-            # if there is only one message with this timestamp, modify it directly.
-            # we do this because time resolution in weechat is less than slack
-            int_time = int(float(ts))
-            if self.messages.count(str(int_time)) == 1:
-                modify_buffer_line(self.channel_buffer, text + suffix, int_time)
-            # otherwise redraw the whole buffer, which is expensive
-            else:
-                self.buffer_redraw()
-            return True
-
-    def add_reaction(self, ts, reaction, user):
-        if self.has_message(ts):
-            message_index = self.messages.index(ts)
-            self.messages[message_index].add_reaction(reaction, user)
-            self.change_message(ts)
-            return True
-
-    def remove_reaction(self, ts, reaction, user):
-        if self.has_message(ts):
-            message_index = self.messages.index(ts)
-            self.messages[message_index].remove_reaction(reaction, user)
-            self.change_message(ts)
-            return True
+                self.eventrouter.receive_slow(s)
+            self.got_history = True
 
     def send_add_reaction(self, msg_number, reaction):
         self.send_change_reaction("reactions.add", msg_number, reaction)
@@ -806,1008 +1370,1245 @@
 
     def send_change_reaction(self, method, msg_number, reaction):
         if 0 < msg_number < len(self.messages):
-            timestamp = self.messages[-msg_number].message_json["ts"]
+            timestamp = self.sorted_message_keys()[-msg_number]
             data = {"channel": self.identifier, "timestamp": timestamp, "name": reaction}
-            async_slack_api_request(self.server.domain, self.server.token, method, data)
-
-    def change_previous_message(self, old, new, flags):
-        message = self.my_last_message()
-        if new == "" and old == "":
-            async_slack_api_request(self.server.domain, self.server.token, 'chat.delete', {"channel": self.identifier, "ts": message['ts']})
-        else:
-            num_replace = 1
-            if 'g' in flags:
-                num_replace = 0
-            new_message = re.sub(old, new, message["text"], num_replace)
-            if new_message != message["text"]:
-                async_slack_api_request(self.server.domain, self.server.token, 'chat.update', {"channel": self.identifier, "ts": message['ts'], "text": new_message.encode("utf-8")})
-
-    def my_last_message(self):
-        for message in reversed(self.messages):
-            if "user" in message.message_json and "text" in message.message_json and message.message_json["user"] == self.server.users.find(self.server.nick).identifier:
-                return message.message_json
-
-    def cache_message(self, message_json, from_me=False):
-        if from_me:
-            message_json["user"] = self.server.users.find(self.server.nick).identifier
-        self.messages.append(Message(message_json))
-        if len(self.messages) > SCROLLBACK_SIZE:
-            self.messages = self.messages[-SCROLLBACK_SIZE:]
-
-    def get_history(self):
-        if self.active:
-            for message in message_cache[self.identifier]:
-                process_message(json.loads(message), True)
-            async_slack_api_request(self.server.domain, self.server.token, SLACK_API_TRANSLATOR[self.type]["history"], {"channel": self.identifier, "count": BACKLOG_SIZE})
-        self.got_history = True
-
-
-class GroupChannel(Channel):
-
-    def __init__(self, server, **kwargs):
-        super(GroupChannel, self).__init__(server, **kwargs)
-        self.type = "group"
-
-
-class MpdmChannel(Channel):
-
-    def __init__(self, server, **kwargs):
-        n = kwargs.get('name')
-        name = "|".join("-".join(n.split("-")[1:-1]).split("--"))
-        kwargs["name"] = name
-        super(MpdmChannel, self).__init__(server, **kwargs)
-        self.type = "group"
-
-
-class DmChannel(Channel):
-
-    def __init__(self, server, **kwargs):
-        super(DmChannel, self).__init__(server, **kwargs)
-        self.type = "im"
-
-    def rename(self):
-        if self.server.users.find(self.name).presence == "active":
-            new_name = self.server.users.find(self.name).formatted_name('+', config.colorize_private_chats)
-        else:
-            new_name = self.server.users.find(self.name).formatted_name(' ', config.colorize_private_chats)
-
-        if self.channel_buffer:
-            if self.current_short_name != new_name:
-                self.current_short_name = new_name
-                w.buffer_set(self.channel_buffer, "short_name", new_name)
+            s = SlackRequest(self.team.token, method, data)
+            self.eventrouter.receive(s)
+
+    def sorted_message_keys(self):
+        keys = []
+        for k in self.messages:
+            if type(self.messages[k]) == SlackMessage:
+                keys.append(k)
+        return sorted(keys)
+
+    # 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, None)
+            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.iteritems():
+            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.iteritems():
+            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 not ts:
+            ts = SlackTS()
+        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 update_remote:
+                s = SlackRequest(self.team.token, SLACK_API_TRANSLATOR[self.type]["mark"], {"channel": self.identifier, "ts": ts}, team_hash=self.team.team_hash, channel_identifier=self.identifier)
+                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):
-        pass
-
-
-class User(object):
-
-    def __init__(self, server, name, identifier, presence="away", deleted=False, is_bot=False):
-        self.server = server
-        self.name = name
-        self.identifier = identifier
-        self.deleted = deleted
-        self.presence = presence
-
-        self.channel_buffer = w.info_get("irc_buffer", "{}.{}".format(domain, self.name))
-        self.update_color()
-        self.name_regex = re.compile(r"([\W]|\A)(@{0,1})" + self.name + "('s|[^'\w]|\Z)")
-        self.is_bot = is_bot
-
-        if deleted:
+        if not self.channel_buffer:
+            return
+        if self.type not in ["channel", "group"]:
             return
-        self.nicklist_pointer = w.nicklist_add_nick(server.buffer, "", self.name, self.color_name, "", "", 1)
-        if self.presence == 'away':
-            w.nicklist_nick_set(self.server.buffer, self.nicklist_pointer, "visible", "0")
+        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
+        # TODO: put this back for mithrandir
+        # here = w.nicklist_search_group(self.channel_buffer, '', NICK_GROUP_HERE)
+        # if not here:
+        #    here = w.nicklist_add_group(self.channel_buffer, '', NICK_GROUP_HERE, "weechat.color.nicklist_group", 1)
+        # afk = w.nicklist_search_group(self.channel_buffer, '', NICK_GROUP_AWAY)
+        # if not afk:
+        #    afk = w.nicklist_add_group(self.channel_buffer, '', NICK_GROUP_AWAY, "weechat.color.nicklist_group", 1)
+
+        if user and len(self.members) < 1000:
+            user = self.team.users[user]
+            nick = w.nicklist_search_nick(self.channel_buffer, "", user.slack_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..
+            if user.identifier in self.members:
+                w.nicklist_add_nick(self.channel_buffer, "", user.name, user.color_name, "", "", 1)
+            # w.nicklist_add_nick(self.channel_buffer, here, user.name, user.color_name, "", "", 1)
+
+        # if we didn't get a user, build a complete list. this is expensive.
         else:
-            w.nicklist_nick_set(self.server.buffer, self.nicklist_pointer, "visible", "1")
-#        w.nicklist_add_nick(server.buffer, "", self.formatted_name(), "", "", "", 1)
-
-    def __str__(self):
-        return self.name
-
-    def __repr__(self):
-        return self.name
-
-    def __eq__(self, compare_str):
-        try:
-            if compare_str == self.name or compare_str == self.identifier:
-                return True
-            elif compare_str[0] == '@' and compare_str[1:] == self.name:
-                return True
+            if len(self.members) < 1000:
+                try:
+                    for user in self.members:
+                        user = self.team.users[user]
+                        if user.deleted:
+                            continue
+                        w.nicklist_add_nick(self.channel_buffer, "", user.name, user.color_name, "", "", 1)
+                        # w.nicklist_add_nick(self.channel_buffer, here, user.name, user.color_name, "", "", 1)
+                except Exception as e:
+                    dbg("DEBUG: {} {} {}".format(self.identifier, self.name, e))
             else:
-                return False
-        except:
-            return False
-
-    def get_aliases(self):
-        return [self.name, "@" + self.name, self.identifier]
-
-    def set_active(self):
-        if not self.deleted:
-            self.presence = "active"
-            dm_channel = self.server.channels.find(self.name)
-            if dm_channel and dm_channel.active:
-                buffer_list_update_next()
-
-        return #temporarily noop this
-        for channel in self.server.channels:
-            if channel.has_user(self.identifier):
-                channel.update_nicklist(self.identifier)
-        w.nicklist_nick_set(self.server.buffer, self.nicklist_pointer, "visible", "1")
-
-    def set_inactive(self):
-        if not self.deleted:
-            self.presence = "away"
-            dm_channel = self.server.channels.find(self.name)
-            if dm_channel and dm_channel.active:
-                buffer_list_update_next()
-
-        return #temporarily noop this
-        if self.deleted:
-            return
-
-        for channel in self.server.channels:
-            if channel.has_user(self.identifier):
-                channel.update_nicklist(self.identifier)
-        w.nicklist_nick_set(self.server.buffer, self.nicklist_pointer, "visible", "0")
+                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 hash_message(self, ts):
+        ts = SlackTS(ts)
+
+        def calc_hash(msg):
+            return sha.sha(str(msg.ts)).hexdigest()
+
+        if ts in self.messages and not self.messages[ts].hash:
+            message = self.messages[ts]
+            tshash = calc_hash(message)
+            hl = 3
+            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_msg = self.hashed_messages.pop(shorthash[:-1])
+                col_new_hash = calc_hash(col_msg)[:hl]
+                col_msg.hash = col_new_hash
+                self.hashed_messages[col_new_hash] = col_msg
+                self.change_message(str(col_msg.ts))
+                if col_msg.thread_channel:
+                    col_msg.thread_channel.rename()
+
+            self.hashed_messages[shorthash] = message
+            message.hash = shorthash
+
+
+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
+        super(SlackDMChannel, self).__init__(eventrouter, **kwargs)
+        self.type = 'im'
+        self.update_color()
+        self.set_name(self.slack_name)
+
+    def set_name(self, slack_name):
+        self.name = slack_name
+
+    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_nicks:
-            if self.name == self.server.nick:
-                self.color_name = w.config_string(w.config_get('weechat.color.chat_nick_self'))
-            else:
-                self.color_name = w.info_get('irc_nick_color_name', self.name)
+        if config.colorize_private_chats:
+            self.color_name = w.info_get('irc_nick_color_name', self.name)
             self.color = w.color(self.color_name)
         else:
             self.color = ""
             self.color_name = ""
 
-    def formatted_name(self, prepend="", enable_color=True):
-        if config.colorize_nicks and enable_color:
+    def formatted_name(self, style="default", typing=False, present=True, enable_color=False, **kwargs):
+        if config.colorize_private_chats and enable_color:
             print_color = self.color
         else:
             print_color = ""
-        return print_color + prepend + self.name
-
-    def create_dm_channel(self):
-        async_slack_api_request(self.server.domain, self.server.token, "im.open", {"user": self.identifier})
-
-
-class Bot(object):
-
-    def __init__(self, server, name, identifier, deleted=False):
-        self.server = server
-        self.name = name
-        self.identifier = identifier
-        self.deleted = deleted
+        if not present:
+            prepend = " "
+        else:
+            prepend = "+"
+        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),
+        }
+        return print_color + select[style]
+
+    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.token, SLACK_API_TRANSLATOR[self.type]["info"], {"name": self.identifier}, team_hash=self.team.team_hash, channel_identifier=self.identifier)
+            self.eventrouter.receive(s)
+        if update_remote:
+            if "join" in SLACK_API_TRANSLATOR[self.type]:
+                s = SlackRequest(self.team.token, SLACK_API_TRANSLATOR[self.type]["join"], {"user": self.user}, team_hash=self.team.team_hash, channel_identifier=self.identifier)
+                self.eventrouter.receive(s)
+        self.create_buffer()
+
+    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.name = "#" + kwargs['name']
+        self.type = "group"
+        self.set_name(self.slack_name)
+
+    def set_name(self, slack_name):
+        self.name = "#" + slack_name
+
+    # def formatted_name(self, prepend="#", enable_color=True, basic=False):
+    #    return prepend + self.slack_name
+
+
+class SlackMPDMChannel(SlackChannel):
+    """
+    An MPDM channel is a special instance of a 'group' channel.
+    We change the name to look less terrible in weechat.
+    """
+
+    def __init__(self, eventrouter, **kwargs):
+        super(SlackMPDMChannel, self).__init__(eventrouter, **kwargs)
+        n = kwargs.get('name')
+        self.set_name(n)
+        self.type = "group"
+
+    def open(self, update_remote=False):
+        self.create_buffer()
+        self.active = True
+        self.get_history()
+        if "info" in SLACK_API_TRANSLATOR[self.type]:
+            s = SlackRequest(self.team.token, SLACK_API_TRANSLATOR[self.type]["info"], {"name": self.identifier}, team_hash=self.team.team_hash, channel_identifier=self.identifier)
+            self.eventrouter.receive(s)
+        # self.create_buffer()
+
+    def set_name(self, n):
+        self.name = "|".join("-".join(n.split("-")[1:-1]).split("--"))
+
+    def formatted_name(self, style="default", typing=False, **kwargs):
+        adjusted_name = "|".join("-".join(self.slack_name.split("-")[1:-1]).split("--"))
+        if config.channel_name_typing_indicator:
+            if not typing:
+                prepend = "#"
+            else:
+                prepend = ">"
+        else:
+            prepend = "#"
+        select = {
+            "default": adjusted_name,
+            "sidebar": prepend + adjusted_name,
+            "base": adjusted_name,
+            "long_default": "{}.{}".format(self.team.preferred_name, adjusted_name),
+            "long_base": "{}.{}".format(self.team.preferred_name, adjusted_name),
+        }
+        return select[style]
+
+    def rename(self):
+        pass
+
+
+class SlackThreadChannel(object):
+    """
+    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.channel_buffer = None
+        # self.identifier = ""
+        # self.name = "#" + kwargs['name']
+        self.type = "thread"
+        self.got_history = False
+        self.label = None
+        # self.set_name(self.slack_name)
+    # def set_name(self, slack_name):
+    #    self.name = "#" + slack_name
+
+    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, **kwargs):
+        data = "{}\t{}".format(nick, text)
+        ts = SlackTS(timestamp)
+        if self.channel_buffer:
+            # backlog messages - we will update the read marker as we print these
+            # backlog = False
+            # if ts <= SlackTS(self.last_read):
+            #    tags = tag("backlog")
+            #    backlog = True
+            # elif self.type in ["im", "mpdm"]:
+            #    tags = tag("dm")
+            #    self.new_messages = True
+            # else:
+            tags = tag("default")
+            # self.new_messages = True
+            w.prnt_date_tags(self.channel_buffer, ts.major, tags, data)
+            modify_print_time(self.channel_buffer, ts.minorstr(), ts.major)
+            # if backlog:
+            #    self.mark_read(ts, update_remote=False, force=True)
+
+    def get_history(self):
+        self.got_history = True
+        for message in self.parent_message.submessages:
+
+            # message = SlackMessage(message_json, team, channel)
+            text = message.render()
+            # print text
+
+            suffix = ''
+            if 'edited' in message.message_json:
+                suffix = ' (edited)'
+            # try:
+            #    channel.unread_count += 1
+            # except:
+            #    channel.unread_count = 1
+            self.buffer_prnt(message.sender, text + suffix, message.ts)
+
+    def send_message(self, message):
+        # team = self.eventrouter.teams[self.team]
+        message = linkify_text(message, self.parent_message.team, self)
+        dbg(message)
+        request = {"type": "message", "channel": self.parent_message.channel.identifier, "text": message, "_team": self.parent_message.team.team_hash, "user": self.parent_message.team.myidentifier, "thread_ts": str(self.parent_message.ts)}
+        self.parent_message.team.send_to_websocket(request)
+        self.mark_read(update_remote=False, force=True)
+
+    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.token, SLACK_API_TRANSLATOR[self.type]["info"], {"name": self.identifier}, team_hash=self.team.team_hash, channel_identifier=self.identifier)
+        #    self.eventrouter.receive(s)
+        # if update_remote:
+        #    if "join" in SLACK_API_TRANSLATOR[self.type]:
+        #        s = SlackRequest(self.team.token, SLACK_API_TRANSLATOR[self.type]["join"], {"name": self.name}, team_hash=self.team.team_hash, channel_identifier=self.identifier)
+        #        self.eventrouter.receive(s)
+        self.create_buffer()
+
+    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 create_buffer(self):
+        """
+        incomplete (muted doesn't work)
+        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_channel", self.formatted_name())
+            w.buffer_set(self.channel_buffer, "short_name", self.formatted_name(style="sidebar", enable_color=True))
+            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.parent_message.render()	)
+            w.buffer_set(self.channel_buffer, "title", topic)
+
+            # self.eventrouter.weechat_controller.set_refresh_buffer_list(True)
+
+        # try:
+        #    if self.unread_count != 0:
+        #        for c in range(1, self.unread_count):
+        #            if self.type == "im":
+        #                w.buffer_set(self.channel_buffer, "hotlist", "2")
+        #            else:
+        #                w.buffer_set(self.channel_buffer, "hotlist", "1")
+        #    else:
+        #        pass
+        #        #dbg("no unread in {}".format(self.name))
+        # except:
+        #    pass
+        #    dbg("exception no unread count")
+        # if self.unread_count != 0 and not self.muted:
+        #    w.buffer_set(self.channel_buffer, "hotlist", "1")
+
+    def destroy_buffer(self, update_remote):
+        if self.channel_buffer is not None:
+            self.channel_buffer = None
+        self.got_history = False
+        # if update_remote and not eventrouter.shutting_down:
+        self.active = False
+
+
+class SlackUser(object):
+    """
+    Represends an individual slack user. Also where you set their name formatting.
+    """
+
+    def __init__(self, **kwargs):
+        # We require these two things for a vaid object,
+        # the rest we can just learn from slack
+        self.identifier = kwargs["id"]
+        self.slack_name = kwargs["name"]
+        self.name = kwargs["name"]
+        for key, value in kwargs.items():
+            setattr(self, key, value)
         self.update_color()
 
-    def __eq__(self, compare_str):
-        if compare_str == self.identifier or compare_str == self.name:
-            return True
-        else:
-            return False
-
-    def __str__(self):
-        return "{}".format(self.identifier)
-
     def __repr__(self):
-        return "{}".format(self.identifier)
+        return "Name:{} Identifier:{}".format(self.name, self.identifier)
+
+    def force_color(self, color_name):
+        self.color_name = color_name
+        self.color = w.color(self.color_name)
 
     def update_color(self):
-        if config.colorize_nicks:
-            self.color_name = w.info_get('irc_nick_color_name', self.name.encode('utf-8'))
-            self.color = w.color(self.color_name)
-        else:
-            self.color_name = ""
-            self.color = ""
+        # This will automatically be none/"" if the user has disabled nick
+        # colourization.
+        self.color_name = w.info_get('nick_color_name', self.name)
+        self.color = w.color(self.color_name)
 
     def formatted_name(self, prepend="", enable_color=True):
-        if config.colorize_nicks and enable_color:
-            print_color = self.color
+        if enable_color:
+            return self.color + prepend + self.name
         else:
-            print_color = ""
-        return print_color + prepend + self.name
-
-
-class Message(object):
-
-    def __init__(self, message_json):
+            return prepend + self.name
+
+
+class SlackBot(SlackUser):
+    """
+    Basically the same as a user, but split out to identify and for future
+    needs
+    """
+    def __init__(self, **kwargs):
+        super(SlackBot, self).__init__(**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.ts = message_json['ts']
-        # split timestamp into time and counter
-        self.ts_time, self.ts_counter = message_json['ts'].split('.')
+        self.submessages = []
+        self.thread_channel = None
+        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.suffix = ''
+        self.ts = SlackTS(message_json['ts'])
+        text = self.message_json.get('text')
+        if text and text.startswith('_') and text.endswith('_') and 'subtype' not in message_json:
+            message_json['text'] = text[1:-1]
+            message_json['subtype'] = 'me_message'
+        if message_json.get('subtype') == 'me_message' and not message_json['text'].startswith(self.sender):
+            message_json['text'] = self.sender + ' ' + self.message_json['text']
+
+    def __hash__(self):
+        return hash(self.ts)
+
+    def render(self, force=False):
+        if len(self.submessages) > 0:
+            return "{} {} {}".format(render(self.message_json, self.team, self.channel, force), self.suffix, "{}[ Thread: {} Replies: {} ]".format(w.color(config.thread_suffix_color), self.hash or self.ts, len(self.submessages)))
+        return "{} {}".format(render(self.message_json, self.team, self.channel, force), self.suffix)
 
     def change_text(self, new_text):
-        if not isinstance(new_text, unicode):
-            new_text = unicode(new_text, 'utf-8')
         self.message_json["text"] = new_text
+        dbg(self.message_json)
+
+    def change_suffix(self, new_suffix):
+        self.suffix = new_suffix
+        dbg(self.message_json)
+
+    def get_sender(self):
+        name = ""
+        name_plain = ""
+        if 'bot_id' in self.message_json and self.message_json['bot_id'] is not None:
+            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))
+        elif 'user' in self.message_json:
+            if self.message_json['user'] == self.team.myidentifier:
+                name = self.team.users[self.team.myidentifier].name
+                name_plain = self.team.users[self.team.myidentifier].name
+            elif self.message_json['user'] in self.team.users:
+                u = self.team.users[self.message_json['user']]
+                if u.is_bot:
+                    name = "{} :]".format(u.formatted_name())
+                else:
+                    name = "{}".format(u.formatted_name())
+                name_plain = "{}".format(u.formatted_name(enable_color=False))
+        elif 'username' in self.message_json:
+            name = "-{}-".format(self.message_json["username"])
+            name_plain = "{}".format(self.message_json["username"])
+        elif 'service_name' in self.message_json:
+            name = "-{}-".format(self.message_json["service_name"])
+            name_plain = "{}".format(self.message_json["service_name"])
+        else:
+            name = ""
+            name_plain = ""
+        return (name, name_plain)
 
     def add_reaction(self, reaction, user):
-        if "reactions" in self.message_json:
+        m = self.message_json.get('reactions', None)
+        if m:
             found = False
-            for r in self.message_json["reactions"]:
+            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({u"name": reaction, u"users": [user]})
+                self.message_json["reactions"].append({"name": reaction, "users": [user]})
         else:
-            self.message_json["reactions"] = [{u"name": reaction, u"users": [user]}]
+            self.message_json["reactions"] = [{"name": reaction, "users": [user]}]
 
     def remove_reaction(self, reaction, user):
-        if "reactions" in self.message_json:
-            for r in self.message_json["reactions"]:
+        m = self.message_json.get('reactions', None)
+        if m:
+            for r in m:
                 if r["name"] == reaction and user in r["users"]:
                     r["users"].remove(user)
         else:
             pass
 
-    def __eq__(self, other):
-        return self.ts_time == other or self.ts == other
+
+class SlackThreadMessage(SlackMessage):
+
+    def __init__(self, parent_id, *args):
+        super(SlackThreadMessage, self).__init__(*args)
+        self.parent_id = parent_id
+
+
+class WeeSlackMetadata(object):
+    """
+    A simple container that we pickle/unpickle to hold data.
+    """
+
+    def __init__(self, meta):
+        self.meta = meta
+
+    def jsonify(self):
+        return self.meta
+
+
+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
+        else:
+            s = self.__str__()
+            if s < other:
+                return -1
+            elif s > other:
+                return 1
+            elif s == other:
+                return 0
+
+    def __hash__(self):
+        return hash("{}.{}".format(self.major, self.minor))
 
     def __repr__(self):
-        return "{} {} {} {}\n".format(self.ts_time, self.ts_counter, self.ts, self.message_json)
-
-    def __lt__(self, other):
-        return self.ts < other.ts
-
-
-def slack_buffer_or_ignore(f):
-    """
-    Only run this function if we're in a slack buffer, else ignore
+        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):
     """
-    @wraps(f)
-    def wrapper(current_buffer, *args, **kwargs):
-        server = servers.find(current_domain_name())
-        if not server:
-            return w.WEECHAT_RC_OK
-        return f(current_buffer, *args, **kwargs)
-    return wrapper
-
-
-def slack_command_cb(data, current_buffer, args):
-    a = args.split(' ', 1)
-    if len(a) > 1:
-        function_name, args = a[0], " ".join(a[1:])
-    else:
-        function_name, args = a[0], None
-
-    try:
-        cmds[function_name](current_buffer, args)
-    except KeyError:
-        w.prnt("", "Command not found: " + function_name)
-    return w.WEECHAT_RC_OK
-
-
-@slack_buffer_or_ignore
-def me_command_cb(data, current_buffer, args):
-    if channels.find(current_buffer):
-        # channel = channels.find(current_buffer)
-        # nick = channel.server.nick
-        message = "_{}_".format(args)
-        buffer_input_cb("", current_buffer, message)
-    return w.WEECHAT_RC_OK
-
-
-@slack_buffer_or_ignore
-def join_command_cb(data, current_buffer, args):
-    args = args.split()
-    if len(args) < 2:
-        w.prnt(current_buffer, "Missing channel argument")
-        return w.WEECHAT_RC_OK_EAT
-    elif command_talk(current_buffer, args[1]):
-        return w.WEECHAT_RC_OK_EAT
-    else:
-        return w.WEECHAT_RC_OK
-
-
-@slack_buffer_or_ignore
-def part_command_cb(data, current_buffer, args):
-    if channels.find(current_buffer) or servers.find(current_buffer):
-        args = args.split()
-        if len(args) > 1:
-            channel = args[1:]
-            servers.find(current_domain_name()).channels.find(channel).close(True)
-        else:
-            channels.find(current_buffer).close(True)
-        return w.WEECHAT_RC_OK_EAT
-    else:
-        return w.WEECHAT_RC_OK
-
-
-# Wrap command_ functions that require they be performed in a slack buffer
-def slack_buffer_required(f):
-    @wraps(f)
-    def wrapper(current_buffer, *args, **kwargs):
-        server = servers.find(current_domain_name())
-        if not server:
-            w.prnt(current_buffer, "This command must be used in a slack buffer")
-            return w.WEECHAT_RC_ERROR
-        return f(current_buffer, *args, **kwargs)
-    return wrapper
-
-
-def command_register(current_buffer, args):
-    CLIENT_ID = "2468770254.51917335286"
-    CLIENT_SECRET = "dcb7fe380a000cba0cca3169a5fe8d70"  # this is not really a secret
-    if not args:
-        message = """
-# ### Retrieving a Slack token via OAUTH ####
-
-1) Paste this into a browser: https://slack.com/oauth/authorize?client_id=2468770254.51917335286&scope=client
-2) Select the team you wish to access from wee-slack in your browser.
-3) Click "Authorize" in the browser **IMPORTANT: the redirect will fail, this is expected**
-4) Copy the "code" portion of the URL to your clipboard
-5) Return to weechat and run `/slack register [code]`
-6) Add the returned token per the normal wee-slack setup instructions
-
-
-"""
-        w.prnt(current_buffer, message)
-    else:
-        aargs = args.split(None, 2)
-        if len(aargs) != 1:
-            w.prnt(current_buffer, "ERROR: invalid args to register")
-        else:
-            # w.prnt(current_buffer, "https://slack.com/api/oauth.access?client_id={}&client_secret={}&code={}".format(CLIENT_ID, CLIENT_SECRET, aargs[0]))
-            ret = urllib.urlopen("https://slack.com/api/oauth.access?client_id={}&client_secret={}&code={}".format(CLIENT_ID, CLIENT_SECRET, aargs[0])).read()
-            d = json.loads(ret)
-            if d["ok"] == True:
-                w.prnt(current_buffer, "Success! Access token is: " + d['access_token'])
-            else:
-                w.prnt(current_buffer, "Failed! Error is: " + d['error'])
-
-
-@slack_buffer_or_ignore
-def msg_command_cb(data, current_buffer, args):
-    dbg("msg_command_cb")
-    aargs = args.split(None, 2)
-    who = aargs[1]
-
-    command_talk(current_buffer, who)
-
-    if len(aargs) > 2:
-        message = aargs[2]
-        server = servers.find(current_domain_name())
-        if server:
-            channel = server.channels.find(who)
-            channel.send_message(message)
-    return w.WEECHAT_RC_OK_EAT
-
-
-@slack_buffer_required
-def command_upload(current_buffer, args):
-    """
-    Uploads a file to the current buffer
-    /slack upload [file_path]
-    """
-    post_data = {}
-    channel = current_buffer_name(short=True)
-    domain = current_domain_name()
-    token = servers.find(domain).token
-
-    if servers.find(domain).channels.find(channel):
-        channel_identifier = servers.find(domain).channels.find(channel).identifier
-
-    if channel_identifier:
-        post_data["token"] = token
-        post_data["channels"] = channel_identifier
-        post_data["file"] = args
-        async_slack_api_upload_request(token, "files.upload", post_data)
-
-
-def command_talk(current_buffer, args):
-    """
-    Open a chat with the specified user
-    /slack talk [user]
-    """
-
-    server = servers.find(current_domain_name())
-    if server:
-        channel = server.channels.find(args)
-        if channel is None:
-            user = server.users.find(args)
-            if user:
-                user.create_dm_channel()
-            else:
-                server.buffer_prnt("User or channel {} not found.".format(args))
-        else:
-            channel.open()
-            if config.switch_buffer_on_join:
-                w.buffer_set(channel.channel_buffer, "display", "1")
-        return True
-    else:
-        return False
-
-
-def command_join(current_buffer, args):
-    """
-    Join the specified channel
-    /slack join [channel]
-    """
-    domain = current_domain_name()
-    if domain == "":
-        if len(servers) == 1:
-            domain = servers[0]
-        else:
-            w.prnt(current_buffer, "You are connected to multiple Slack instances, please execute /join from a server buffer. i.e. (domain).slack.com")
-            return
-    channel = servers.find(domain).channels.find(args)
-    if channel is not None:
-        servers.find(domain).channels.find(args).open()
-    else:
-        w.prnt(current_buffer, "Channel not found.")
-
-
-@slack_buffer_required
-def command_channels(current_buffer, args):
-    """
-    List all the channels for the slack instance (name, id, active)
-    /slack channels
-    """
-    server = servers.find(current_domain_name())
-    for channel in server.channels:
-        line = "{:<25} {} {}".format(channel.name, channel.identifier, channel.active)
-        server.buffer_prnt(line)
-
-
-def command_nodistractions(current_buffer, args):
-    global hide_distractions
-    hide_distractions = not hide_distractions
-    if config.distracting_channels != ['']:
-        for channel in config.distracting_channels:
-            try:
-                channel_buffer = channels.find(channel).channel_buffer
-                if channel_buffer:
-                    w.buffer_set(channels.find(channel).channel_buffer, "hidden", str(int(hide_distractions)))
-            except:
-                dbg("Can't hide channel {} .. removing..".format(channel), main_buffer=True)
-                config.distracting_channels.pop(config.distracting_channels.index(channel))
-                save_distracting_channels()
-
-
-def command_distracting(current_buffer, args):
-    if channels.find(current_buffer) is None:
-        w.prnt(current_buffer, "This command must be used in a channel buffer")
-        return
-    fullname = channels.find(current_buffer).fullname()
-    if config.distracting_channels.count(fullname) == 0:
-        config.distracting_channels.append(fullname)
-    else:
-        config.distracting_channels.pop(config.distracting_channels.index(fullname))
-    save_distracting_channels()
-
-
-def save_distracting_channels():
-    w.config_set_plugin('distracting_channels', ','.join(config.distracting_channels))
-
-
-@slack_buffer_required
-def command_users(current_buffer, args):
-    """
-    List all the users for the slack instance (name, id, away)
-    /slack users
-    """
-    server = servers.find(current_domain_name())
-    for user in server.users:
-        line = "{:<40} {} {}".format(user.formatted_name(), user.identifier, user.presence)
-        server.buffer_prnt(line)
-
-
-def command_setallreadmarkers(current_buffer, args):
-    """
-    Sets the read marker for all channels
-    /slack setallreadmarkers
+    This handles the main entry call to slack, rtm.start
     """
-    for channel in channels:
-        channel.mark_read()
-
-
-def command_changetoken(current_buffer, args):
-    w.config_set_plugin('slack_api_token', args)
-
-
-def command_test(current_buffer, args):
-    w.prnt(current_buffer, "worked!")
-
-
-def away_command_cb(data, current_buffer, args):
-    (all, message) = re.match("^/away(?:\s+(-all))?(?:\s+(.+))?", args).groups()
-    if all is None:
-        server = servers.find(current_domain_name())
-        if not server:
-            return w.WEECHAT_RC_OK
-        if message is None:
-            server.set_active()
-        else:
-            server.set_away(message)
-        return w.WEECHAT_RC_OK_EAT
-    for server in servers:
-        if message is None:
-            server.set_active()
+    if login_data["ok"]:
+
+        metadata = pickle.loads(login_data["wee_slack_request_metadata"])
+
+        # 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(**item)
+                # users.append(SlackUser(**item))
+
+            bots = {}
+            for item in login_data["bots"]:
+                bots[item["id"]] = SlackBot(**item)
+
+            channels = {}
+            for item in login_data["channels"]:
+                channels[item["id"]] = SlackChannel(eventrouter, **item)
+
+            for item in login_data["ims"]:
+                channels[item["id"]] = SlackDMChannel(eventrouter, users, **item)
+
+            for item in login_data["groups"]:
+                if item["name"].startswith('mpdm-'):
+                    channels[item["id"]] = SlackMPDMChannel(eventrouter, **item)
+                else:
+                    channels[item["id"]] = SlackGroupChannel(eventrouter, **item)
+
+            t = SlackTeam(
+                eventrouter,
+                metadata.token,
+                login_data['url'],
+                login_data["team"]["domain"],
+                login_data["self"]["name"],
+                login_data["self"]["id"],
+                users,
+                bots,
+                channels,
+                muted_channels=login_data["self"]["prefs"]["muted_channels"],
+                highlight_words=login_data["self"]["prefs"]["highlight_words"],
+            )
+            eventrouter.register_team(t)
+
         else:
-            server.set_away(message)
-        return w.WEECHAT_RC_OK
-
-
-@slack_buffer_required
-def command_away(current_buffer, args):
-    """
-    Sets your status as 'away'
-    /slack away
-    """
-    server = servers.find(current_domain_name())
-    async_slack_api_request(server.domain, server.token, 'presence.set', {"presence": "away"})
-
-
-@slack_buffer_required
-def command_back(current_buffer, args):
-    """
-    Sets your status as 'back'
-    /slack back
-    """
-    server = servers.find(current_domain_name())
-    async_slack_api_request(server.domain, server.token, 'presence.set', {"presence": "active"})
-
-
-@slack_buffer_required
-def command_markread(current_buffer, args):
-    """
-    Marks current channel as read
-    /slack markread
-    """
-    # refactor this - one liner i think
-    channel = current_buffer_name(short=True)
-    domain = current_domain_name()
-    if servers.find(domain).channels.find(channel):
-        servers.find(domain).channels.find(channel).mark_read()
-
-
-@slack_buffer_required
-def command_slash(current_buffer, args):
-    """
-    Support for custom slack commands
-    /slack slash /customcommand arg1 arg2 arg3
-    """
-
-    server = servers.find(current_domain_name())
-    channel = current_buffer_name(short=True)
-    domain = current_domain_name()
-
-    if args is None:
-        server.buffer_prnt("Usage: /slack slash /someslashcommand [arguments...].")
-        return
-
-    split_args = args.split(None, 1)
-
-    command = split_args[0]
-    text = split_args[1] if len(split_args) > 1 else ""
-
-    if servers.find(domain).channels.find(channel):
-        channel_identifier = servers.find(domain).channels.find(channel).identifier
-
-    if channel_identifier:
-        async_slack_api_request(server.domain, server.token, 'chat.command', {'command': command, 'text': text, 'channel': channel_identifier})
-    else:
-        server.buffer_prnt("User or channel not found.")
-
-
-def command_flushcache(current_buffer, args):
-    global message_cache
-    message_cache = collections.defaultdict(list)
-    cache_write_cb("", "")
-
-
-def command_cachenow(current_buffer, args):
-    cache_write_cb("", "")
-
-
-def command_neveraway(current_buffer, args):
-    global never_away
-    if never_away:
-        never_away = False
-        dbg("unset never_away", main_buffer=True)
-    else:
-        never_away = True
-        dbg("set never_away", main_buffer=True)
-
-
-def command_printvar(current_buffer, args):
-    w.prnt("", "{}".format(eval(args)))
-
-
-def command_p(current_buffer, args):
-    w.prnt("", "{}".format(eval(args)))
-
-
-def command_debug(current_buffer, args):
-    create_slack_debug_buffer()
-
-
-def command_debugstring(current_buffer, args):
-    global debug_string
-    if args == '':
-        debug_string = None
-    else:
-        debug_string = args
-
-
-def command_search(current_buffer, args):
-    pass
-#    if not slack_buffer:
-#        create_slack_buffer()
-#    w.buffer_set(slack_buffer, "display", "1")
-#    query = args
-#    w.prnt(slack_buffer,"\nSearched for: %s\n\n" % (query))
-#    reply = slack_api_request('search.messages', {"query":query}).read()
-#    data = json.loads(reply)
-#    for message in data['messages']['matches']:
-#        message["text"] = message["text"].encode('ascii', 'ignore')
-#        formatted_message = "%s / %s:\t%s" % (message["channel"]["name"], message['username'], message['text'])
-#        w.prnt(slack_buffer,str(formatted_message))
-
-
-def command_nick(current_buffer, args):
-    pass
-#    urllib.urlopen("https://%s/account/settings" % (domain))
-#    browser.select_form(nr=0)
-#    browser.form['username'] = args
-#    reply = browser.submit()
-
-
-def command_help(current_buffer, args):
-    help_cmds = {k[8:]: v.__doc__ for k, v in globals().items() if k.startswith("command_")}
-
-    if args:
-        try:
-            help_cmds = {args: help_cmds[args]}
-        except KeyError:
-            w.prnt("", "Command not found: " + args)
-            return
-
-    for cmd, helptext in help_cmds.items():
-        w.prnt('', w.color("bold") + cmd)
-        w.prnt('', (helptext or 'No help text').strip())
-        w.prnt('', '')
-
-# Websocket handling methods
-
-
-def command_openweb(current_buffer, args):
-    trigger = config.trigger_value
-    if trigger != "0":
-        if args is None:
-            channel = channels.find(current_buffer)
-            url = "{}/messages/{}".format(channel.server.server_buffer_name, channel.name)
-            topic = w.buffer_get_string(channel.channel_buffer, "title")
-            w.buffer_set(channel.channel_buffer, "title", "{}:{}".format(trigger, url))
-            w.hook_timer(1000, 0, 1, "command_openweb", json.dumps({"topic": topic, "buffer": current_buffer}))
-        else:
-            # TODO: fix this dirty hack because i don't know the right way to send multiple args.
-            args = current_buffer
-            data = json.loads(args)
-            channel_buffer = channels.find(data["buffer"]).channel_buffer
-            w.buffer_set(channel_buffer, "title", data["topic"])
-    return w.WEECHAT_RC_OK
-
-
-@slack_buffer_or_ignore
-def topic_command_cb(data, current_buffer, args):
-    n = len(args.split())
-    if n < 2:
-        channel = channels.find(current_buffer)
-        if channel:
-            w.prnt(current_buffer, 'Topic for {} is "{}"'.format(channel.name, channel.topic))
-        return w.WEECHAT_RC_OK_EAT
-    elif command_topic(current_buffer, args.split(None, 1)[1]):
-        return w.WEECHAT_RC_OK_EAT
-    else:
-        return w.WEECHAT_RC_ERROR
-
-
-def command_topic(current_buffer, args):
-    """
-    Change the topic of a channel
-    /slack topic [<channel>] [<topic>|-delete]
-    """
-    server = servers.find(current_domain_name())
-    if server:
-        arrrrgs = args.split(None, 1)
-        if arrrrgs[0].startswith('#'):
-            channel = server.channels.find(arrrrgs[0])
-            topic = arrrrgs[1]
-        else:
-            channel = server.channels.find(current_buffer)
-            topic = args
-
-        if channel:
-            if topic == "-delete":
-                async_slack_api_request(server.domain, server.token, 'channels.setTopic', {"channel": channel.identifier, "topic": ""})
-            else:
-                async_slack_api_request(server.domain, server.token, 'channels.setTopic', {"channel": channel.identifier, "topic": topic})
-            return True
-        else:
-            return False
-    else:
-        return False
-
-
-def slack_websocket_cb(server, fd):
+            t = eventrouter.teams.get(th)
+            t.set_reconnect_url(login_data['url'])
+            t.connect()
+
+        # web_socket_url = login_data['url']
+        # try:
+        #    ws = create_connection(web_socket_url, sslopt=sslopt_ca_certs)
+        #    w.hook_fd(ws.sock._sock.fileno(), 1, 0, 0, "receive_ws_callback", t.get_team_hash())
+        #    #ws_hook = w.hook_fd(ws.sock._sock.fileno(), 1, 0, 0, "receive_ws_callback", pickle.dumps(t))
+        #    ws.sock.setblocking(0)
+        #    t.attach_websocket(ws)
+        #    t.set_connected()
+        # except Exception as e:
+        #    dbg("websocket connection error: {}".format(e))
+        #    return False
+
+        t.buffer_prnt('Connected to Slack')
+        t.buffer_prnt('{:<20} {}'.format("Websocket URL", login_data["url"]))
+        t.buffer_prnt('{:<20} {}'.format("User name", login_data["self"]["name"]))
+        t.buffer_prnt('{:<20} {}'.format("User ID", login_data["self"]["id"]))
+        t.buffer_prnt('{:<20} {}'.format("Team name", login_data["team"]["name"]))
+        t.buffer_prnt('{:<20} {}'.format("Team domain", login_data["team"]["domain"]))
+        t.buffer_prnt('{:<20} {}'.format("Team id", login_data["team"]["id"]))
+
+        dbg("connected to {}".format(t.domain))
+
+    # self.identifier = self.domain
+
+
+def handle_groupshistory(message_json, eventrouter, **kwargs):
+    handle_history(message_json, eventrouter, **kwargs)
+
+
+def handle_channelshistory(message_json, eventrouter, **kwargs):
+    handle_history(message_json, eventrouter, **kwargs)
+
+
+def handle_imhistory(message_json, eventrouter, **kwargs):
+    handle_history(message_json, eventrouter, **kwargs)
+
+
+def handle_history(message_json, eventrouter, **kwargs):
+    request_metadata = pickle.loads(message_json["wee_slack_request_metadata"])
+    kwargs['team'] = eventrouter.teams[request_metadata.team_hash]
+    kwargs['channel'] = kwargs['team'].channels[request_metadata.channel_identifier]
     try:
-        data = servers.find(server).ws.recv()
-        message_json = json.loads(data)
-        # this magic attaches json that helps find the right dest
-        message_json['_server'] = server
-    except WebSocketConnectionClosedException:
-        servers.find(server).ws.close()
-        return w.WEECHAT_RC_OK
-    except Exception:
-        dbg("socket issue: {}\n".format(traceback.format_exc()))
-        return w.WEECHAT_RC_OK
-    # dispatch here
-    if "reply_to" in message_json:
-        function_name = "reply"
-    elif "type" in message_json:
-        function_name = message_json["type"]
-    else:
-        function_name = "unknown"
-    try:
-        proc[function_name](message_json)
-    except KeyError:
-        if function_name:
-            dbg("Function not implemented: {}\n{}".format(function_name, message_json))
-        else:
-            dbg("Function not implemented\n{}".format(message_json))
-    w.bar_item_update("slack_typing_notice")
-    return w.WEECHAT_RC_OK
-
-
-def process_reply(message_json):
-    server = servers.find(message_json["_server"])
-    identifier = message_json["reply_to"]
-    item = server.message_buffer.pop(identifier)
-    if 'text' in item and type(item['text']) is not unicode:
-        item['text'] = item['text'].decode('UTF-8', 'replace')
-    if "type" in item:
-        if item["type"] == "message" and "channel" in item.keys():
-            item["ts"] = message_json["ts"]
-            channels.find(item["channel"]).cache_message(item, from_me=True)
-            text = unfurl_refs(item["text"], ignore_alt_text=config.unfurl_ignore_alt_text)
-
-            channels.find(item["channel"]).buffer_prnt(item["user"], text, item["ts"])
-    dbg("REPLY {}".format(item))
-
-
-def process_pong(message_json):
-    pass
-
-
-def process_pref_change(message_json):
-    server = servers.find(message_json["_server"])
-    if message_json['name'] == u'muted_channels':
-        muted = message_json['value'].split(',')
-        for c in server.channels:
-            if c.identifier in muted:
-                c.muted = True
-            else:
-                c.muted = False
+        clear = request_metadata.clear
+    except:
+        clear = False
+    dbg(clear)
+    kwargs['output_type'] = "backlog"
+    if clear:
+        w.buffer_clear(kwargs['channel'].channel_buffer)
+    for message in reversed(message_json["messages"]):
+        process_message(message, eventrouter, **kwargs)
+
+###### New/converted process_ and subprocess_ methods
+
+
+def process_reconnect_url(message_json, eventrouter, **kwargs):
+    kwargs['team'].set_reconnect_url(message_json['url'])
+
+
+def process_manual_presence_change(message_json, eventrouter, **kwargs):
+    process_presence_change(message_json, eventrouter, **kwargs)
+
+
+def process_presence_change(message_json, eventrouter, **kwargs):
+    kwargs["user"].presence = message_json["presence"]
+
+
+def process_pref_change(message_json, eventrouter, **kwargs):
+    team = kwargs["team"]
+    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_team_join(message_json):
-    server = servers.find(message_json["_server"])
-    item = message_json["user"]
-    server.add_user(User(server, item["name"], item["id"], item["presence"]))
-    server.buffer_prnt("New user joined: {}".format(item["name"]))
-
-
-def process_manual_presence_change(message_json):
-    process_presence_change(message_json)
-
-
-def process_presence_change(message_json):
-    server = servers.find(message_json["_server"])
-    identifier = message_json.get("user", server.nick)
-    if message_json["presence"] == 'active':
-        server.users.find(identifier).set_active()
-    else:
-        server.users.find(identifier).set_inactive()
-
-
-def process_channel_marked(message_json):
-    channel = channels.find(message_json["channel"])
-    channel.mark_read(False)
-    w.buffer_set(channel.channel_buffer, "hotlist", "-1")
-
-
-def process_group_marked(message_json):
-    channel = channels.find(message_json["channel"])
-    channel.mark_read(False)
-    w.buffer_set(channel.channel_buffer, "hotlist", "-1")
-
-
-def process_channel_created(message_json):
-    server = servers.find(message_json["_server"])
-    item = message_json["channel"]
-    if server.channels.find(message_json["channel"]["name"]):
-        server.channels.find(message_json["channel"]["name"]).open(False)
+def process_user_typing(message_json, eventrouter, **kwargs):
+    channel = kwargs["channel"]
+    team = kwargs["team"]
+    if channel:
+        channel.set_typing(team.users.get(message_json["user"]).name)
+        w.bar_item_update("slack_typing_notice")
+
+
+def process_team_join(message_json, eventrouter, **kwargs):
+    user = message_json['user']
+    team = kwargs["team"]
+    team.users[user["id"]] = SlackUser(**user)
+
+
+def process_pong(message_json, eventrouter, **kwargs):
+    pass
+
+
+def process_message(message_json, eventrouter, store=True, **kwargs):
+    channel = kwargs["channel"]
+    team = kwargs["team"]
+    # try:
+    #  send these subtype messages elsewhere
+    known_subtypes = [
+        'thread_message',
+        'message_replied',
+        'message_changed',
+        'message_deleted',
+        'channel_join',
+        'channel_leave',
+        'channel_topic',
+        # 'group_join',
+        # 'group_leave',
+    ]
+    if "thread_ts" in message_json and "reply_count" not in message_json:
+        message_json["subtype"] = "thread_message"
+
+    subtype = message_json.get("subtype", None)
+    if subtype and subtype in known_subtypes:
+        f = eval('subprocess_' + subtype)
+        f(message_json, eventrouter, channel, team)
+
     else:
-        item = message_json["channel"]
-        item["prepend_name"] = "#"
-        server.add_channel(Channel(server, **item))
-    server.buffer_prnt("New channel created: {}".format(item["name"]))
-
-
-def process_channel_left(message_json):
-    server = servers.find(message_json["_server"])
-    server.channels.find(message_json["channel"]).close(False)
-
-
-def process_channel_join(message_json):
-    server = servers.find(message_json["_server"])
-    channel = server.channels.find(message_json["channel"])
-    text = unfurl_refs(message_json["text"], ignore_alt_text=False)
-    channel.buffer_prnt(w.prefix("join").rstrip(), text, message_json["ts"])
-    channel.user_join(message_json["user"])
-
-
-def process_channel_topic(message_json):
-    server = servers.find(message_json["_server"])
-    channel = server.channels.find(message_json["channel"])
-    text = unfurl_refs(message_json["text"], ignore_alt_text=False)
-    channel.buffer_prnt(w.prefix("network").rstrip(), text, message_json["ts"])
-    channel.set_topic(message_json["topic"])
-
-
-def process_channel_joined(message_json):
-    server = servers.find(message_json["_server"])
-    if server.channels.find(message_json["channel"]["name"]):
-        server.channels.find(message_json["channel"]["name"]).open(False)
+        message = SlackMessage(message_json, team, channel)
+        text = message.render()
+        dbg("Rendered message: %s" % text)
+        dbg("Sender: %s (%s)" % (message.sender, message.sender_plain))
+
+        # Handle actions (/me).
+        # We don't use `subtype` here because creating the SlackMessage may
+        # have changed the subtype based on the detected message contents.
+        if message.message_json.get('subtype') == 'me_message':
+            try:
+                channel.unread_count_display += 1
+            except:
+                channel.unread_count_display = 1
+            channel.buffer_prnt(w.prefix("action").rstrip(), text, message.ts, tag_nick=message.sender_plain, **kwargs)
+
+        else:
+            suffix = ''
+            if 'edited' in message_json:
+                suffix = ' (edited)'
+            try:
+                channel.unread_count_display += 1
+            except:
+                channel.unread_count_display = 1
+            channel.buffer_prnt(message.sender, text + suffix, message.ts, tag_nick=message.sender_plain, **kwargs)
+
+        if store:
+            channel.store_message(message, team)
+        dbg("NORMAL REPLY {}".format(message_json))
+    # except:
+    #    channel.buffer_prnt("WEE-SLACK-ERROR", json.dumps(message_json), message_json["ts"], **kwargs)
+    #    traceback.print_exc()
+
+
+def subprocess_thread_message(message_json, eventrouter, channel, team):
+    # print ("THREADED: " + str(message_json))
+    parent_ts = message_json.get('thread_ts', None)
+    if parent_ts:
+        parent_message = channel.messages.get(SlackTS(parent_ts), None)
+        if parent_message:
+            message = SlackThreadMessage(parent_ts, message_json, team, channel)
+            parent_message.submessages.append(message)
+            channel.hash_message(parent_ts)
+            channel.store_message(message, team)
+            channel.change_message(parent_ts)
+
+            text = message.render()
+            # channel.buffer_prnt(message.sender, text, message.ts, **kwargs)
+            if parent_message.thread_channel:
+                parent_message.thread_channel.buffer_prnt(message.sender, text, message.ts)
+
+#    channel = channels.find(message_json["channel"])
+#    server = channel.server
+#    #threadinfo = channel.get_message(message_json["thread_ts"])
+#    message = Message(message_json, server=server, channel=channel)
+#    dbg(message, main_buffer=True)
+#
+#    orig = channel.get_message(message_json['thread_ts'])
+#    if orig[0]:
+#        channel.get_message(message_json['thread_ts'])[2].add_thread_message(message)
+#    else:
+#        dbg("COULDN'T find orig message {}".format(message_json['thread_ts']), main_buffer=True)
+
+    # if threadinfo[0]:
+    #    channel.messages[threadinfo[1]].become_thread()
+    #    message_json["item"]["ts"], message_json)
+    # channel.change_message(message_json["thread_ts"], None, message_json["text"])
+    # channel.become_thread(message_json["item"]["ts"], message_json)
+
+
+def subprocess_channel_join(message_json, eventrouter, channel, team):
+    joinprefix = w.prefix("join")
+    message = SlackMessage(message_json, team, channel, override_sender=joinprefix)
+    channel.buffer_prnt(joinprefix, message.render(), message_json["ts"], tagset='joinleave')
+    channel.user_joined(message_json['user'])
+
+
+def subprocess_channel_leave(message_json, eventrouter, channel, team):
+    leaveprefix = w.prefix("quit")
+    message = SlackMessage(message_json, team, channel, override_sender=leaveprefix)
+    channel.buffer_prnt(leaveprefix, message.render(), message_json["ts"], tagset='joinleave')
+    channel.user_left(message_json['user'])
+    # channel.update_nicklist(message_json['user'])
+    # channel.update_nicklist()
+
+
+def subprocess_message_replied(message_json, eventrouter, channel, team):
+    pass
+
+
+def subprocess_message_changed(message_json, eventrouter, channel, team):
+    m = message_json.get("message", None)
+    if m:
+        new_message = m
+        # message = SlackMessage(new_message, team, channel)
+        if "attachments" in m:
+            message_json["attachments"] = m["attachments"]
+        if "text" in m:
+            if "text" in message_json:
+                message_json["text"] += m["text"]
+                dbg("added text!")
+            else:
+                message_json["text"] = m["text"]
+        if "fallback" in m:
+            if "fallback" in message_json:
+                message_json["fallback"] += m["fallback"]
+            else:
+                message_json["fallback"] = m["fallback"]
+
+    text_before = (len(new_message['text']) > 0)
+    new_message["text"] += unwrap_attachments(message_json, text_before)
+    if "edited" in new_message:
+        channel.change_message(new_message["ts"], new_message["text"], ' (edited)')
     else:
-        item = message_json["channel"]
-        item["prepend_name"] = "#"
-        server.add_channel(Channel(server, **item))
-
-
-def process_channel_leave(message_json):
-    server = servers.find(message_json["_server"])
-    channel = server.channels.find(message_json["channel"])
+        channel.change_message(new_message["ts"], new_message["text"])
+
+
+def subprocess_message_deleted(message_json, eventrouter, channel, team):
+    channel.change_message(message_json["deleted_ts"], "(deleted)", '')
+
+
+def subprocess_channel_topic(message_json, eventrouter, channel, team):
     text = unfurl_refs(message_json["text"], ignore_alt_text=False)
-    channel.buffer_prnt(w.prefix("quit").rstrip(), text, message_json["ts"])
-    channel.user_leave(message_json["user"])
-
-
-def process_channel_archive(message_json):
-    server = servers.find(message_json["_server"])
-    channel = server.channels.find(message_json["channel"])
-    channel.detach_buffer()
-
-
-def process_group_join(message_json):
-    process_channel_join(message_json)
-
-
-def process_group_leave(message_json):
-    process_channel_leave(message_json)
-
-
-def process_group_topic(message_json):
-    process_channel_topic(message_json)
-
-
-def process_group_left(message_json):
-    server = servers.find(message_json["_server"])
-    server.channels.find(message_json["channel"]).close(False)
-
-
-def process_group_joined(message_json):
-    server = servers.find(message_json["_server"])
-    if server.channels.find(message_json["channel"]["name"]):
-        server.channels.find(message_json["channel"]["name"]).open(False)
-    else:
-        item = message_json["channel"]
-        item["prepend_name"] = "#"
-        if item["name"].startswith("mpdm-"):
-            server.add_channel(MpdmChannel(server, **item))
+    channel.buffer_prnt(w.prefix("network").rstrip(), text, message_json["ts"], tagset="muted")
+    channel.render_topic(message_json["topic"])
+
+
+def process_reply(message_json, eventrouter, **kwargs):
+    dbg('processing reply')
+    team = kwargs["team"]
+    identifier = message_json["reply_to"]
+    try:
+        original_message_json = team.ws_replies[identifier]
+        del team.ws_replies[identifier]
+        if "ts" in message_json:
+            original_message_json["ts"] = message_json["ts"]
         else:
-            server.add_channel(GroupChannel(server, **item))
-
-def process_group_archive(message_json):
-    channel = server.channels.find(message_json["channel"])
-    channel.detach_buffer()
-
-
-def process_mpim_close(message_json):
-    server = servers.find(message_json["_server"])
-    server.channels.find(message_json["channel"]).close(False)
-
-
-def process_mpim_open(message_json):
-    server = servers.find(message_json["_server"])
-    server.channels.find(message_json["channel"]).open(False)
-
-
-def process_im_close(message_json):
-    server = servers.find(message_json["_server"])
-    server.channels.find(message_json["channel"]).close(False)
-
-
-def process_im_open(message_json):
-    server = servers.find(message_json["_server"])
-    server.channels.find(message_json["channel"]).open()
-
-
-def process_im_marked(message_json):
-    channel = channels.find(message_json["channel"])
-    channel.mark_read(False)
-    if channel.channel_buffer is not None:
-        w.buffer_set(channel.channel_buffer, "hotlist", "-1")
-
-
-def process_im_created(message_json):
-    server = servers.find(message_json["_server"])
-    item = message_json["channel"]
-    channel_name = server.users.find(item["user"]).name
-    if server.channels.find(channel_name):
-        server.channels.find(channel_name).open(False)
+            dbg("no reply ts {}".format(message_json))
+
+        c = original_message_json.get('channel', None)
+        channel = team.channels[c]
+        m = SlackMessage(original_message_json, team, channel)
+
+        # if "type" in message_json:
+        #    if message_json["type"] == "message" and "channel" in message_json.keys():
+        #        message_json["ts"] = message_json["ts"]
+        #        channels.find(message_json["channel"]).store_message(m, from_me=True)
+
+        #        channels.find(message_json["channel"]).buffer_prnt(server.nick, m.render(), m.ts)
+
+        process_message(m.message_json, eventrouter, channel=channel, team=team)
+        channel.mark_read(update_remote=True, force=True)
+        dbg("REPLY {}".format(message_json))
+    except KeyError:
+        dbg("Unexpected reply {}".format(message_json))
+
+
+def process_channel_marked(message_json, eventrouter, **kwargs):
+    """
+    complete
+    """
+    channel = kwargs["channel"]
+    ts = message_json.get("ts", None)
+    if ts:
+        channel.mark_read(ts=ts, force=True, update_remote=False)
     else:
-        item = message_json["channel"]
-        item['name'] = server.users.find(item["user"]).name
-        server.add_channel(DmChannel(server, **item))
-    server.buffer_prnt("New direct message channel created: {}".format(item["name"]))
-
-
-def process_user_typing(message_json):
-    server = servers.find(message_json["_server"])
-    channel = server.channels.find(message_json["channel"])
-    if channel:
-        channel.set_typing(server.users.find(message_json["user"]).name)
-
-
-def process_bot_enable(message_json):
-    process_bot_integration(message_json)
-
-
-def process_bot_disable(message_json):
-    process_bot_integration(message_json)
-
-
-def process_bot_integration(message_json):
-    server = servers.find(message_json["_server"])
-    channel = server.channels.find(message_json["channel"])
-
-    time = message_json['ts']
-    text = "{} {}".format(server.users.find(message_json['user']).formatted_name(),
-                          render_message(message_json))
-    bot_name = get_user(message_json, server)
-    bot_name = bot_name.encode('utf-8')
-    channel.buffer_prnt(bot_name, text, time)
-
-# todo: does this work?
-
-
-def process_error(message_json):
-    pass
-
-
-def process_reaction_added(message_json):
+        dbg("tried to mark something weird {}".format(message_json))
+
+
+def process_group_marked(message_json, eventrouter, **kwargs):
+    process_channel_marked(message_json, eventrouter, **kwargs)
+
+
+def process_im_marked(message_json, eventrouter, **kwargs):
+    process_channel_marked(message_json, eventrouter, **kwargs)
+
+
+def process_mpim_marked(message_json, eventrouter, **kwargs):
+    process_channel_marked(message_json, eventrouter, **kwargs)
+
+
+def process_channel_joined(message_json, eventrouter, **kwargs):
+    item = message_json["channel"]
+    kwargs['team'].channels[item["id"]].update_from_message_json(item)
+    kwargs['team'].channels[item["id"]].open()
+
+
+def process_channel_created(message_json, eventrouter, **kwargs):
+    item = message_json["channel"]
+    c = SlackChannel(eventrouter, team=kwargs["team"], **item)
+    kwargs['team'].channels[item["id"]] = c
+    kwargs['team'].buffer_prnt('Channel created: {}'.format(c.slack_name))
+
+
+def process_channel_rename(message_json, eventrouter, **kwargs):
+    item = message_json["channel"]
+    channel = kwargs['team'].channels[item["id"]]
+    channel.slack_name = message_json['channel']['name']
+
+
+def process_im_created(message_json, eventrouter, **kwargs):
+    team = kwargs['team']
+    item = message_json["channel"]
+    c = SlackDMChannel(eventrouter, team=team, users=team.users, **item)
+    team.channels[item["id"]] = c
+    kwargs['team'].buffer_prnt('IM channel created: {}'.format(c.name))
+
+
+def process_im_open(message_json, eventrouter, **kwargs):
+    channel = kwargs['channel']
+    item = message_json
+    kwargs['team'].channels[item["channel"]].check_should_open(True)
+    w.buffer_set(channel.channel_buffer, "hotlist", "2")
+
+
+def process_im_close(message_json, eventrouter, **kwargs):
+    item = message_json
+    cbuf = kwargs['team'].channels[item["channel"]].channel_buffer
+    eventrouter.weechat_controller.unregister_buffer(cbuf, False, True)
+
+
+def process_group_joined(message_json, eventrouter, **kwargs):
+    item = message_json["channel"]
+    if item["name"].startswith("mpdm-"):
+        c = SlackMPDMChannel(eventrouter, team=kwargs["team"], **item)
+    else:
+        c = SlackGroupChannel(eventrouter, team=kwargs["team"], **item)
+    kwargs['team'].channels[item["id"]] = c
+    kwargs['team'].channels[item["id"]].open()
+
+
+def process_reaction_added(message_json, eventrouter, **kwargs):
+    channel = kwargs['team'].channels[message_json["item"]["channel"]]
     if message_json["item"].get("type") == "message":
-        channel = channels.find(message_json["item"]["channel"])
-        channel.add_reaction(message_json["item"]["ts"], message_json["reaction"], message_json["user"])
+        ts = SlackTS(message_json['item']["ts"])
+
+        message = channel.messages.get(ts, None)
+        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, **kwargs):
+    channel = kwargs['team'].channels[message_json["item"]["channel"]]
+    if message_json["item"].get("type") == "message":
+        ts = SlackTS(message_json['item']["ts"])
+
+        message = channel.messages.get(ts, None)
+        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_reaction_removed(message_json):
-    if message_json["item"].get("type") == "message":
-        channel = channels.find(message_json["item"]["channel"])
-        channel.remove_reaction(message_json["item"]["ts"], message_json["reaction"], message_json["user"])
+###### New module/global methods
+
+
+def render(message_json, team, channel, force=False):
+    # If we already have a rendered version in the object, just return that.
+    if not force and message_json.get("_rendered_text", ""):
+        return message_json["_rendered_text"]
     else:
-        dbg("Reaction to item type not supported: " + str(message_json))
+        # server = servers.find(message_json["_server"])
+
+        if "fallback" in message_json:
+            text = message_json["fallback"]
+        elif "text" in message_json:
+            if message_json['text'] is not None:
+                text = message_json["text"]
+            else:
+                text = ""
+        else:
+            text = ""
+
+        text = unfurl_refs(text, ignore_alt_text=config.unfurl_ignore_alt_text)
+
+        text_before = (len(text) > 0)
+        text += unfurl_refs(unwrap_attachments(message_json, text_before), ignore_alt_text=config.unfurl_ignore_alt_text)
+
+        text = text.lstrip()
+        text = text.replace("\t", "    ")
+        text = text.replace("&lt;", "<")
+        text = text.replace("&gt;", ">")
+        text = text.replace("&amp;", "&")
+        text = re.sub(r'(^| )\*([^*]+)\*([^a-zA-Z0-9_]|$)',
+                      r'\1{}\2{}\3'.format(w.color('bold'), w.color('-bold')), text)
+        text = re.sub(r'(^| )_([^_]+)_([^a-zA-Z0-9_]|$)',
+                      r'\1{}\2{}\3'.format(w.color('underline'), w.color('-underline')), text)
+
+#        if self.threads:
+#            text += " [Replies: {} Thread ID: {} ] ".format(len(self.threads), self.thread_id)
+#            #for thread in self.threads:
+
+        text += create_reaction_string(message_json.get("reactions", ""))
+        message_json["_rendered_text"] = text
+        return text
+
+
+def linkify_text(message, team, channel):
+    # 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()
+    message = message.replace('\x02', '*').replace('\x1F', '_').split(' ')
+    for item in enumerate(message):
+        targets = re.match('^\s*([@#])([\w.-]+[\w. -])(\W*)', item[1])
+        if targets and targets.groups()[0] == '@':
+            named = targets.groups()
+            if named[1] in ["group", "channel", "here"]:
+                message[item[0]] = "<!{}>".format(named[1])
+            else:
+                try:
+                    if usernames[named[1]]:
+                        message[item[0]] = "<@{}>{}".format(usernames[named[1]], named[2])
+                except:
+                    message[item[0]] = "@{}{}".format(named[1], named[2])
+        if targets and targets.groups()[0] == '#':
+            named = targets.groups()
+            try:
+                if channels[named[1]]:
+                    message[item[0]] = "<#{}|{}>{}".format(channels[named[1]], named[1], named[2])
+            except:
+                message[item[0]] = "#{}{}".format(named[1], named[2])
+
+    # dbg(message)
+    return " ".join(message)
+
+
+def unfurl_refs(text, ignore_alt_text=False):
+    """
+    input : <@U096Q7CQM|someuser> has joined the channel
+    ouput : someuser has joined the channel
+    """
+    # Find all strings enclosed by <>
+    #  - <https://example.com|example with spaces>
+    #  - <#C2147483705|#otherchannel>
+    #  - <@U2147483697|@othernick>
+    # Test patterns lives in ./_pytest/test_unfurl.py
+    matches = re.findall(r"(<[@#]?(?:[^<]*)>)", text)
+    for m in matches:
+        # Replace them with human readable strings
+        text = text.replace(m, unfurl_ref(m[1:-1], ignore_alt_text))
+    return text
+
+
+def unfurl_ref(ref, ignore_alt_text=False):
+    id = ref.split('|')[0]
+    display_text = ref
+    if ref.find('|') > -1:
+        if ignore_alt_text:
+            display_text = resolve_ref(id)
+        else:
+            if id.startswith("#C"):
+                display_text = "#{}".format(ref.split('|')[1])
+            elif id.startswith("@U"):
+                display_text = ref.split('|')[1]
+            else:
+                url, desc = ref.split('|', 1)
+                display_text = "{} ({})".format(url, desc)
+    else:
+        display_text = resolve_ref(ref)
+    return display_text
+
+
+def unwrap_attachments(message_json, text_before):
+    attachment_text = ''
+    a = message_json.get("attachments", None)
+    if a:
+        if text_before:
+            attachment_text = '\n'
+        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', None)
+            title_link = attachment.get('title_link', None)
+            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(prepend_title_text + title)
+                prepend_title_text = ''
+            t.append(attachment.get("from_url", ""))
+
+            atext = attachment.get("text", None)
+            if atext:
+                tx = re.sub(r' *\n[\n ]+', '\n', atext)
+                t.append(prepend_title_text + tx)
+                prepend_title_text = ''
+            fields = attachment.get("fields", None)
+            if fields:
+                for f in fields:
+                    if f['title'] != '':
+                        t.append('%s %s' % (f['title'], f['value'],))
+                    else:
+                        t.append(f['value'])
+            fallback = attachment.get("fallback", None)
+            if t == [] and fallback:
+                t.append(fallback)
+            attachment_text += "\n".join([x.strip() for x in t if x])
+    return attachment_text
+
+
+def resolve_ref(ref):
+    # TODO: This hack to use eventrouter needs to go
+    # this resolver should probably move to the slackteam or eventrouter itself
+    # global EVENTROUTER
+    if 'EVENTROUTER' in globals():
+        e = EVENTROUTER
+        if ref.startswith('@U') or ref.startswith('@W'):
+            for t in e.teams.keys():
+                if ref[1:] in e.teams[t].users:
+                    # try:
+                    return "@{}".format(e.teams[t].users[ref[1:]].name)
+                    # except:
+                    #    dbg("NAME: {}".format(ref))
+        elif ref.startswith('#C'):
+            for t in e.teams.keys():
+                if ref[1:] in e.teams[t].channels:
+                    # try:
+                    return "{}".format(e.teams[t].channels[ref[1:]].name)
+                    # except:
+                    #    dbg("CHANNEL: {}".format(ref))
+
+        # Something else, just return as-is
+    return ref
 
 
 def create_reaction_string(reactions):
@@ -1831,8 +2632,7 @@
     return reaction_string
 
 
-def modify_buffer_line(buffer, new_line, time):
-    time = int(float(time))
+def modify_buffer_line(buffer, new_line, timestamp, time_id):
     # get a pointer to this buffer's lines
     own_lines = w.hdata_pointer(w.hdata_get('buffer'), buffer, 'own_lines')
     if own_lines:
@@ -1846,11 +2646,12 @@
             # get a pointer to the data in line_pointer via layout of struct_hdata_line
             data = w.hdata_pointer(struct_hdata_line, line_pointer, 'data')
             if data:
-                date = w.hdata_time(struct_hdata_line_data, data, 'date')
+                line_timestamp = w.hdata_time(struct_hdata_line_data, data, 'date')
+                line_time_id = w.hdata_integer(struct_hdata_line_data, data, 'date_printed')
                 # prefix = w.hdata_string(struct_hdata_line_data, data, 'prefix')
 
-                if int(date) == int(time):
-                    # w.prnt("", "found matching time date is {}, time is {} ".format(date, time))
+                if timestamp == int(line_timestamp) and int(time_id) == line_time_id:
+                    # w.prnt("", "found matching time date is {}, time is {} ".format(timestamp, line_timestamp))
                     w.hdata_update(struct_hdata_line_data, data, {"message": new_line})
                     break
                 else:
@@ -1860,551 +2661,445 @@
     return w.WEECHAT_RC_OK
 
 
-def render_message(message_json, force=False):
-    # If we already have a rendered version in the object, just return that.
-    if not force and message_json.get("_rendered_text", ""):
-        return message_json["_rendered_text"]
-    else:
-        # server = servers.find(message_json["_server"])
-
-        if "fallback" in message_json:
-            text = message_json["fallback"]
-        elif "text" in message_json:
-            if message_json['text'] is not None:
-                text = message_json["text"]
-            else:
-                text = u""
-        else:
-            text = u""
-
-        text = unfurl_refs(text, ignore_alt_text=config.unfurl_ignore_alt_text)
-
-        text_before = (len(text) > 0)
-        text += unfurl_refs(unwrap_attachments(message_json, text_before), ignore_alt_text=config.unfurl_ignore_alt_text)
-
-        text = text.lstrip()
-        text = text.replace("\t", "    ")
-        text = text.replace("&lt;", "<")
-        text = text.replace("&gt;", ">")
-        text = text.replace("&amp;", "&")
-        text = text.encode('utf-8')
-
-        if "reactions" in message_json:
-            text += create_reaction_string(message_json["reactions"])
-        message_json["_rendered_text"] = text
-        return text
-
-
-def process_message(message_json, cache=True):
-    try:
-        # send these subtype messages elsewhere
-        known_subtypes = ["message_changed", 'message_deleted', 'channel_join', 'channel_leave', 'channel_topic', 'group_join', 'group_leave', 'group_topic', 'bot_enable', 'bot_disable']
-        if "subtype" in message_json and message_json["subtype"] in known_subtypes:
-            proc[message_json["subtype"]](message_json)
-
-        else:
-            server = servers.find(message_json["_server"])
-            channel = channels.find(message_json["channel"])
-
-            # do not process messages in unexpected channels
-            if not channel.active:
-                channel.open(False)
-                dbg("message came for closed channel {}".format(channel.name))
-                return
-
-            time = message_json['ts']
-            text = render_message(message_json)
-            name = get_user(message_json, server)
-            name = name.encode('utf-8')
-
-            # special case with actions.
-            if text.startswith("_") and text.endswith("_"):
-                text = text[1:-1]
-                if name != channel.server.nick:
-                    text = name + " " + text
-                channel.buffer_prnt(w.prefix("action").rstrip(), text, time)
-
-            else:
-                suffix = ''
-                if 'edited' in message_json:
-                    suffix = ' (edited)'
-                channel.buffer_prnt(name, text + suffix, time)
-
-            if cache:
-                channel.cache_message(message_json)
-
-    except Exception:
-        channel = channels.find(message_json["channel"])
-        dbg("cannot process message {}\n{}".format(message_json, traceback.format_exc()))
-        if channel and ("text" in message_json) and message_json['text'] is not None:
-            channel.buffer_prnt('unknown', message_json['text'])
-
-
-def process_message_changed(message_json):
-    m = message_json["message"]
-    if "message" in message_json:
-        if "attachments" in m:
-            message_json["attachments"] = m["attachments"]
-        if "text" in m:
-            if "text" in message_json:
-                message_json["text"] += m["text"]
-                dbg("added text!")
-            else:
-                message_json["text"] = m["text"]
-        if "fallback" in m:
-            if "fallback" in message_json:
-                message_json["fallback"] += m["fallback"]
-            else:
-                message_json["fallback"] = m["fallback"]
-
-    text_before = (len(m['text']) > 0)
-    m["text"] += unwrap_attachments(message_json, text_before)
-    channel = channels.find(message_json["channel"])
-    if "edited" in m:
-        channel.change_message(m["ts"], m["text"], ' (edited)')
-    else:
-        channel.change_message(m["ts"], m["text"])
-
-
-def process_message_deleted(message_json):
-    channel = channels.find(message_json["channel"])
-    channel.change_message(message_json["deleted_ts"], "(deleted)")
-
-
-def unwrap_attachments(message_json, text_before):
-    attachment_text = ''
-    if "attachments" in message_json:
-        if text_before:
-            attachment_text = u'\n'
-        for attachment in message_json["attachments"]:
-            # Attachments should be rendered roughly like:
-            #
-            # $pretext
-            # $author: (if rest of line is non-empty) $title ($title_link) OR $from_url
-            # $author: (if no $author on previous line) $text
-            # $fields
-            t = []
-            prepend_title_text = ''
-            if 'author_name' in attachment:
-                prepend_title_text = attachment['author_name'] + ": "
-            if 'pretext' in attachment:
-                t.append(attachment['pretext'])
-            if "title" in attachment:
-                if 'title_link' in attachment:
-                    t.append('%s%s (%s)' % (prepend_title_text, attachment["title"], attachment["title_link"],))
-                else:
-                    t.append(prepend_title_text + attachment["title"])
-                prepend_title_text = ''
-            elif "from_url" in attachment:
-                t.append(attachment["from_url"])
-            if "text" in attachment:
-                tx = re.sub(r' *\n[\n ]+', '\n', attachment["text"])
-                t.append(prepend_title_text + tx)
-                prepend_title_text = ''
-            if 'fields' in attachment:
-                for f in attachment['fields']:
-                    if f['title'] != '':
-                        t.append('%s %s' % (f['title'], f['value'],))
-                    else:
-                        t.append(f['value'])
-            if t == [] and "fallback" in attachment:
-                t.append(attachment["fallback"])
-            attachment_text += "\n".join([x.strip() for x in t if x])
-    return attachment_text
-
-
-def resolve_ref(ref):
-    if ref.startswith('@U') or ref.startswith('@W'):
-        if users.find(ref[1:]):
-            try:
-                return "@{}".format(users.find(ref[1:]).name)
-            except:
-                dbg("NAME: {}".format(ref))
-    elif ref.startswith('#C'):
-        if channels.find(ref[1:]):
-            try:
-                return "{}".format(channels.find(ref[1:]).name)
-            except:
-                dbg("CHANNEL: {}".format(ref))
-
-    # Something else, just return as-is
-    return ref
-
-
-def unfurl_ref(ref, ignore_alt_text=False):
-    id = ref.split('|')[0]
-    display_text = ref
-    if ref.find('|') > -1:
-        if ignore_alt_text:
-            display_text = resolve_ref(id)
-        else:
-            if id.startswith("#C") or id.startswith("@U"):
-                display_text = ref.split('|')[1]
-            else:
-                url, desc = ref.split('|', 1)
-                display_text = u"{} ({})".format(url, desc)
-    else:
-        display_text = resolve_ref(ref)
-    return display_text
-
-
-def unfurl_refs(text, ignore_alt_text=False):
+def modify_print_time(buffer, new_id, time):
     """
-    input : <@U096Q7CQM|someuser> has joined the channel
-    ouput : someuser has joined the channel
+    This overloads the time printed field to let us store the slack
+    per message unique id that comes after the "." in a slack ts
     """
-    # Find all strings enclosed by <>
-    #  - <https://example.com|example with spaces>
-    #  - <#C2147483705|#otherchannel>
-    #  - <@U2147483697|@othernick>
-    # Test patterns lives in ./_pytest/test_unfurl.py
-    matches = re.findall(r"(<[@#]?(?:[^<]*)>)", text)
-    for m in matches:
-        # Replace them with human readable strings
-        text = text.replace(m, unfurl_ref(m[1:-1], ignore_alt_text))
-    return text
-
-
-def get_user(message_json, server):
-    if 'bot_id' in message_json and message_json['bot_id'] is not None:
-        name = u"{} :]".format(server.bots.find(message_json["bot_id"]).formatted_name())
-    elif 'user' in message_json:
-        u = server.users.find(message_json['user'])
-        if u.is_bot:
-            name = u"{} :]".format(u.formatted_name())
-        else:
-            name = u.name
-    elif 'username' in message_json:
-        name = u"-{}-".format(message_json["username"])
-    elif 'service_name' in message_json:
-        name = u"-{}-".format(message_json["service_name"])
-    else:
-        name = u""
-    return name
-
-# END Websocket handling methods
-
-
-def typing_bar_item_cb(data, buffer, args):
-    typers = [x for x in channels if x.is_someone_typing()]
-    if len(typers) > 0:
-        direct_typers = []
-        channel_typers = []
-        for dm in channels.find_by_class(DmChannel):
-            direct_typers.extend(dm.get_typing_list())
-        direct_typers = ["D/" + x for x in direct_typers]
-        current_channel = w.current_buffer()
-        channel = channels.find(current_channel)
-        try:
-            if channel and channel.__class__ != DmChannel:
-                channel_typers = channels.find(current_channel).get_typing_list()
-        except:
-            w.prnt("", "Bug on {}".format(channel))
-        typing_here = ", ".join(channel_typers + direct_typers)
-        if len(typing_here) > 0:
-            color = w.color('yellow')
-            return color + "typing: " + typing_here
-    return ""
-
-
-def typing_update_cb(data, remaining_calls):
-    w.bar_item_update("slack_typing_notice")
-    return w.WEECHAT_RC_OK
-
-
-def buffer_list_update_cb(data, remaining_calls):
-    global buffer_list_update
-
-    now = time.time()
-    if buffer_list_update and previous_buffer_list_update + 1 < now:
-        # gray_check = False
-        # if len(servers) > 1:
-        #    gray_check = True
-        for channel in channels:
-            channel.rename()
-        buffer_list_update = False
-    return w.WEECHAT_RC_OK
-
-
-def buffer_list_update_next():
-    global buffer_list_update
-    buffer_list_update = True
-
-
-def hotlist_cache_update_cb(data, remaining_calls):
-    # this keeps the hotlist dupe up to date for the buffer switch, but is prob technically a race condition. (meh)
-    global hotlist
-    prev_hotlist = hotlist
-    hotlist = w.infolist_get("hotlist", "", "")
-    w.infolist_free(prev_hotlist)
-    return w.WEECHAT_RC_OK
-
-
-def buffer_closing_cb(signal, sig_type, data):
-    if channels.find(data):
-        channels.find(data).closed()
-    return w.WEECHAT_RC_OK
-
-
-def buffer_opened_cb(signal, sig_type, data):
-    channels.update_hashtable()
-    return w.WEECHAT_RC_OK
-
-
-def buffer_switch_cb(signal, sig_type, data):
-    global previous_buffer, hotlist
-    # this is to see if we need to gray out things in the buffer list
-    if channels.find(previous_buffer):
-        channels.find(previous_buffer).mark_read()
-
-    new_channel = channels.find(data)
-    if new_channel:
-        if new_channel.got_history == False:
-            new_channel.get_history()
-    # channel_name = current_buffer_name()
-    previous_buffer = data
-    return w.WEECHAT_RC_OK
-
-
-def typing_notification_cb(signal, sig_type, data):
-    msg = w.buffer_get_string(data, "input")
-    if len(msg) > 8 and msg[:1] != "/":
-        global typing_timer
-        now = time.time()
-        if typing_timer + 4 < now:
-            channel = channels.find(current_buffer_name())
-            if channel:
-                identifier = channel.identifier
-                request = {"type": "typing", "channel": identifier}
-                channel.server.send_to_websocket(request, expect_reply=False)
-                typing_timer = now
-    return w.WEECHAT_RC_OK
-
-
-def slack_ping_cb(data, remaining):
-    """
-    Periodic websocket ping to detect broken connection.
-    """
-    servers.find(data).ping()
-    return w.WEECHAT_RC_OK
-
-
-def slack_connection_persistence_cb(data, remaining_calls):
-    """
-    Reconnect if a connection is detected down
-    """
-    for server in servers:
-        if not server.connected:
-            server.buffer_prnt("Disconnected from slack, trying to reconnect..")
-            if server.ws_hook is not None:
-                w.unhook(server.ws_hook)
-            server.connect_to_slack()
-    return w.WEECHAT_RC_OK
-
-
-def slack_never_away_cb(data, remaining):
-    global never_away
-    if never_away:
-        for server in servers:
-            identifier = server.channels.find("slackbot").identifier
-            request = {"type": "typing", "channel": identifier}
-            # request = {"type":"typing","channel":"slackbot"}
-            server.send_to_websocket(request, expect_reply=False)
-    return w.WEECHAT_RC_OK
-
-
-def nick_completion_cb(data, completion_item, buffer, completion):
-    """
-    Adds all @-prefixed nicks to completion list
-    """
-
-    channel = channels.find(buffer)
-    if channel is None or channel.members is None:
-        return w.WEECHAT_RC_OK
-    for m in channel.members:
-        user = channel.server.users.find(m)
-        w.hook_completion_list_add(completion, "@" + user.name, 1, w.WEECHAT_LIST_POS_SORT)
-    return w.WEECHAT_RC_OK
-
-
-def complete_next_cb(data, buffer, command):
-    """Extract current word, if it is equal to a nick, prefix it with @ and
-    rely on nick_completion_cb adding the @-prefixed versions to the
-    completion lists, then let Weechat's internal completion do its
-    thing
-
-    """
-
-    channel = channels.find(buffer)
-    if channel is None or channel.members is None:
-        return w.WEECHAT_RC_OK
-    input = w.buffer_get_string(buffer, "input")
-    current_pos = w.buffer_get_integer(buffer, "input_pos") - 1
-    input_length = w.buffer_get_integer(buffer, "input_length")
-    word_start = 0
-    word_end = input_length
-    # If we're on a non-word, look left for something to complete
-    while current_pos >= 0 and input[current_pos] != '@' and not input[current_pos].isalnum():
-        current_pos = current_pos - 1
-    if current_pos < 0:
-        current_pos = 0
-    for l in range(current_pos, 0, -1):
-        if input[l] != '@' and not input[l].isalnum():
-            word_start = l + 1
-            break
-    for l in range(current_pos, input_length):
-        if not input[l].isalnum():
-            word_end = l
-            break
-    word = input[word_start:word_end]
-    for m in channel.members:
-        user = channel.server.users.find(m)
-        if user.name == word:
-            # Here, we cheat.  Insert a @ in front and rely in the @
-            # nicks being in the completion list
-            w.buffer_set(buffer, "input", input[:word_start] + "@" + input[word_start:])
-            w.buffer_set(buffer, "input_pos", str(w.buffer_get_integer(buffer, "input_pos") + 1))
-            return w.WEECHAT_RC_OK_EAT
-    return w.WEECHAT_RC_OK
-
-
-# Slack specific requests
-def async_slack_api_request(domain, token, request, post_data, priority=False):
-    if not STOP_TALKING_TO_SLACK:
-        post_data["token"] = token
-        url = 'url:https://{}/api/{}?{}'.format(domain, request, urllib.urlencode(post_data))
-        context = pickle.dumps({"request": request, "token": token, "post_data": post_data})
-        params = {'useragent': 'wee_slack {}'.format(SCRIPT_VERSION)}
-        dbg("URL: {} context: {} params: {}".format(url, context, params))
-        w.hook_process_hashtable(url, params, config.slack_timeout, "url_processor_cb", context)
-
-
-def async_slack_api_upload_request(token, request, post_data, priority=False):
-    if not STOP_TALKING_TO_SLACK:
-        url = 'https://slack.com/api/{}'.format(request)
-        file_path = os.path.expanduser(post_data["file"])
-        if ' ' in file_path:
-            file_path = file_path.replace(' ','\ ')
-        command = 'curl -F file=@{} -F channels={} -F token={} {}'.format(file_path, post_data["channels"], token, url)
-        context = pickle.dumps({"request": request, "token": token, "post_data": post_data})
-        w.hook_process(command, config.slack_timeout, "url_processor_cb", context)
-
-
-# funny, right?
-big_data = {}
-
-
-def url_processor_cb(data, command, return_code, out, err):
-    global big_data
-    data = pickle.loads(data)
-    identifier = sha.sha("{}{}".format(data, command)).hexdigest()
-    if identifier not in big_data:
-        big_data[identifier] = ''
-    big_data[identifier] += out
-    if return_code == 0:
-        try:
-            my_json = json.loads(big_data[identifier])
-        except:
-            dbg("request failed, doing again...")
-            dbg("response length: {} identifier {}\n{}".format(len(big_data[identifier]), identifier, data))
-            my_json = False
-
-        big_data.pop(identifier, None)
-
-        if my_json:
-            if data["request"] == 'rtm.start':
-                servers.find(data["token"]).connected_to_slack(my_json)
-                servers.update_hashtable()
-
-            else:
-                if "channel" in data["post_data"]:
-                    channel = data["post_data"]["channel"]
-                token = data["token"]
-                if "messages" in my_json:
-                    my_json["messages"].reverse()
-                    for message in my_json["messages"]:
-                        message["_server"] = servers.find(token).domain
-                        message["channel"] = servers.find(token).channels.find(channel).identifier
-                        process_message(message)
-                if "channel" in my_json:
-                    if "members" in my_json["channel"]:
-                        channels.find(my_json["channel"]["id"]).members = set(my_json["channel"]["members"])
-    else:
-        if return_code != -1:
-            big_data.pop(identifier, None)
-        dbg("return code: {}, data: {}, output: {}, error: {}".format(return_code, data, out, err))
+
+    # get a pointer to this buffer's lines
+    own_lines = w.hdata_pointer(w.hdata_get('buffer'), buffer, 'own_lines')
+    if own_lines:
+        # get a pointer to the last line
+        line_pointer = w.hdata_pointer(w.hdata_get('lines'), own_lines, 'last_line')
+        # hold the structure of a line and of line data
+        struct_hdata_line = w.hdata_get('line')
+        struct_hdata_line_data = w.hdata_get('line_data')
+
+        # get a pointer to the data in line_pointer via layout of struct_hdata_line
+        data = w.hdata_pointer(struct_hdata_line, line_pointer, 'data')
+        if data:
+            w.hdata_update(struct_hdata_line_data, data, {"date_printed": new_id})
 
     return w.WEECHAT_RC_OK
 
 
-def cache_write_cb(data, remaining):
-    cache_file = open("{}/{}".format(WEECHAT_HOME, CACHE_NAME), 'w')
-    cache_file.write(CACHE_VERSION + "\n")
-    for channel in channels:
-        if channel.active:
-            for message in channel.messages:
-                cache_file.write("{}\n".format(json.dumps(message.message_json)))
-    return w.WEECHAT_RC_OK
-
-
-def cache_load():
-    global message_cache
+def tag(tagset, user=None):
+    if user:
+        user.replace(" ", "_")
+        default_tag = "nick_" + user
+    else:
+        default_tag = 'nick_unknown'
+    tagsets = {
+        # when replaying something old
+        "backlog": "no_highlight,notify_none,logger_backlog_end",
+        # when posting messages to a muted channel
+        "muted": "no_highlight,notify_none,logger_backlog_end",
+        # when my nick is in the message
+        "highlightme": "notify_highlight,log1",
+        # when receiving a direct message
+        "dm": "notify_private,notify_message,log1,irc_privmsg",
+        "dmfromme": "notify_none,log1,irc_privmsg",
+        # when this is a join/leave, attach for smart filter ala:
+        # if user in [x.strip() for x in w.prefix("join"), w.prefix("quit")]
+        "joinleave": "irc_smart_filter,no_highlight",
+        # catchall ?
+        "default": "notify_message,log1",
+    }
+    return default_tag + "," + tagsets[tagset]
+
+###### New/converted command_ commands
+
+
+@slack_buffer_or_ignore
+def part_command_cb(data, current_buffer, args):
+    data = decode_from_utf8(data)
+    args = decode_from_utf8(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:
+        e.weechat_controller.unregister_buffer(current_buffer, update_remote=True, close_buffer=True)
+    return w.WEECHAT_RC_OK_EAT
+
+
+@slack_buffer_or_ignore
+def topic_command_cb(data, current_buffer, args):
+    n = len(args.split())
+    if n < 2:
+        channel = channels.find(current_buffer)
+        if channel:
+            w.prnt(current_buffer, 'Topic for {} is "{}"'.format(channel.name, channel.topic))
+        return w.WEECHAT_RC_OK_EAT
+    elif command_topic(data, current_buffer, args.split(None, 1)[1]):
+        return w.WEECHAT_RC_OK_EAT
+    else:
+        return w.WEECHAT_RC_ERROR
+
+
+@slack_buffer_required
+def command_topic(data, current_buffer, args):
+    """
+    Change the topic of a channel
+    /slack topic [<channel>] [<topic>|-delete]
+    """
+    data = decode_from_utf8(data)
+    args = decode_from_utf8(args)
+    e = EVENTROUTER
+    team = e.weechat_controller.buffers[current_buffer].team
+    # server = servers.find(current_domain_name())
+    args = args.split(' ')
+    if len(args) > 2 and args[1].startswith('#'):
+        cmap = team.get_channel_map()
+        channel_name = args[1][1:]
+        channel = team.channels[cmap[channel_name]]
+        topic = " ".join(args[2:])
+    else:
+        channel = e.weechat_controller.buffers[current_buffer]
+        topic = " ".join(args[1:])
+
+    if channel:
+        if topic == "-delete":
+            topic = ''
+        s = SlackRequest(team.token, "channels.setTopic", {"channel": channel.identifier, "topic": topic}, team_hash=team.team_hash)
+        EVENTROUTER.receive(s)
+        return w.WEECHAT_RC_OK_EAT
+    else:
+        return w.WEECHAT_RC_ERROR_EAT
+
+
+@slack_buffer_or_ignore
+def me_command_cb(data, current_buffer, args):
+    data = decode_from_utf8(data)
+    args = decode_from_utf8(args)
+    message = "_{}_".format(args.split(' ', 1)[1])
+    buffer_input_callback("EVENTROUTER", current_buffer, message)
+    return w.WEECHAT_RC_OK_EAT
+
+
+@slack_buffer_or_ignore
+def msg_command_cb(data, current_buffer, args):
+    data = decode_from_utf8(data)
+    args = decode_from_utf8(args)
+    dbg("msg_command_cb")
+    aargs = args.split(None, 2)
+    who = aargs[1]
+    command_talk(data, current_buffer, 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[channel]]
+            channel.send_message(message)
+    return w.WEECHAT_RC_OK_EAT
+
+
+@slack_buffer_or_ignore
+def command_talk(data, current_buffer, args):
+    """
+    Open a chat with the specified user
+    /slack talk [user]
+    """
+
+    data = decode_from_utf8(data)
+    args = decode_from_utf8(args)
+    e = EVENTROUTER
+    team = e.weechat_controller.buffers[current_buffer].team
+    channel_name = args.split(' ')[1]
+    c = team.get_channel_map()
+    if channel_name not in c:
+        u = team.get_username_map()
+        if channel_name in u:
+            s = SlackRequest(team.token, "im.open", {"user": u[channel_name]}, team_hash=team.team_hash)
+            EVENTROUTER.receive(s)
+            dbg("found user")
+            # refresh channel map here
+            c = team.get_channel_map()
+
+    if channel_name.startswith('#'):
+        channel_name = channel_name[1:]
+    if channel_name in c:
+        chan = team.channels[c[channel_name]]
+        chan.open()
+        if config.switch_buffer_on_join:
+            w.buffer_set(chan.channel_buffer, "display", "1")
+        return w.WEECHAT_RC_OK_EAT
+    return w.WEECHAT_RC_OK_EAT
+
+
+def command_showmuted(data, current_buffer, args):
+    current = w.current_buffer()
+    w.prnt(EVENTROUTER.weechat_controller.buffers[current].team.channel_buffer, str(EVENTROUTER.weechat_controller.buffers[current].team.muted_channels))
+
+
+def thread_command_callback(data, current_buffer, args):
+    data = decode_from_utf8(data)
+    args = decode_from_utf8(args)
+    current = w.current_buffer()
+    channel = EVENTROUTER.weechat_controller.buffers.get(current)
+    if channel:
+        args = args.split()
+        if args[0] == '/thread':
+            if len(args) == 2:
+                try:
+                    pm = channel.messages[SlackTS(args[1])]
+                except:
+                    pm = channel.hashed_messages[args[1]]
+                tc = SlackThreadChannel(EVENTROUTER, pm)
+                pm.thread_channel = tc
+                tc.open()
+                # tc.create_buffer()
+                return w.WEECHAT_RC_OK_EAT
+        elif args[0] == '/reply':
+            count = int(args[1])
+            msg = " ".join(args[2:])
+            mkeys = channel.sorted_message_keys()
+            mkeys.reverse()
+            parent_id = str(mkeys[count - 1])
+            channel.send_message(msg, request_dict_ext={"thread_ts": parent_id})
+            return w.WEECHAT_RC_OK_EAT
+        w.prnt(current, "Invalid thread command.")
+        return w.WEECHAT_RC_OK_EAT
+
+
+def rehistory_command_callback(data, current_buffer, args):
+    data = decode_from_utf8(data)
+    args = decode_from_utf8(args)
+    current = w.current_buffer()
+    channel = EVENTROUTER.weechat_controller.buffers.get(current)
+    channel.got_history = False
+    w.buffer_clear(channel.channel_buffer)
+    channel.get_history()
+    return w.WEECHAT_RC_OK_EAT
+
+
+@slack_buffer_required
+def hide_command_callback(data, current_buffer, args):
+    data = decode_from_utf8(data)
+    args = decode_from_utf8(args)
+    c = EVENTROUTER.weechat_controller.buffers.get(current_buffer, None)
+    if c:
+        name = c.formatted_name(style='long_default')
+        if name in config.distracting_channels:
+            w.buffer_set(c.channel_buffer, "hidden", "1")
+    return w.WEECHAT_RC_OK_EAT
+
+
+def slack_command_cb(data, current_buffer, args):
+    data = decode_from_utf8(data)
+    args = decode_from_utf8(args)
+    a = args.split(' ', 1)
+    if len(a) > 1:
+        function_name, args = a[0], args
+    else:
+        function_name, args = a[0], args
+
     try:
-        file_name = "{}/{}".format(WEECHAT_HOME, CACHE_NAME)
-        cache_file = open(file_name, 'r')
-        if cache_file.readline() == CACHE_VERSION + "\n":
-            dbg("Loading messages from cache.", main_buffer=True)
-            for line in cache_file:
-                j = json.loads(line)
-                message_cache[j["channel"]].append(line)
-            dbg("Completed loading messages from cache.", main_buffer=True)
-    except ValueError:
-        w.prnt("", "Failed to load cache file, probably illegal JSON.. Ignoring")
-        pass
-    except IOError:
-        w.prnt("", "cache file not found")
-        pass
-
-# END Slack specific requests
-
-# Utility Methods
-
-
-def current_domain_name():
-    buffer = w.current_buffer()
-    if servers.find(buffer):
-        return servers.find(buffer).domain
+        EVENTROUTER.cmds[function_name]("", current_buffer, args)
+    except KeyError:
+        w.prnt("", "Command not found: " + function_name)
+    return w.WEECHAT_RC_OK
+
+
+@slack_buffer_required
+def command_distracting(data, current_buffer, args):
+    channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer, None)
+    if channel:
+        fullname = channel.formatted_name(style="long_default")
+    if config.distracting_channels.count(fullname) == 0:
+        config.distracting_channels.append(fullname)
+    else:
+        config.distracting_channels.pop(config.distracting_channels.index(fullname))
+    save_distracting_channels()
+
+
+def save_distracting_channels():
+    w.config_set_plugin('distracting_channels', ','.join(config.distracting_channels))
+
+
+@slack_buffer_required
+def command_slash(data, current_buffer, args):
+    """
+    Support for custom slack commands
+    /slack slash /customcommand arg1 arg2 arg3
+    """
+    e = EVENTROUTER
+    channel = e.weechat_controller.buffers.get(current_buffer, None)
+    if channel:
+        team = channel.team
+
+        if args is None:
+            server.buffer_prnt("Usage: /slack slash /someslashcommand [arguments...].")
+            return
+
+        split_args = args.split(None, 2)
+        command = split_args[1]
+        text = split_args[2] if len(split_args) > 2 else ""
+
+        s = SlackRequest(team.token, "chat.command", {"command": command, "text": text, 'channel': channel.identifier}, team_hash=team.team_hash, channel_identifier=channel.identifier)
+        EVENTROUTER.receive(s)
+
+
+@slack_buffer_required
+def command_mute(data, current_buffer, args):
+    current = w.current_buffer()
+    channel_id = EVENTROUTER.weechat_controller.buffers[current].identifier
+    team = EVENTROUTER.weechat_controller.buffers[current].team
+    if channel_id not in team.muted_channels:
+        team.muted_channels.add(channel_id)
+    else:
+        team.muted_channels.discard(channel_id)
+    s = SlackRequest(team.token, "users.prefs.set", {"name": "muted_channels", "value": ",".join(team.muted_channels)}, team_hash=team.team_hash, channel_identifier=channel_id)
+    EVENTROUTER.receive(s)
+
+
+@slack_buffer_required
+def command_openweb(data, current_buffer, args):
+    # if done from server buffer, open slack for reals
+    channel = EVENTROUTER.weechat_controller.buffers[current_buffer]
+    if isinstance(channel, SlackTeam):
+        url = "https://{}".format(channel.team.domain)
     else:
-        # number = w.buffer_get_integer(buffer, "number")
-        name = w.buffer_get_string(buffer, "name")
-        name = ".".join(name.split(".")[:-1])
-        return name
-
-
-def current_buffer_name(short=False):
-    buffer = w.current_buffer()
-    # number = w.buffer_get_integer(buffer, "number")
-    name = w.buffer_get_string(buffer, "name")
-    if short:
-        try:
-            name = name.split('.')[-1]
-        except:
-            pass
-    return name
-
-
-def closed_slack_buffer_cb(data, buffer):
-    global slack_buffer
-    slack_buffer = None
+        now = SlackTS()
+        url = "https://{}/archives/{}/p{}000000".format(channel.team.domain, channel.slack_name, now.majorstr())
+    w.prnt_date_tags(channel.team.channel_buffer, SlackTS().major, "openweb,logger_backlog_end,notify_none", url)
+
+
+def command_nodistractions(data, current_buffer, args):
+    global hide_distractions
+    hide_distractions = not hide_distractions
+    if config.distracting_channels != ['']:
+        for channel in config.distracting_channels:
+            dbg('hiding channel {}'.format(channel))
+            # try:
+            for c in EVENTROUTER.weechat_controller.buffers.itervalues():
+                if c == channel:
+                    dbg('found channel {} to hide'.format(channel))
+                    w.buffer_set(c.channel_buffer, "hidden", str(int(hide_distractions)))
+            # except:
+            #    dbg("Can't hide channel {} .. removing..".format(channel), main_buffer=True)
+#                config.distracting_channels.pop(config.distracting_channels.index(channel))
+#                save_distracting_channels()
+
+
+@slack_buffer_required
+def command_upload(data, current_buffer, args):
+    channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer)
+    url = 'https://slack.com/api/files.upload'
+    fname = args.split(' ', 1)
+    file_path = os.path.expanduser(fname[1])
+    team = EVENTROUTER.weechat_controller.buffers[current_buffer].team
+    if ' ' in file_path:
+        file_path = file_path.replace(' ', '\ ')
+
+    command = 'curl -F file=@{} -F channels={} -F token={} {}'.format(file_path, channel.identifier, team.token, url)
+    w.hook_process(command, config.slack_timeout, '', '')
+
+
+def away_command_cb(data, current_buffer, args):
+    data = decode_from_utf8(data)
+    args = decode_from_utf8(args)
+    # TODO: reimplement all.. maybe
+    (all, message) = re.match("^/away(?:\s+(-all))?(?:\s+(.+))?", args).groups()
+    if message is None:
+        command_back(data, current_buffer, args)
+    else:
+        command_away(data, current_buffer, args)
     return w.WEECHAT_RC_OK
 
 
-def create_slack_buffer():
-    global slack_buffer
-    slack_buffer = w.buffer_new("slack", "", "", "closed_slack_buffer_cb", "")
-    w.buffer_set(slack_buffer, "notify", "0")
-    # w.buffer_set(slack_buffer, "display", "1")
-    return w.WEECHAT_RC_OK
+@slack_buffer_required
+def command_away(data, current_buffer, args):
+    """
+    Sets your status as 'away'
+    /slack away
+    """
+    team = EVENTROUTER.weechat_controller.buffers[current_buffer].team
+    s = SlackRequest(team.token, "presence.set", {"presence": "away"}, team_hash=team.team_hash)
+    EVENTROUTER.receive(s)
+
+
+@slack_buffer_required
+def command_status(data, current_buffer, args):
+    """
+    Lets you set your Slack Status (not to be confused with away/here)
+    /slack status [emoji] [status_message]
+    """
+    e = EVENTROUTER
+    channel = e.weechat_controller.buffers.get(current_buffer, None)
+    if channel:
+        team = channel.team
+
+        if args is None:
+            server.buffer_prnt("Usage: /slack status [status emoji] [status text].")
+            return
+
+        split_args = args.split(None, 2)
+        emoji = split_args[1] if len(split_args) > 1 else ""
+        text = split_args[2] if len(split_args) > 2 else ""
+
+        profile = {"status_text":text,"status_emoji":emoji}
+
+        s = SlackRequest(team.token, "users.profile.set", {"profile": profile}, team_hash=team.team_hash, channel_identifier=channel.identifier)
+        EVENTROUTER.receive(s)
+
+
+@slack_buffer_required
+def command_back(data, current_buffer, args):
+    """
+    Sets your status as 'back'
+    /slack back
+    """
+    team = EVENTROUTER.weechat_controller.buffers[current_buffer].team
+    s = SlackRequest(team.token, "presence.set", {"presence": "active"}, team_hash=team.team_hash)
+    EVENTROUTER.receive(s)
+
+
+@slack_buffer_required
+def label_command_cb(data, current_buffer, args):
+    data = decode_from_utf8(data)
+    args = decode_from_utf8(args)
+    channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer)
+    if channel and channel.type == 'thread':
+        aargs = args.split(None, 2)
+        new_name = " +" + aargs[1]
+        channel.label = new_name
+        w.buffer_set(channel.channel_buffer, "short_name", new_name)
+
+
+def command_p(data, current_buffer, args):
+    args = args.split(' ', 1)[1]
+    w.prnt("", "{}".format(eval(args)))
+
+###### NEW EXCEPTIONS
+
+
+class ProcessNotImplemented(Exception):
+    """
+    Raised when we try to call process_(something), but
+    (something) has not been defined as a function.
+    """
+    def __init__(self, function_name):
+        super(ProcessNotImplemented, self).__init__(function_name)
+
+
+class InvalidType(Exception):
+    """
+    Raised when we do type checking to ensure objects of the wrong
+    type are not used improperly.
+    """
+    def __init__(self, type_str):
+        super(InvalidType, self).__init__(type_str)
+
+###### New but probably old and need to migrate
 
 
 def closed_slack_debug_buffer_cb(data, buffer):
@@ -2423,38 +3118,98 @@
         w.buffer_set(slack_debug, "notify", "0")
 
 
-def quit_notification_cb(signal, sig_type, data):
-    stop_talking_to_slack()
-
-
-def script_unloaded():
-    stop_talking_to_slack()
+def load_emoji():
+    try:
+        global EMOJI
+        DIR = w.info_get("weechat_dir", "")
+        # no idea why this does't work w/o checking the type?!
+        dbg(type(DIR), 0)
+        ef = open('{}/weemoji.json'.format(DIR), 'r')
+        EMOJI = json.loads(ef.read())
+        ef.close()
+    except:
+        dbg("Unexpected error: {}".format(sys.exc_info()), 5)
     return w.WEECHAT_RC_OK
 
 
-def stop_talking_to_slack():
-    """
-    Prevents a race condition where quitting closes buffers
-    which triggers leaving the channel because of how close
-    buffer is handled
+def setup_hooks():
+    cmds = {k[8:]: v for k, v in globals().items() if k.startswith("command_")}
+
+    w.bar_item_new('slack_typing_notice', 'typing_bar_item_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", "EVENTROUTER")
+    w.hook_signal('buffer_switch', "buffer_switch_callback", "EVENTROUTER")
+    w.hook_signal('window_switch', "buffer_switch_callback", "EVENTROUTER")
+    w.hook_signal('quit', "quit_notification_cb", "")
+    w.hook_signal('input_text_changed', "typing_notification_cb", "")
+
+    w.hook_command(
+        # Command name and description
+        'slack', 'Plugin to allow typing notification and sync of read markers for slack.com',
+        # Usage
+        '[command] [command options]',
+        # Description of arguments
+        'Commands:\n' +
+        '\n'.join(cmds.keys()) +
+        '\nUse /slack help [command] to find out more\n',
+        # Completions
+        '|'.join(cmds.keys()),
+        # Function name
+        'slack_command_cb', '')
+    # w.hook_command('me', '', 'stuff', 'stuff2', '', 'me_command_cb', '')
+
+    w.hook_command_run('/me', 'me_command_cb', '')
+    w.hook_command_run('/query', 'command_talk', '')
+    w.hook_command_run('/join', 'command_talk', '')
+    w.hook_command_run('/part', 'part_command_cb', '')
+    w.hook_command_run('/leave', 'part_command_cb', '')
+    w.hook_command_run('/topic', 'command_topic', '')
+    w.hook_command_run('/thread', 'thread_command_callback', '')
+    w.hook_command_run('/reply', 'thread_command_callback', '')
+    w.hook_command_run('/rehistory', 'rehistory_command_callback', '')
+    w.hook_command_run('/hide', 'hide_command_callback', '')
+    w.hook_command_run('/msg', 'msg_command_cb', '')
+    w.hook_command_run('/label', 'label_command_cb', '')
+    w.hook_command_run("/input complete_next", "complete_next_cb", "")
+    w.hook_command_run('/away', 'away_command_cb', '')
+
+    w.hook_completion("nicks", "complete @-nicks for slack", "nick_completion_cb", "")
+    w.hook_completion("emoji", "complete :emoji: for slack", "emoji_completion_cb", "")
+
+    # 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):
     """
-    global STOP_TALKING_TO_SLACK
-    STOP_TALKING_TO_SLACK = True
-    cache_write_cb("", "")
-    return w.WEECHAT_RC_OK
-
-
-def scrolled_cb(signal, sig_type, data):
-    try:
-        if w.window_get_integer(data, "scrolling") == 1:
-            channels.find(w.current_buffer()).set_scrolling()
+    send debug output to the slack-debug buffer and optionally write to a file.
+    """
+    # TODO: do this smarter
+    # return
+    if level >= config.debug_level:
+        global debug_string
+        message = "DEBUG: {}".format(message)
+        if fout:
+            file('/tmp/debug.log', 'a+').writelines(message + '\n')
+        if main_buffer:
+                # w.prnt("", "---------")
+                w.prnt("", "slack: " + message)
         else:
-            channels.find(w.current_buffer()).unset_scrolling()
-    except:
-        pass
-    return w.WEECHAT_RC_OK
-
-# END Utility Methods
+            if slack_debug and (not debug_string or debug_string in message):
+                # w.prnt(slack_debug, "---------")
+                w.prnt(slack_debug, message)
+
+###### Config code
+
 
 class PluginConfig(object):
     # Default settings.
@@ -2464,10 +3219,9 @@
     # extracted.
     # TODO: setting descriptions.
     settings = {
-        'colorize_messages': 'false',
-        'colorize_nicks': 'true',
         'colorize_private_chats': 'false',
         'debug_mode': 'false',
+        'debug_level': '3',
         'distracting_channels': '',
         'show_reaction_nicks': 'false',
         'slack_api_token': 'INSERT VALID KEY HERE!',
@@ -2475,12 +3229,21 @@
         'switch_buffer_on_join': 'true',
         'trigger_value': 'false',
         'unfurl_ignore_alt_text': 'false',
+        'record_events': 'false',
+        'thread_suffix_color': 'lightcyan',
+        'unhide_buffers_with_activity': 'false',
+        'short_buffer_names': 'false',
+        'channel_name_typing_indicator': 'true',
+        'background_load_all_history': 'false',
+        'never_away': 'false',
+        'server_aliases': '',
     }
 
     # Set missing settings to their defaults. Load non-missing settings from
     # weechat configs.
     def __init__(self):
-        for key,default in self.settings.iteritems():
+        self.migrate()
+        for key, default in self.settings.iteritems():
             if not w.config_get_plugin(key):
                 w.config_set_plugin(key, default)
         self.config_changed(None, None, None)
@@ -2514,6 +3277,11 @@
     def get_distracting_channels(self, key):
         return [x.strip() for x in w.config_get_plugin(key).split(',')]
 
+    def get_server_aliases(self, key):
+        alias_list = w.config_get_plugin(key)
+        if len(alias_list) > 0:
+            return dict(item.split(":") for item in alias_list.split(","))
+
     def get_slack_api_token(self, key):
         token = w.config_get_plugin("slack_api_token")
         if token.startswith('${sec.data'):
@@ -2521,13 +3289,63 @@
         else:
             return token
 
+    def get_thread_suffix_color(self, key):
+        return w.config_get_plugin("thread_suffix_color")
+
+    def get_debug_level(self, key):
+        return int(w.config_get_plugin(key))
+
     def get_slack_timeout(self, key):
         return int(w.config_get_plugin(key))
 
+    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")
+
+
+# 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 >> f, '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)
+    f.flush()
+    return
+
 
 # Main
 if __name__ == "__main__":
 
+    w = WeechatWrapper(weechat)
+
     if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE,
                   SCRIPT_DESC, "script_unloaded", ""):
 
@@ -2536,81 +3354,41 @@
             w.prnt("", "\nERROR: Weechat version 1.3+ is required to use {}.\n\n".format(SCRIPT_NAME))
         else:
 
-            WEECHAT_HOME = w.info_get("weechat_dir", "")
-            CACHE_NAME = "slack.cache"
-            STOP_TALKING_TO_SLACK = False
+            global EVENTROUTER
+            EVENTROUTER = EventRouter()
+            # setup_trace()
+
+            # WEECHAT_HOME = w.info_get("weechat_dir", "")
+            # STOP_TALKING_TO_SLACK = False
 
             # Global var section
             slack_debug = None
             config = PluginConfig()
             config_changed_cb = config.config_changed
 
-            cmds = {k[8:]: v for k, v in globals().items() if k.startswith("command_")}
-            proc = {k[8:]: v for k, v in globals().items() if k.startswith("process_")}
-
             typing_timer = time.time()
-            domain = None
-            previous_buffer = None
-            slack_buffer = None
-
-            buffer_list_update = False
-            previous_buffer_list_update = 0
-
-            never_away = False
+            # domain = None
+            # previous_buffer = None
+            # slack_buffer = None
+
+            # never_away = False
             hide_distractions = False
-            hotlist = w.infolist_get("hotlist", "", "")
-            main_weechat_buffer = w.info_get("irc_buffer", "{}.{}".format(domain, "DOESNOTEXIST!@#$"))
-
-            message_cache = collections.defaultdict(list)
-            cache_load()
-
-            servers = SearchList()
-            for token in config.slack_api_token.split(','):
-                server = SlackServer(token)
-                servers.append(server)
-            channels = SearchList()
-            users = SearchList()
+            # hotlist = w.infolist_get("hotlist", "", "")
+            # main_weechat_buffer = w.info_get("irc_buffer", "{}.{}".format(domain, "DOESNOTEXIST!@#$"))
 
             w.hook_config("plugins.var.python." + SCRIPT_NAME + ".*", "config_changed_cb", "")
-            w.hook_timer(3000, 0, 0, "slack_connection_persistence_cb", "")
+
+            load_emoji()
+            setup_hooks()
 
             # attach to the weechat hooks we need
-            w.hook_timer(1000, 0, 0, "typing_update_cb", "")
-            w.hook_timer(1000, 0, 0, "buffer_list_update_cb", "")
-            w.hook_timer(1000, 0, 0, "hotlist_cache_update_cb", "")
-            w.hook_timer(1000 * 60 * 29, 0, 0, "slack_never_away_cb", "")
-            w.hook_timer(1000 * 60 * 5, 0, 0, "cache_write_cb", "")
-            w.hook_signal('buffer_closing', "buffer_closing_cb", "")
-            w.hook_signal('buffer_opened', "buffer_opened_cb", "")
-            w.hook_signal('buffer_switch', "buffer_switch_cb", "")
-            w.hook_signal('window_switch', "buffer_switch_cb", "")
-            w.hook_signal('input_text_changed', "typing_notification_cb", "")
-            w.hook_signal('quit', "quit_notification_cb", "")
-            w.hook_signal('window_scrolled', "scrolled_cb", "")
-            w.hook_command(
-                # Command name and description
-                'slack', 'Plugin to allow typing notification and sync of read markers for slack.com',
-                # Usage
-                '[command] [command options]',
-                # Description of arguments
-                'Commands:\n' +
-                '\n'.join(cmds.keys()) +
-                '\nUse /slack help [command] to find out more\n',
-                # Completions
-                '|'.join(cmds.keys()),
-                # Function name
-                'slack_command_cb', '')
-    #        w.hook_command('me', 'me_command_cb', '')
-            w.hook_command('me', '', 'stuff', 'stuff2', '', 'me_command_cb', '')
-            w.hook_command_run('/query', 'join_command_cb', '')
-            w.hook_command_run('/join', 'join_command_cb', '')
-            w.hook_command_run('/part', 'part_command_cb', '')
-            w.hook_command_run('/leave', 'part_command_cb', '')
-            w.hook_command_run('/topic', 'topic_command_cb', '')
-            w.hook_command_run('/msg', 'msg_command_cb', '')
-            w.hook_command_run("/input complete_next", "complete_next_cb", "")
-            w.hook_command_run('/away', 'away_command_cb', '')
-            w.hook_completion("nicks", "complete @-nicks for slack",
-                              "nick_completion_cb", "")
-            w.bar_item_new('slack_typing_notice', 'typing_bar_item_cb', '')
+
+            tokens = config.slack_api_token.split(',')
+            for t in tokens:
+                s = SlackRequest(t, 'rtm.start', {})
+                EVENTROUTER.receive(s)
+            if config.record_events:
+                EVENTROUTER.record()
+            EVENTROUTER.handle_next()
+            w.hook_timer(10, 0, 0, "handle_next", "")
             # END attach to the weechat hooks we need