# HG changeset patch # User Steve Losh # Date 1507491060 14400 # Node ID 15bd14518a985c3e39d2309468be69ab65ec5550 # Parent 2b37786c6a97e6330077d2fa398aa3964523eba8 More diff -r 2b37786c6a97 -r 15bd14518a98 .hgsubstate --- a/.hgsubstate Fri Sep 08 19:37:36 2017 -0400 +++ b/.hgsubstate Sun Oct 08 15:31:00 2017 -0400 @@ -37,5 +37,5 @@ f6f2d6618a321f5b0065586a7bc934325fec81ab vim/bundle/targets 5d5c71044880443035e07009497962feacb56b20 vim/bundle/vimtex bf3fd7f67e730f93765bd3c1cfcdb18fd4043521 vim/bundle/vitality -d091242de628c09b3ff47e1c67e7cecff39adc44 vim/bundle/vlime +faff23c03a5a1f28d6ef5963e6a9caa46af7074a vim/bundle/vlime 6876fe38b33732cb124d415ffc4156f16da5e118 vim/bundle/windowswap diff -r 2b37786c6a97 -r 15bd14518a98 agignore --- a/agignore Fri Sep 08 19:37:36 2017 -0400 +++ b/agignore Sun Oct 08 15:31:00 2017 -0400 @@ -5,3 +5,4 @@ *.public.key *.private.key *.pgp +*.fasl diff -r 2b37786c6a97 -r 15bd14518a98 bin/arduino-serial --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/arduino-serial Sun Oct 08 15:31:00 2017 -0400 @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +rlwrap -I -m @ -M .lisp -a ___ -O '^[a-zA-Z0-9]+>' -p BLUE -q '"' python -m serial.tools.miniterm /dev/cu.usbmodem* 9600 --exit-char 38 --eol LF diff -r 2b37786c6a97 -r 15bd14518a98 ffignore --- a/ffignore Fri Sep 08 19:37:36 2017 -0400 +++ b/ffignore Sun Oct 08 15:31:00 2017 -0400 @@ -5,6 +5,7 @@ UnityTempFile syntax:regex -^tags$ -^tags.bak$ +\btags$ +\btags.bak$ \.meta$ +\.fasl$ diff -r 2b37786c6a97 -r 15bd14518a98 fish/config.fish --- a/fish/config.fish Fri Sep 08 19:37:36 2017 -0400 +++ b/fish/config.fish Sun Oct 08 15:31:00 2017 -0400 @@ -6,7 +6,7 @@ function eff; nvim ~/.config/fish/functions; end function eg; nvim ~/.gitconfig; end function eh; nvim ~/.hgrc; end -function ei; nvim ~/Dropbox/inventory.markdown; end +function ei; hg -R ~/src/inventory/ pull -u; and nvim ~/src/inventory/inventory.markdown; and hg -R ~/src/inventory/ ci -m 'Update inventory'; and hg -R ~/src/inventory/ push; end function ek; nvim ~/lib/dotfiles/keymando/keymandorc.rb; end function em; nvim ~/.mutt/muttrc; end function es; nvim ~/.slate; end @@ -98,6 +98,9 @@ set -g -x GPG_TTY (tty) +set -g -x FZF_DEFAULT_COMMAND 'ffind' +set -g -x FZF_CTRL_T_COMMAND "$FZF_DEFAULT_COMMAND" + function headed_java -d "Put Java into headed mode" echo "Changing _JAVA_OPTIONS" echo "from: $_JAVA_OPTIONS" diff -r 2b37786c6a97 -r 15bd14518a98 inputrc --- a/inputrc Fri Sep 08 19:37:36 2017 -0400 +++ b/inputrc Sun Oct 08 15:31:00 2017 -0400 @@ -4,3 +4,5 @@ set input-meta on set output-meta on set convert-meta off + +"\C-i": rlwrap-call-editor diff -r 2b37786c6a97 -r 15bd14518a98 vim/vimrc --- a/vim/vimrc Fri Sep 08 19:37:36 2017 -0400 +++ b/vim/vimrc Sun Oct 08 15:31:00 2017 -0400 @@ -822,6 +822,17 @@ " }}} " Filetype-specific ------------------------------------------------------- {{{ +" Arduino {{{ + +augroup ft_arduino + au! + + au FileType arduino setlocal noexpandtab shiftwidth=8 tabstop=8 softtabstop=8 + au FileType arduino inoremap A; + au FileType arduino inoremap mzA;`z +augroup END + +" }}} " Assembly {{{ augroup ft_asm @@ -1178,9 +1189,12 @@ au FileType lisp inoremap " lol, vim ignores indentexpr if lisp is set - au FileType lisp setlocal nolisp + " au FileType lisp setlocal nolisp au FileType lisp setlocal iskeyword+=!,?,%,- - " au FileType lisp setlocal equalprg=lispindent + + " stay with lispindent for now + au FileType lisp setlocal lisp + au FileType lisp setlocal equalprg=lispindent " scratch buffer au FileType lisp nnoremap :e scratch.lisp @@ -2073,6 +2087,7 @@ au FileType fish setlocal commentstring=#\ %s au FileType gnuplot setlocal commentstring=#\ %s au FileType cs setlocal commentstring=//\ %s + au FileType arduino setlocal commentstring=//\ %s au FileType pandabt setlocal commentstring=//\ %s augroup END @@ -2592,7 +2607,7 @@ \ "SPEED": 1 \ } -" let g:vlime_indent_keywords = {"foo": 2} +" let g:vlime_indent_keywords = {"defsystem": 1} function! CleanVlimeWindows() call vlime#plugin#CloseWindow("preview") diff -r 2b37786c6a97 -r 15bd14518a98 weechat/python/autoload/wee_slack.py --- a/weechat/python/autoload/wee_slack.py Fri Sep 08 19:37:36 2017 -0400 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,3551 +0,0 @@ -# -*- coding: utf-8 -*- - -from __future__ import unicode_literals - -from functools import wraps - -import time -import json -import pickle -import sha -import os -import re -import urllib -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 -except: - pass - -SCRIPT_NAME = "slack" -SCRIPT_AUTHOR = "Ryan Huber " -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 - -RECORD_DIR = "/tmp/weeslack-debug" - -SLACK_API_TRANSLATOR = { - "channel": { - "history": "channels.history", - "join": "channels.join", - "leave": "channels.leave", - "mark": "channels.mark", - "info": "channels.info", - }, - "im": { - "history": "im.history", - "join": "im.open", - "leave": "im.close", - "mark": "im.mark", - }, - "group": { - "history": "groups.history", - "join": "channels.join", - "leave": "groups.leave", - "mark": "groups.mark", - "info": "groups.info" - }, - "thread": { - "history": None, - "join": None, - "leave": None, - "mark": None, - } - - -} - -###### Decorators have to be up here - - -def slack_buffer_or_ignore(f): - """ - Only run this function if we're in a slack buffer, else ignore - """ - @wraps(f) - def wrapper(data, current_buffer, *args, **kwargs): - if current_buffer not in EVENTROUTER.weechat_controller.buffers: - return w.WEECHAT_RC_OK - return f(data, current_buffer, *args, **kwargs) - return wrapper - - -def slack_buffer_required(f): - """ - Only run this function if we're in a slack buffer, else print error - """ - @wraps(f) - def wrapper(data, current_buffer, *args, **kwargs): - if current_buffer not in EVENTROUTER.weechat_controller.buffers: - return w.WEECHAT_RC_ERROR - return f(data, current_buffer, *args, **kwargs) - return wrapper - - -NICK_GROUP_HERE = "0|Here" -NICK_GROUP_AWAY = "1|Away" - -sslopt_ca_certs = {} -if hasattr(ssl, "get_default_verify_paths") and callable(ssl.get_default_verify_paths): - ssl_defaults = ssl.get_default_verify_paths() - if ssl_defaults.cafile is not None: - sslopt_ca_certs = {'ca_certs': ssl_defaults.cafile} - -###### 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: - 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: - return decode_from_utf8(orig_attr) - - -##### Helpers - -def get_nick_color_name(nick): - info_name_prefix = "irc_" if int(weechat_version) < 0x1050000 else "" - return w.info_get(info_name_prefix + "nick_color_name", nick) - - -##### 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: - 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: - # 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(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: - 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: - 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: - 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 - 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_ERROR - - reaction = re.match("^\s*(\d*)(\+|-):(.*):\s*$", data) - substitute = re.match("^(\d*)s/", data) - if reaction: - if reaction.group(2) == "+": - channel.send_add_reaction(int(reaction.group(1) or 1), reaction.group(3)) - elif reaction.group(2) == "-": - channel.send_remove_reaction(int(reaction.group(1) or 1), reaction.group(3)) - elif substitute: - msgno = int(substitute.group(1) or 1) - try: - old, new, flags = re.split(r'(? ">channel" and - user presence via " name" <-> "+name". - """ - 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 and channel.type != "thread": - identifier = channel.identifier - request = {"type": "typing", "channel": identifier} - channel.team.send_to_websocket(request, expect_reply=False) - typing_timer = now - return w.WEECHAT_RC_OK - - -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 - """ - - 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.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.token or compare_str == self.domain or compare_str == self.subdomain: - return True - else: - return False - - def add_channel(self, channel): - self.channels[channel["id"]] = channel - channel.set_related_server(self) - - # def connect_request_generate(self): - # return SlackRequest(self.token, 'rtm.start', {}) - - # def 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): - 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') - w.buffer_set(self.channel_buffer, "localvar_set_nick", self.nick) - 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: - 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) - self.hook = 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): - w.unhook(self.hook) - 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() - - def update_member_presence(self, user, presence): - user.presence = presence - - for c in self.channels: - c = self.channels[c] - if user.id in c.members: - c.update_nicklist(user.id) - - -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() - self.unread_count_display = 0 - - 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 set_unread_count_display(self, count): - self.unread_count_display = count - 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") - - 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() - # 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') - w.buffer_set(self.channel_buffer, "localvar_set_channel", self.formatted_name()) - w.buffer_set(self.channel_buffer, "localvar_set_nick", self.team.nick) - w.buffer_set(self.channel_buffer, "short_name", self.formatted_name(style="sidebar", enable_color=True)) - self.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) - self.update_nicklist() - - if "info" in SLACK_API_TRANSLATOR[self.type]: - s = SlackRequest(self.team.token, SLACK_API_TRANSLATOR[self.type]["info"], {"channel": self.identifier}, team_hash=self.team.team_hash, channel_identifier=self.identifier) - self.eventrouter.receive(s) - - if self.type == "im": - if "join" in SLACK_API_TRANSLATOR[self.type]: - s = SlackRequest(self.team.token, SLACK_API_TRANSLATOR[self.type]["join"], {"user": self.user, "return_im": "true"}, team_hash=self.team.team_hash, channel_identifier=self.identifier) - self.eventrouter.receive(s) - - 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 - 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: - # 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) - 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_nth_previous_message(self, n, old, new, flags): - message = self.my_last_message(n) - 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: - 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, msgno): - 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: - msgno -= 1 - if msgno == 0: - 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: - 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) - - def send_remove_reaction(self, msg_number, reaction): - self.send_change_reaction("reactions.remove", msg_number, reaction) - - def send_change_reaction(self, method, msg_number, reaction): - if 0 < msg_number < len(self.messages): - timestamp = self.sorted_message_keys()[-msg_number] - data = {"channel": self.identifier, "timestamp": timestamp, "name": reaction} - 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): - if not self.channel_buffer: - return - if self.type not in ["channel", "group"]: - 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 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.. - nick_group = afk - if self.team.is_user_present(user.identifier): - nick_group = here - if user.identifier in self.members: - w.nicklist_add_nick(self.channel_buffer, nick_group, user.name, user.color_name, "", "", 1) - - # if we didn't get a user, build a complete list. this is expensive. - else: - if len(self.members) < 1000: - try: - for user in self.members: - user = self.team.users[user] - if user.deleted: - continue - nick_group = afk - if self.team.is_user_present(user.identifier): - nick_group = here - w.nicklist_add_nick(self.channel_buffer, nick_group, user.name, user.color_name, "", "", 1) - except Exception as e: - dbg("DEBUG: {} {} {}".format(self.identifier, self.name, e)) - else: - w.nicklist_remove_all(self.channel_buffer) - for fn in ["1| too", "2| many", "3| users", "4| to", "5| show"]: - w.nicklist_add_group(self.channel_buffer, '', fn, w.color('white'), 1) - - def 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_private_chats: - self.color_name = get_nick_color_name(self.name) - self.color = w.color(self.color_name) - else: - self.color = "" - self.color_name = "" - - 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 = "" - 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_nick", self.parent_message.team.nick) - 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 __repr__(self): - 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): - # This will automatically be none/"" if the user has disabled nick - # colourization. - self.color_name = get_nick_color_name(self.name) - self.color = w.color(self.color_name) - - def formatted_name(self, prepend="", enable_color=True): - if enable_color: - return self.color + prepend + self.name - else: - 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.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): - 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 self.message_json.get('bot_id') in self.team.bots: - name = "{} :]".format(self.team.bots[self.message_json["bot_id"]].formatted_name()) - name_plain = "{}".format(self.team.bots[self.message_json["bot_id"]].formatted_name(enable_color=False)) - 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): - m = self.message_json.get('reactions', None) - if m: - found = False - for r in m: - if r["name"] == reaction and user not in r["users"]: - r["users"].append(user) - found = True - if not found: - self.message_json["reactions"].append({"name": reaction, "users": [user]}) - else: - self.message_json["reactions"] = [{"name": reaction, "users": [user]}] - - def remove_reaction(self, reaction, user): - m = self.message_json.get('reactions', None) - if m: - for r in m: - if r["name"] == reaction and user in r["users"]: - r["users"].remove(user) - else: - pass - - -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 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): - """ - This handles the main entry call to slack, rtm.start - """ - 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: - 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_channelsinfo(channel_json, eventrouter, **kwargs): - request_metadata = pickle.loads(channel_json["wee_slack_request_metadata"]) - team = eventrouter.teams[request_metadata.team_hash] - channel = team.channels[request_metadata.channel_identifier] - unread_count_display = channel_json['channel']['unread_count_display'] - channel.set_unread_count_display(unread_count_display) - -def handle_groupsinfo(group_json, eventrouter, **kwargs): - request_metadata = pickle.loads(group_json["wee_slack_request_metadata"]) - team = eventrouter.teams[request_metadata.team_hash] - group = team.channels[request_metadata.channel_identifier] - unread_count_display = group_json['group']['unread_count_display'] - group_id = group_json['group']['id'] - group.set_unread_count_display(unread_count_display) - -def handle_imopen(im_json, eventrouter, **kwargs): - request_metadata = pickle.loads(im_json["wee_slack_request_metadata"]) - team = eventrouter.teams[request_metadata.team_hash] - im = team.channels[request_metadata.channel_identifier] - unread_count_display = im_json['channel']['unread_count_display'] - im.set_unread_count_display(unread_count_display) - -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: - 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): - if "user" in kwargs: - user = kwargs["user"] - team = kwargs["team"] - team.update_member_presence(user, 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_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: - 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: - 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 = unhtmlescape(unfurl_refs(message_json["text"], ignore_alt_text=False)) - channel.buffer_prnt(w.prefix("network").rstrip(), text, message_json["ts"], tagset="muted") - channel.render_topic(unhtmlescape(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: - 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: - 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": - 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)) - -###### New module/global methods - -def render_formatting(text): - text = re.sub(r'(^| )\*([^*]+)\*([^a-zA-Z0-9_]|$)', - r'\1{}\2{}\3'.format(w.color(config.render_bold_as), - w.color('-' + config.render_bold_as)), - text) - text = re.sub(r'(^| )_([^_]+)_([^a-zA-Z0-9_]|$)', - r'\1{}\2{}\3'.format(w.color(config.render_italic_as), - w.color('-' + config.render_italic_as)), - text) - return text - - -def render(message_json, team, channel, force=False): - # If we already have a rendered version in the object, just return that. - if not force and message_json.get("_rendered_text", ""): - return message_json["_rendered_text"] - else: - # server = servers.find(message_json["_server"]) - - if "fallback" in message_json: - text = message_json["fallback"] - elif "text" in message_json: - if message_json['text'] is not None: - text = message_json["text"] - else: - text = "" - 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 = unhtmlescape(text.replace("\t", " ")) - if message_json.get('mrkdwn', True): - text = render_formatting(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 IRC formatting chars with Slack formatting chars. - .replace('\x02', '*') - .replace('\x1D', '_') - .replace('\x1F', config.map_underline_to) - # Escape chars that have special meaning to Slack. Note that we do not - # (and should not) perform full HTML entity-encoding here. - # See https://api.slack.com/docs/message-formatting for details. - .replace('&', '&') - .replace('<', '<') - .replace('>', '>') - .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 unhtmlescape(text): - return text.replace("<", "<") \ - .replace(">", ">") \ - .replace("&", "&") - - -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): - count = 0 - if not isinstance(reactions, list): - reaction_string = " [{}]".format(reactions) - else: - reaction_string = ' [' - for r in reactions: - if len(r["users"]) > 0: - count += 1 - if config.show_reaction_nicks: - nicks = [resolve_ref("@{}".format(user)) for user in r["users"]] - users = "({})".format(",".join(nicks)) - else: - users = len(r["users"]) - reaction_string += ":{}:{} ".format(r["name"], users) - reaction_string = reaction_string[:-1] + ']' - if count == 0: - reaction_string = '' - return reaction_string - - -def modify_buffer_line(buffer, new_line, 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: - # get a pointer to the last line - line_pointer = w.hdata_pointer(w.hdata_get('lines'), own_lines, 'last_line') - # hold the structure of a line and of line data - struct_hdata_line = w.hdata_get('line') - struct_hdata_line_data = w.hdata_get('line_data') - - while line_pointer: - # get a pointer to the data in line_pointer via layout of struct_hdata_line - data = w.hdata_pointer(struct_hdata_line, line_pointer, 'data') - if data: - 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 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: - pass - # move backwards one line and try again - exit the while if you hit the end - line_pointer = w.hdata_move(struct_hdata_line, line_pointer, -1) - return w.WEECHAT_RC_OK - - -def modify_print_time(buffer, new_id, time): - """ - This overloads the time printed field to let us store the slack - per message unique id that comes after the "." in a slack ts - """ - - # 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 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] - if who == "*": - who = EVENTROUTER.weechat_controller.buffers[current_buffer].slack_name - else: - 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[who]] - 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: - 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: - 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 - - -@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) - 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): - global slack_debug - slack_debug = None - return w.WEECHAT_RC_OK - - -def create_slack_debug_buffer(): - global slack_debug, debug_string - if slack_debug is not None: - w.buffer_set(slack_debug, "display", "1") - else: - debug_string = None - slack_debug = w.buffer_new("slack-debug", "", "", "closed_slack_debug_buffer_cb", "") - w.buffer_set(slack_debug, "notify", "0") - - -def 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 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): - """ - 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: - if slack_debug and (not debug_string or debug_string in message): - # w.prnt(slack_debug, "---------") - w.prnt(slack_debug, message) - -###### Config code - -Setting = collections.namedtuple('Setting', ['default', 'desc']) - -class PluginConfig(object): - # Default settings. - # These are, initially, each a (default, desc) tuple; the former is the - # default value of the setting, in the (string) format that weechat - # expects, and the latter is the user-friendly description of the setting. - # At __init__ time these values are extracted, the description is used to - # set or update the setting description for use with /help, and the default - # value is used to set the default for any settings not already defined. - # Following this procedure, the keys remain the same, but the values are - # the real (python) values of the settings. - default_settings = { - 'background_load_all_history': Setting( - default='false', - desc='Load history for each channel in the background as soon as it' - ' opens, rather than waiting for the user to look at it.'), - 'channel_name_typing_indicator': Setting( - default='true', - desc='Change the prefix of a channel from # to > when someone is' - ' typing in it. Note that this will (temporarily) affect the sort' - ' order if you sort buffers by name rather than by number.'), - 'colorize_private_chats': Setting( - default='false', - desc='Whether to use nick-colors in DM windows.'), - 'debug_mode': Setting( - default='false', - desc='Open a dedicated buffer for debug messages and start logging' - ' to it. How verbose the logging is depends on log_level.'), - 'debug_level': Setting( - default='3', - desc='Show only this level of debug info (or higher) when' - ' debug_mode is on. Lower levels -> more messages.'), - 'distracting_channels': Setting( - default='', - desc='List of channels to hide.'), - 'map_underline_to': Setting( - default='_', - desc='When sending underlined text to slack, use this formatting' - ' character for it. The default ("_") sends it as italics. Use' - ' "*" to send bold instead.'), - 'never_away': Setting( - default='false', - desc='Poke Slack every five minutes so that it never marks you "away".'), - 'record_events': Setting( - default='false', - desc='Log all traffic from Slack to disk as JSON.'), - 'render_bold_as': Setting( - default='bold', - desc='When receiving bold text from Slack, render it as this in weechat.'), - 'render_italic_as': Setting( - default='italic', - desc='When receiving bold text from Slack, render it as this in weechat.' - ' If your terminal lacks italic support, consider using "underline" instead.'), - 'server_aliases': Setting( - default='', - desc='A comma separated list of `subdomain:alias` pairs. The alias' - ' will be used instead of the actual name of the slack (in buffer' - ' names, logging, etc). E.g `work:no_fun_allowed` would make your' - ' work slack show up as `no_fun_allowed` rather than `work.slack.com`.'), - 'short_buffer_names': Setting( - default='false', - desc='Use `foo.#channel` rather than `foo.slack.com.#channel` as the' - ' internal name for Slack buffers. Overrides server_aliases.'), - 'show_reaction_nicks': Setting( - default='false', - desc='Display the name of the reacting user(s) alongside each reactji.'), - 'slack_api_token': Setting( - default='INSERT VALID KEY HERE!', - desc='List of Slack API tokens, one per Slack instance you want to' - ' connect to. See the README for details on how to get these.'), - 'slack_timeout': Setting( - default='20000', - desc='How long (ms) to wait when communicating with Slack.'), - 'switch_buffer_on_join': Setting( - default='true', - desc='When /joining a channel, automatically switch to it as well.'), - 'thread_suffix_color': Setting( - default='lightcyan', - desc='Color to use for the [thread: XXX] suffix on messages that' - ' have threads attached to them.'), - 'unfurl_ignore_alt_text': Setting( - default='false', - desc='When displaying ("unfurling") links to channels/users/etc,' - ' ignore the "alt text" present in the message and instead use the' - ' canonical name of the thing being linked to.'), - 'unhide_buffers_with_activity': Setting( - default='false', - desc='When activity occurs on a buffer, unhide it even if it was' - ' previously hidden (whether by the user or by the' - ' distracting_channels setting).'), - } - - # Set missing settings to their defaults. Load non-missing settings from - # weechat configs. - def __init__(self): - self.settings = {} - # Set all descriptions, replace the values in the dict with the - # default setting value rather than the (setting,desc) tuple. - # Use items() rather than iteritems() so we don't need to worry about - # invalidating the iterator. - for key, (default, desc) in self.default_settings.items(): - w.config_set_desc_plugin(key, desc) - self.settings[key] = default - - # Migrate settings from old versions of Weeslack... - self.migrate() - # ...and then set anything left over from the defaults. - for key, default in self.settings.iteritems(): - if not w.config_get_plugin(key): - w.config_set_plugin(key, default) - self.config_changed(None, None, None) - - def __str__(self): - return "".join([x + "\t" + str(self.settings[x]) + "\n" for x in self.settings.keys()]) - - def config_changed(self, data, key, value): - for key in self.settings: - self.settings[key] = self.fetch_setting(key) - if self.debug_mode: - create_slack_debug_buffer() - return w.WEECHAT_RC_OK - - def fetch_setting(self, key): - if hasattr(self, 'get_' + key): - try: - return getattr(self, 'get_' + key)(key) - except: - return self.settings[key] - else: - # Most settings are on/off, so make get_boolean the default - return self.get_boolean(key) - - def __getattr__(self, key): - return self.settings[key] - - def get_boolean(self, key): - return w.config_string_to_boolean(w.config_get_plugin(key)) - - def get_string(self, key): - return w.config_get_plugin(key) - - def get_int(self, key): - return int(w.config_get_plugin(key)) - - get_debug_level = get_int - get_map_underline_to = get_string - get_render_bold_as = get_string - get_render_italic_as = get_string - get_slack_timeout = get_int - get_thread_suffix_color = get_string - - 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'): - return w.string_eval_expression(token, {}, {}, {}) - else: - return token - - 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", ""): - - weechat_version = w.info_get("version_number", "") or 0 - if int(weechat_version) < 0x1030000: - w.prnt("", "\nERROR: Weechat version 1.3+ is required to use {}.\n\n".format(SCRIPT_NAME)) - else: - - global EVENTROUTER - EVENTROUTER = EventRouter() - # 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 - - typing_timer = time.time() - # 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!@#$")) - - w.hook_config("plugins.var.python." + SCRIPT_NAME + ".*", "config_changed_cb", "") - - load_emoji() - setup_hooks() - - # attach to the weechat hooks we need - - 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