# HG changeset patch # User Steve Losh # Date 1499181903 0 # Node ID b89b95f1cb1d06596c0c46bfb5a9e7e95ce71913 # Parent 73adae8ca49db3eba032a0028e51c8190c9fb410 Oh god diff -r 73adae8ca49d -r b89b95f1cb1d .hgsub --- 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 diff -r 73adae8ca49d -r b89b95f1cb1d .hgsubstate --- 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 diff -r 73adae8ca49d -r b89b95f1cb1d bin/sbcl-vlime --- /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\")" "$@" diff -r 73adae8ca49d -r b89b95f1cb1d hgignore --- 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 diff -r 73adae8ca49d -r b89b95f1cb1d lispwords --- 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) diff -r 73adae8ca49d -r b89b95f1cb1d vim/bundle/vlime-vim --- /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 diff -r 73adae8ca49d -r b89b95f1cb1d vim/vimrc --- 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 command! -bang Q q @@ -474,18 +473,12 @@ command! -bang Wq wq command! -bang WQ wq -" 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= nnoremap :set paste! -" Toggle [i]nvisible characters -nnoremap i :set list! - " Unfuck my screen nnoremap U :syntax sync fromstart:redraw! @@ -493,6 +486,9 @@ nnoremap Go :Start! git push origin nnoremap Gu :Start! git push upstream +" Open current directory in Finder +nnoremap O :!open . + " 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=@"x$p:let @"=@z -" Ranger -nnoremap r :silent !ranger %:h:redraw! -nnoremap R :silent !ranger:redraw! - -" Jump (see the J mini-plugin later on) -nnoremap J :J - " Indent from insert mode " has to be imap because we want to be able to use the "go-indent" mapping imap gi @@ -861,22 +850,30 @@ hi def link replResult Debug hi def link replComment Comment endfunction "}}} +function! MapLispReplKeys() "{{{ + nnoremap q :call QuickloadLispSystem() + nnoremap Q :call QuickloadLispPrompt() +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 + au FileType lisp setlocal iskeyword+=!,?,% au FileType lisp setlocal equalprg=lispindent " scratch buffer au FileType lisp nnoremap :e scratch.lisp - " Handy stuff + " Open REPLs + au FileType lisp nnoremap Os :call OpenLispReplSBCL() + au FileType lisp nnoremap Oc :call OpenLispReplCCL() + au FileType lisp nnoremap Oe :call OpenLispReplECL() + au FileType lisp nnoremap Oa :call OpenLispReplABCL() + + " Misc mappings + au FileType lisp nnoremap gi :call IndentToplevelLispForm() + au FileType lisp nnoremap q :call QuickloadLispSystem() + au FileType lisp nnoremap Q :call QuickloadLispPrompt() au FileType lisp nnoremap [] :call DuplicateLispForm() - " 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 os :call OpenLispReplSBCL() - au FileType lisp nnoremap oc :call OpenLispReplCCL() - au FileType lisp nnoremap oe :call OpenLispReplECL() - au FileType lisp nnoremap oa :call OpenLispReplABCL() - au FileType lisp nnoremap s :call SendToplevelLispForm() - au FileType lisp nnoremap c :call NeoReplSendRaw("nil\n"):sleep 20m:call NeoReplSendRaw(" ") - au FileType lisp nnoremap Q :call NeoReplSendRaw("(load \"vendor/quickutils.lisp\")\n") - au FileType lisp nnoremap 0 :call NeoReplSendRaw("0\n") - au FileType lisp nnoremap 1 :call NeoReplSendRaw("1\n") - au FileType lisp nnoremap 2 :call NeoReplSendRaw("2\n") - au FileType lisp nnoremap 3 :call NeoReplSendRaw("3\n") - au FileType lisp nnoremap gi :call IndentToplevelLispForm() - au FileType lisp nnoremap d :call DisassembleLispSymbol() - au FileType lisp nnoremap q :call QuickloadLispSystem() - " Navigate trees of sexps with arrows au FileType lisp call s:vim_sexp_mappings() au FileType lisp noremap :call SexpBack() @@ -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 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 =unfuck_deoplete_enter() -" inoremap ) =unfuck_deoplete_rparen() - -function! s:unfuck_deoplete_enter() abort - return deoplete#close_popup() . "\" -endfunction - -" function! s:unfuck_deoplete_rparen() abort -" return deoplete#close_popup() . ")" -" endfunction - -" let g:deoplete#disable_auto_complete = 1 - -" inoremap -" \ pumvisible() ? "\" : -" \ check_back_space() ? "\" : -" \ 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 d :Dispatch @@ -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 s + " Please just stop + nunmap W + nunmap O + " Oh my god will you fuck off already " nnoremap dp :diffput " nnoremap do :diffobtain @@ -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 :call vlime#plugin#FindDefinition(vlime#ui#CurAtom()) + nnoremap - :call CleanVlimeWindows() +endfunction + +augroup CustomVlimeInputBuffer + autocmd! + " autocmd FileType vlime_input inoremap =VlimeKey("tab") + autocmd FileType vlime_input setlocal omnifunc=VlimeCompleteFunc + " autocmd FileType vlime_input setlocal indentexpr=VlimeCalcCurIndent() + autocmd FileType vlime_input inoremap +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 e :call vlime#plugin#Compile(vlime#ui#CurTopExpr(v:true)) + au FileType lisp nnoremap f :call vlime#plugin#CompileFile(expand("%:p")) + au FileType lisp nnoremap S :call vlime#plugin#SendToREPL(vlime#ui#CurTopExpr()) + au FileType lisp nnoremap i :call vlime#plugin#Inspect(vlime#ui#CurExprOrAtom()) + au FileType lisp nnoremap M :call vlime#plugin#DocumentationSymbol(vlime#ui#CurOperator()) + + " 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 + au FileType vlime_xref nnoremap :call vlime#ui#xref#OpenCurXref() + au FileType vlime_notes nnoremap :call vlime#ui#compiler_notes#OpenCurNote() + au FileType vlime_sldb nnoremap :call vlime#ui#sldb#ChooseCurRestart() + au FileType vlime_inspector nnoremap :call vlime#ui#inspector#InspectorSelect() + + " Fix d + au FileType vlime_sldb nnoremap d :call vlime#ui#sldb#ShowFrameDetails() + + " Fix p + au FileType vlime_inspector nnoremap p :call vlime#ui#inspector#InspectorPop() +augroup end + +" }}} +" Windowswap {{{ + +let g:windowswap_map_keys = 0 "prevent default bindings +nnoremap W :call WindowSwap#EasyWindowSwap() + +" }}} " }}} " Text objects ------------------------------------------------------------ {{{ @@ -2599,7 +2637,7 @@ endfunc " TODO: Figure out the diffexpr shit necessary to make this buffer-local. -nnoremap W :call ToggleDiffWhitespace() +" nnoremap W :call ToggleDiffWhitespace() " }}} " Error Toggles {{{ diff -r 73adae8ca49d -r b89b95f1cb1d weechat/python/autoload/wee_slack.py --- 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 " -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 [] [|-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("<", "<") + text = text.replace(">", ">") + text = text.replace("&", "&") + 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 <> + # - + # - <#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("<", "<") - text = text.replace(">", ">") - text = text.replace("&", "&") - 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 <> - # - - # - <#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 [] [|-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