diff options
-rw-r--r-- | README.md | 23 | ||||
-rw-r--r-- | _pytest/conftest.py | 4 | ||||
-rw-r--r-- | _pytest/test_process_message.py | 5 | ||||
-rw-r--r-- | _pytest/test_unfurl.py | 42 | ||||
-rw-r--r-- | wee_slack.py | 211 |
5 files changed, 202 insertions, 83 deletions
@@ -12,7 +12,8 @@ A WeeChat native client for Slack.com. Provides supplemental features only avail Features -------- - * **New** Emoji reactions! + * **New** Upload to slack capabilities! + * Emoji reactions! * Edited messages work just like the official clients, where the original message changes and has (edited) appended. * Unfurled urls dont generate a new message, but replace the original with more info as it is received. * Regex style message editing (s/oldtext/newtext/) @@ -45,7 +46,6 @@ Dependencies ------------ * WeeChat 1.1+ http://weechat.org/ * websocket-client https://pypi.python.org/pypi/websocket-client/ - * curl http://curl.haxx.se/ Setup ------ @@ -65,19 +65,14 @@ wee-slack doesn't use the Slack IRC gateway. If you currently connect via the ga ####1. Install dependencies -##### OSX +##### OSX and Linux ``` pip install websocket-client ``` -##### Linux (ubuntu) -``` -sudo apt-get install curl -pip install websocket-client -``` ##### FreeBSD ``` -pkg install curl py27-websocket-client py27-six +pkg install py27-websocket-client py27-six ``` ####2. copy wee_slack.py to ~/.weechat/python/autoload @@ -170,11 +165,21 @@ Turn off colorized nicks: /set plugins.var.python.slack_extension.colorize_nicks 0 ``` +Set channel prefix to something other than my-slack-subdomain.slack.com (e.g. when using buffers.pl): +``` +/set plugins.var.python.slack_extension.server_alias.my-slack-subdomain "mysub" +``` + Set all read markers to a specific time: ``` /slack setallreadmarkers (time in epoch) ``` +Upload a file to the current slack buffer: +``` +/slack upload [file_path] +``` + Debug mode: ``` /slack debug diff --git a/_pytest/conftest.py b/_pytest/conftest.py index 0cbceb9..2f90977 100644 --- a/_pytest/conftest.py +++ b/_pytest/conftest.py @@ -1,4 +1,8 @@ import pytest +import sys + +sys.path.append(str(pytest.config.rootdir)) + from wee_slack import SlackServer from wee_slack import Channel from wee_slack import User diff --git a/_pytest/test_process_message.py b/_pytest/test_process_message.py index 6c58336..a8cf9a2 100644 --- a/_pytest/test_process_message.py +++ b/_pytest/test_process_message.py @@ -33,8 +33,3 @@ def test_process_message(slack_debug, monkeypatch, myservers, mychannels, myuser print called # assert called['buffer_prnt'] == 2 # assert called['buffer_prnt_changed'] == 1 - - - - - diff --git a/_pytest/test_unfurl.py b/_pytest/test_unfurl.py new file mode 100644 index 0000000..9af7cf2 --- /dev/null +++ b/_pytest/test_unfurl.py @@ -0,0 +1,42 @@ +import wee_slack +import pytest +import json + +slack = wee_slack + +unfurl_map = [ + { "input": "foo", + "output": "foo", + }, + { "input": "<@U2147483697|@othernick>: foo", + "output": "@testuser: foo", + "ignore_alt_text": True + }, + { "input": "foo <#C2147483705|#otherchannel> foo", + "output": "foo #otherchannel foo", + }, + { "input": "foo <#C2147483705> foo", + "output": "foo #testchan foo", + }, + { "input": "url: <https://example.com|example> suffix", + "output": "url: https://example.com (example) suffix", + }, + { "input": "url: <https://example.com|example with spaces> suffix", + "output": "url: https://example.com (example with spaces) suffix", + }, + ] + + +def test_unfurl_refs(myservers, mychannels, myusers): + slack.servers = myservers + slack.channels = mychannels + slack.users = myusers + slack.message_cache = {} + slack.servers[0].users = myusers + print mychannels[0].identifier + + for k in unfurl_map: + if "ignore_alt_text" in k: + assert slack.unfurl_refs(k["input"], ignore_alt_text=k["ignore_alt_text"]) == k["output"] + else: + assert slack.unfurl_refs(k["input"]) == k["output"] diff --git a/wee_slack.py b/wee_slack.py index a5b4c9e..6e1b465 100644 --- a/wee_slack.py +++ b/wee_slack.py @@ -4,6 +4,7 @@ from functools import wraps import time import json +import os import pickle import sha import re @@ -120,6 +121,7 @@ class SlackServer(object): self.nick = None self.name = None self.domain = None + self.server_buffer_name = None self.login_data = None self.buffer = None self.token = token @@ -201,6 +203,13 @@ class SlackServer(object): self.domain = login_data["team"]["domain"] + ".slack.com" dbg("connected to {}".format(self.domain)) self.identifier = self.domain + + alias = w.config_get_plugin("server_alias.{}".format(login_data["team"]["domain"])) + if alias: + self.server_buffer_name = alias + else: + self.server_buffer_name = self.domain + self.nick = login_data["self"]["name"] self.create_local_buffer() @@ -241,8 +250,8 @@ class SlackServer(object): self.buffer_prnt('{:<20} {}'.format("Team id", login_data["team"]["id"]), backlog=True) def create_local_buffer(self): - if not w.buffer_search("", self.domain): - self.buffer = w.buffer_new(self.domain, "buffer_input_cb", "", "", "") + if not w.buffer_search("", self.server_buffer_name): + self.buffer = w.buffer_new(self.server_buffer_name, "buffer_input_cb", "", "", "") w.buffer_set(self.buffer, "nicklist", "1") w.nicklist_add_group(self.buffer, '', NICK_GROUP_HERE, "weechat.color.nicklist_group", 1) @@ -287,6 +296,11 @@ class SlackServer(object): item["last_read"] = 0 name = self.users.find(item["user"]).name self.add_channel(DmChannel(self, name, item["id"], item["is_open"], item["last_read"])) + for item in data['self']['prefs']['muted_channels'].split(','): + if item == '': + continue + if self.channels.find(item) is not None: + self.channels.find(item).muted = True for item in self.channels: item.get_history() @@ -338,6 +352,7 @@ class Channel(object): self.last_received = None self.messages = [] self.scrolling = False + self.muted = False if active: self.create_buffer() self.attach_buffer() @@ -369,11 +384,11 @@ class Channel(object): self.members_table[user] = self.server.users.find(user) def create_buffer(self): - channel_buffer = w.buffer_search("", "{}.{}".format(self.server.domain, self.name)) + channel_buffer = w.buffer_search("", "{}.{}".format(self.server.server_buffer_name, self.name)) if channel_buffer: self.channel_buffer = channel_buffer else: - self.channel_buffer = w.buffer_new("{}.{}".format(self.server.domain, self.name), "buffer_input_cb", self.name, "", "") + self.channel_buffer = w.buffer_new("{}.{}".format(self.server.server_buffer_name, self.name), "buffer_input_cb", self.name, "", "") if self.type == "im": w.buffer_set(self.channel_buffer, "localvar_set_type", 'private') else: @@ -382,7 +397,7 @@ class Channel(object): buffer_list_update_next() def attach_buffer(self): - channel_buffer = w.buffer_search("", "{}.{}".format(self.server.domain, self.name)) + channel_buffer = w.buffer_search("", "{}.{}".format(self.server.server_buffer_name, self.name)) if channel_buffer != main_weechat_buffer: self.channel_buffer = channel_buffer w.buffer_set(self.channel_buffer, "localvar_set_nick", self.server.nick) @@ -417,7 +432,7 @@ class Channel(object): dbg("DEBUG: {} {} {}".format(self.identifier, self.name, e)) def fullname(self): - return "{}.{}".format(self.server.domain, self.name) + return "{}.{}".format(self.server.server_buffer_name, self.name) def has_user(self, name): return name in self.members @@ -531,17 +546,13 @@ class Channel(object): async_slack_api_request(self.server.domain, self.server.token, SLACK_API_TRANSLATOR[self.type]["mark"], {"channel": self.identifier, "ts": time}) def rename(self): - if current_domain_name() != self.server.domain and channels_not_on_current_server_color: - color = w.color(channels_not_on_current_server_color) - else: - color = "" if self.is_someone_typing(): new_name = ">{}".format(self.name[1:]) else: new_name = self.name if self.channel_buffer: - if w.buffer_get_string(self.channel_buffer, "short_name") != (color + new_name): - w.buffer_set(self.channel_buffer, "short_name", color + new_name) + if w.buffer_get_string(self.channel_buffer, "short_name") != new_name: + w.buffer_set(self.channel_buffer, "short_name", new_name) # deprecated in favor of redrawing the entire buffer # def buffer_prnt_changed(self, user, text, time, append=""): @@ -569,7 +580,9 @@ class Channel(object): tags = "notify_highlight" elif user != self.server.nick and self.name in self.server.users: tags = "notify_private,notify_message" - elif user in [w.prefix("join"), w.prefix("quit")]: + elif self.muted: + tags = "no_highlight,notify_none,logger_backlog_end" + elif user in [x.strip() for x in w.prefix("join"), w.prefix("quit")]: tags = "irc_smart_filter" else: tags = "notify_message" @@ -584,7 +597,8 @@ class Channel(object): name = name.decode('utf-8') #colorize nicks in each line chat_color = w.config_string(w.config_get('weechat.color.chat')) - message = message.decode('UTF-8', 'replace') + if type(message) is not unicode: + message = message.decode('UTF-8', 'replace') for user in self.server.users: if user.name in message: message = user.name_regex.sub( @@ -683,15 +697,12 @@ class DmChannel(Channel): self.type = "im" def rename(self): - if current_domain_name() != self.server.domain and channels_not_on_current_server_color: - force_color = w.color(channels_not_on_current_server_color) - else: - force_color = None + global colorize_private_chats if self.server.users.find(self.name).presence == "active": - new_name = self.server.users.find(self.name).formatted_name('+', force_color) + new_name = self.server.users.find(self.name).formatted_name('+', colorize_private_chats) else: - new_name = self.server.users.find(self.name).formatted_name(' ', force_color) + new_name = self.server.users.find(self.name).formatted_name(' ', colorize_private_chats) if self.channel_buffer: w.buffer_set(self.channel_buffer, "short_name", new_name) @@ -766,9 +777,9 @@ class User(object): self.color = "" self.color_name = "" - def formatted_name(self, prepend="", force_color=None): - if colorize_nicks: - print_color = force_color or self.color + def formatted_name(self, prepend="", enable_color=True): + if colorize_nicks and enable_color: + print_color = self.color else: print_color = "" return print_color + prepend + self.name @@ -902,6 +913,26 @@ def slack_buffer_required(f): @slack_buffer_required +def command_upload(current_buffer, args): + """ + Uploads a file to the current buffer + /slack upload [file_path] + """ + post_data = {} + channel = current_buffer_name(short=True) + domain = current_domain_name() + token = servers.find(domain).token + + if servers.find(domain).channels.find(channel): + channel_identifier = servers.find(domain).channels.find(channel).identifier + + if channel_identifier: + post_data["token"] = token + post_data["channels"] = channel_identifier + post_data["file"] = args + async_slack_api_upload_request(token, "files.upload", post_data) + +@slack_buffer_required def command_talk(current_buffer, args): """ Open a chat with the specified user @@ -1112,7 +1143,7 @@ def command_openweb(current_buffer, args): if trigger != "0": if args is None: channel = channels.find(current_buffer) - url = "{}/messages/{}".format(channel.server.domain, channel.name) + url = "{}/messages/{}".format(channel.server.server_buffer_name, channel.name) topic = w.buffer_get_string(channel.channel_buffer, "title") w.buffer_set(channel.channel_buffer, "title", "{}:{}".format(trigger, url)) w.hook_timer(1000, 0, 1, "command_openweb", json.dumps({"topic": topic, "buffer": current_buffer})) @@ -1169,6 +1200,19 @@ def process_pong(message_json): pass +def process_pref_change(message_json): + server = servers.find(message_json["myserver"]) + if message_json['name'] == u'muted_channels': + muted = message_json['value'].split(',') + for c in server.channels: + if c.identifier in muted: + c.muted = True + else: + c.muted = False + else: + dbg("Preference change not implemented: {}\n{}".format(message_json['name'])) + + def process_team_join(message_json): server = servers.find(message_json["myserver"]) item = message_json["user"] @@ -1470,43 +1514,60 @@ def unwrap_attachments(message_json): # attachment_text = attachment_text.encode('ascii', 'ignore') return attachment_text +def resolve_ref(ref): + if ref.startswith('@U'): + if users.find(ref[1:]): + try: + return "@{}".format(users.find(ref[1:]).name) + except: + dbg("NAME: {}".format(ref)) + elif ref.startswith('#C'): + if channels.find(ref[1:]): + try: + return "{}".format(channels.find(ref[1:]).name) + except: + dbg("CHANNEL: {}".format(ref)) + + # Something else, just return as-is + return ref + +def unfurl_ref(ref, ignore_alt_text=False): + id = ref.split('|')[0] + display_text = ref + if ref.find('|') > -1: + if ignore_alt_text: + display_text = resolve_ref(id) + else: + if id.startswith("#C") or id.startswith("@U"): + display_text = ref.split('|')[1] + else: + url, desc = ref.split('|', 1) + display_text = "{} ({})".format(url, desc) + else: + display_text = resolve_ref(ref) + return display_text def unfurl_refs(text, ignore_alt_text=False): """ Worst code ever written. this needs work """ if text and text.find('<') > -1: - newtext = [] - text = text.split(" ") - for item in text: - # dbg(item) - prefix = "" - suffix = "" - start = item.find('<') - end = item.find('>') - if start > -1 and end > -1: - prefix = item[:start] - suffix = item[end+1:] - item = item[start + 1:end] - if item.find('|') > -1: - if ignore_alt_text: - item = item.split('|')[1] - else: - item = item.split('|')[0] - if item.startswith('@U'): - if users.find(item[1:]): - try: - item = "@{}".format(users.find(item[1:]).name) - except: - dbg("NAME: {}".format(item)) - if item.startswith('#C'): - if channels.find(item[1:]): - item = "{}".format(channels.find(item[1:]).name) - newtext.append(prefix + item + suffix) - text = " ".join(newtext) - return text - else: - return text + end = 0 + newtext = "" + while text.find('<') > -1: + # Prepend prefix + newtext += text[:text.find('<')] + text = text[text.find('<'):] + end = text.find('>') + if end == -1: + newtext += text + break + # Format thingabob + newtext += unfurl_ref(text[1:end], ignore_alt_text) + text = text[end+1:] + newtext += text + return newtext + return text def get_user(message_json, server): @@ -1674,6 +1735,8 @@ def complete_next_cb(data, buffer, command): # If we're on a non-word, look left for something to complete while current_pos >= 0 and input[current_pos] != '@' and not input[current_pos].isalnum(): current_pos = current_pos - 1 + if current_pos < 0: + current_pos = 0 for l in range(current_pos, 0, -1): if input[l] != '@' and not input[l].isalnum(): word_start = l + 1 @@ -1695,12 +1758,21 @@ def complete_next_cb(data, buffer, command): # Slack specific requests -# NOTE: switched to async/curl because sync slowed down the UI +# NOTE: switched to async because sync slowed down the UI def async_slack_api_request(domain, token, request, post_data, priority=False): if not STOP_TALKING_TO_SLACK: post_data["token"] = token - url = 'https://{}/api/{}'.format(domain, request) - command = 'curl -A "wee_slack {}" -s --data "{}" {}'.format(SCRIPT_VERSION, urllib.urlencode(post_data), url) + url = 'url:https://{}/api/{}?{}'.format(domain, request, urllib.urlencode(post_data)) + context = pickle.dumps({"request": request, "token": token, "post_data": post_data}) + params = { 'useragent': 'wee_slack {}'.format(SCRIPT_VERSION) } + dbg("URL: {} context: {} params: {}".format(url, context, params)) + w.hook_process_hashtable(url, params, 20000, "url_processor_cb", context) + +def async_slack_api_upload_request(token, request, post_data, priority=False): + if not STOP_TALKING_TO_SLACK: + url = 'https://slack.com/api/{}'.format(request) + file_path = os.path.expanduser(post_data["file"]) + command = 'curl -F file=@{} -F channels={} -F token={} {}'.format(file_path, post_data["channels"], token, url) context = pickle.dumps({"request": request, "token": token, "post_data": post_data}) w.hook_process(command, 20000, "url_processor_cb", context) @@ -1718,8 +1790,8 @@ def url_processor_cb(data, command, return_code, out, err): try: my_json = json.loads(big_data[identifier]) except: - dbg("curl failed, doing again...") - dbg("curl length: {} identifier {}\n{}".format(len(big_data[identifier]), identifier, data)) + dbg("request failed, doing again...") + dbg("response length: {} identifier {}\n{}".format(len(big_data[identifier]), identifier, data)) my_json = False big_data.pop(identifier, None) @@ -1742,9 +1814,10 @@ def url_processor_cb(data, command, return_code, out, err): if "channel" in my_json: if "members" in my_json["channel"]: channels.find(my_json["channel"]["id"]).members = set(my_json["channel"]["members"]) - elif return_code != -1: - big_data.pop(identifier, None) - dbg("return code: {}".format(return_code)) + else: + if return_code != -1: + big_data.pop(identifier, None) + dbg("return code: {}, data: {}".format(return_code, data)) return w.WEECHAT_RC_OK @@ -1835,7 +1908,7 @@ def create_slack_debug_buffer(): def config_changed_cb(data, option, value): - global slack_api_token, distracting_channels, channels_not_on_current_server_color, colorize_nicks, slack_debug, debug_mode, \ + global slack_api_token, distracting_channels, colorize_nicks, colorize_private_chats, slack_debug, debug_mode, \ unfurl_ignore_alt_text slack_api_token = w.config_get_plugin("slack_api_token") @@ -1844,13 +1917,11 @@ def config_changed_cb(data, option, value): slack_api_token = w.string_eval_expression(slack_api_token, {}, {}, {}) distracting_channels = [x.strip() for x in w.config_get_plugin("distracting_channels").split(',')] - channels_not_on_current_server_color = w.config_get_plugin("channels_not_on_current_server_color") - if channels_not_on_current_server_color == "0": - channels_not_on_current_server_color = False colorize_nicks = w.config_get_plugin('colorize_nicks') == "1" debug_mode = w.config_get_plugin("debug_mode").lower() if debug_mode != '' and debug_mode != 'false': create_slack_debug_buffer() + colorize_private_chats = w.config_string_to_boolean(w.config_get_plugin("colorize_private_chats")) unfurl_ignore_alt_text = False if w.config_get_plugin('unfurl_ignore_alt_text') != "0": @@ -1901,12 +1972,12 @@ if __name__ == "__main__": w.config_set_plugin('slack_api_token', "INSERT VALID KEY HERE!") if not w.config_get_plugin('distracting_channels'): w.config_set_plugin('distracting_channels', "") - if not w.config_get_plugin('channels_not_on_current_server_color'): - w.config_set_plugin('channels_not_on_current_server_color', "0") if not w.config_get_plugin('debug_mode'): w.config_set_plugin('debug_mode', "") if not w.config_get_plugin('colorize_nicks'): w.config_set_plugin('colorize_nicks', "1") + if not w.config_get_plugin('colorize_private_chats'): + w.config_set_plugin('colorize_private_chats', "0") if not w.config_get_plugin('trigger_value'): w.config_set_plugin('trigger_value', "0") if not w.config_get_plugin('unfurl_ignore_alt_text'): @@ -1914,6 +1985,8 @@ if __name__ == "__main__": if not w.config_get_plugin('switch_buffer_on_join'): w.config_set_plugin('switch_buffer_on_join', "1") + w.config_option_unset('channels_not_on_current_server_color') + version = w.info_get("version_number", "") or 0 if int(version) >= 0x00040400: legacy_mode = False |