diff options
-rw-r--r-- | README.md | 24 | ||||
-rw-r--r-- | _pytest/conftest.py | 4 | ||||
-rw-r--r-- | _pytest/test_topic_command.py | 96 | ||||
-rw-r--r-- | _pytest/test_utf8_helpers.py | 72 | ||||
-rw-r--r-- | wee_slack.py | 466 |
5 files changed, 487 insertions, 175 deletions
@@ -34,7 +34,7 @@ Features * Away/back status handling * Expands/shows metadata for things like tweets/links * Displays edited messages (slack.com irc mode currently doesn't show these) - * *Super fun* debug mode. See what the websocket is saying with `/slack debug` + * *Super fun* debug mode. See what the websocket is saying In Development -------------- @@ -121,10 +121,10 @@ Join a channel: /join [channel] ``` -Start a direct chat with someone: +Start a direct chat with someone or multiple users: ``` -/query [username] -/slack talk [username] +/query <username>[,<username2>[,<username3>...]] +/slack talk <username>[,<username2>[,<username3>...]] ``` List channels: @@ -176,11 +176,6 @@ Add a reaction to the nth last message. The number can be omitted and defaults t 3+:smile: ``` -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] @@ -191,9 +186,10 @@ Run a Slack slash command. Simply prepend `/slack slash` to what you'd type in t /slack slash /desiredcommand arg1 arg2 arg3 ``` -Debug mode: +To send a command as a normal message instead of performing the action, prefix it with a slash or a space, like so: ``` -/slack debug +//slack + s/a/b/ ``` #### Threads @@ -249,6 +245,12 @@ Show channel name in hotlist after activity /set weechat.look.hotlist_names_level 14 ``` +Enable debug mode and change debug level (default 3, decrease to increase logging and vice versa): +``` +/set plugins.var.python.slack.debug_mode on +/set plugins.var.python.slack.debug_level 2 +``` + Support -------------- diff --git a/_pytest/conftest.py b/_pytest/conftest.py index 232814f..ca267fd 100644 --- a/_pytest/conftest.py +++ b/_pytest/conftest.py @@ -49,7 +49,9 @@ class FakeWeechat(): this is the thing that acts as "w." everywhere.. basically mock out all of the weechat calls here i guess """ - WEECHAT_RC_OK = True + WEECHAT_RC_ERROR = 0 + WEECHAT_RC_OK = 1 + WEECHAT_RC_OK_EAT = 2 def __init__(self): pass diff --git a/_pytest/test_topic_command.py b/_pytest/test_topic_command.py new file mode 100644 index 0000000..9d9a35e --- /dev/null +++ b/_pytest/test_topic_command.py @@ -0,0 +1,96 @@ +import wee_slack +from wee_slack import parse_topic_command, topic_command_cb +from mock import patch + + +def test_parse_topic_without_arguments(): + channel_name, topic = parse_topic_command('/topic') + + assert channel_name is None + assert topic is None + + +def test_parse_topic_with_text(): + channel_name, topic = parse_topic_command('/topic some topic text') + + assert channel_name is None + assert topic == 'some topic text' + + +def test_parse_topic_with_delete(): + channel_name, topic = parse_topic_command('/topic -delete') + + assert channel_name is None + assert topic == '' + + +def test_parse_topic_with_channel(): + channel_name, topic = parse_topic_command('/topic #general') + + assert channel_name == 'general' + assert topic is None + + +def test_parse_topic_with_channel_and_text(): + channel_name, topic = parse_topic_command( + '/topic #general some topic text') + + assert channel_name == 'general' + assert topic == 'some topic text' + + +def test_parse_topic_with_channel_and_delete(): + channel_name, topic = parse_topic_command('/topic #general -delete') + + assert channel_name == 'general' + assert topic == '' + + +def test_call_topic_without_arguments(realish_eventrouter): + team = realish_eventrouter.teams.values()[-1] + channel = team.channels.values()[-1] + current_buffer = channel.channel_buffer + wee_slack.EVENTROUTER = realish_eventrouter + + command = '/topic' + + with patch('wee_slack.w.prnt') as fake_prnt: + result = topic_command_cb(None, current_buffer, command) + fake_prnt.assert_called_with( + channel.channel_buffer, + 'Topic for {} is "{}"'.format(channel.name, channel.topic), + ) + assert result == wee_slack.w.WEECHAT_RC_OK_EAT + + +def test_call_topic_with_unknown_channel(realish_eventrouter): + team = realish_eventrouter.teams.values()[-1] + channel = team.channels.values()[-1] + current_buffer = channel.channel_buffer + wee_slack.EVENTROUTER = realish_eventrouter + + command = '/topic #nonexisting' + + with patch('wee_slack.w.prnt') as fake_prnt: + result = topic_command_cb(None, current_buffer, command) + fake_prnt.assert_called_with( + team.channel_buffer, + "#nonexisting: No such channel", + ) + assert result == wee_slack.w.WEECHAT_RC_OK_EAT + + +def test_call_topic_with_channel_and_string(realish_eventrouter): + team = realish_eventrouter.teams.values()[-1] + channel = team.channels.values()[-1] + current_buffer = channel.channel_buffer + wee_slack.EVENTROUTER = realish_eventrouter + + command = '/topic #general new topic' + + result = topic_command_cb(None, current_buffer, command) + request = realish_eventrouter.queue[-1] + assert request.request == 'channels.setTopic' + assert request.post_data == { + 'channel': 'C407ABS94', 'token': 'xoxoxoxox', 'topic': 'new topic'} + assert result == wee_slack.w.WEECHAT_RC_OK_EAT diff --git a/_pytest/test_utf8_helpers.py b/_pytest/test_utf8_helpers.py new file mode 100644 index 0000000..33c66ce --- /dev/null +++ b/_pytest/test_utf8_helpers.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- + +from collections import OrderedDict +from wee_slack import decode_from_utf8, encode_to_utf8, utf8_decode + + +def test_decode_preserves_string_without_utf8(): + assert u'test' == decode_from_utf8(b'test') + +def test_decode_preserves_unicode_strings(): + assert u'æøå' == decode_from_utf8(u'æøå') + +def test_decode_preserves_mapping_type(): + value_dict = {'a': 'x', 'b': 'y', 'c': 'z'} + value_ord_dict = OrderedDict(value_dict) + assert type(value_dict) == type(decode_from_utf8(value_dict)) + assert type(value_ord_dict) == type(decode_from_utf8(value_ord_dict)) + +def test_decode_preserves_iterable_type(): + value_set = {'a', 'b', 'c'} + value_tuple = ('a', 'b', 'c') + assert type(value_set) == type(decode_from_utf8(value_set)) + assert type(value_tuple) == type(decode_from_utf8(value_tuple)) + +def test_decodes_utf8_string_to_unicode(): + assert u'æøå' == decode_from_utf8(b'æøå') + +def test_decodes_utf8_dict_to_unicode(): + assert {u'æ': u'å', u'ø': u'å'} == decode_from_utf8({b'æ': b'å', b'ø': b'å'}) + +def test_decodes_utf8_list_to_unicode(): + assert [u'æ', u'ø', u'å'] == decode_from_utf8([b'æ', b'ø', b'å']) + +def test_encode_preserves_string_without_utf8(): + assert b'test' == encode_to_utf8(u'test') + +def test_encode_preserves_byte_strings(): + assert b'æøå' == encode_to_utf8(b'æøå') + +def test_encode_preserves_mapping_type(): + value_dict = {'a': 'x', 'b': 'y', 'c': 'z'} + value_ord_dict = OrderedDict(value_dict) + assert type(value_dict) == type(encode_to_utf8(value_dict)) + assert type(value_ord_dict) == type(encode_to_utf8(value_ord_dict)) + +def test_encode_preserves_iterable_type(): + value_set = {'a', 'b', 'c'} + value_tuple = ('a', 'b', 'c') + assert type(value_set) == type(encode_to_utf8(value_set)) + assert type(value_tuple) == type(encode_to_utf8(value_tuple)) + +def test_encodes_utf8_string_to_unicode(): + assert b'æøå' == encode_to_utf8(u'æøå') + +def test_encodes_utf8_dict_to_unicode(): + assert {b'æ': b'å', b'ø': b'å'} == encode_to_utf8({u'æ': u'å', u'ø': u'å'}) + +def test_encodes_utf8_list_to_unicode(): + assert [b'æ', b'ø', b'å'] == encode_to_utf8([u'æ', u'ø', u'å']) + +@utf8_decode +def method_with_utf8_decode(*args, **kwargs): + return (args, kwargs) + +def test_utf8_decode(): + args = (b'æ', b'ø', b'å') + kwargs = {b'æ': b'å', b'ø': b'å'} + + result_args, result_kwargs = method_with_utf8_decode(*args, **kwargs) + + assert result_args == decode_from_utf8(args) + assert result_kwargs == decode_from_utf8(kwargs) diff --git a/wee_slack.py b/wee_slack.py index 9e9202c..8854e18 100644 --- a/wee_slack.py +++ b/wee_slack.py @@ -40,21 +40,28 @@ RECORD_DIR = "/tmp/weeslack-debug" SLACK_API_TRANSLATOR = { "channel": { "history": "channels.history", - "join": "channels.join", - "leave": "channels.leave", + "join": "conversations.join", + "leave": "conversations.leave", "mark": "channels.mark", "info": "channels.info", }, "im": { "history": "im.history", - "join": "im.open", - "leave": "im.close", + "join": "conversations.open", + "leave": "conversations.close", "mark": "im.mark", }, + "mpim": { + "history": "mpim.history", + "join": "mpim.open", # conversations.open lacks unread_count_display + "leave": "conversations.close", + "mark": "mpim.mark", + "info": "groups.info", + }, "group": { "history": "groups.history", - "join": "channels.join", - "leave": "groups.leave", + "join": "conversations.join", + "leave": "conversations.leave", "mark": "groups.mark", "info": "groups.info" }, @@ -95,6 +102,17 @@ def slack_buffer_required(f): return wrapper +def utf8_decode(f): + """ + Decode all arguments from byte strings to unicode strings. Use this for + functions called from outside of this script, e.g. callbacks from weechat. + """ + @wraps(f) + def wrapper(*args, **kwargs): + return f(*decode_from_utf8(args), **decode_from_utf8(kwargs)) + return wrapper + + NICK_GROUP_HERE = "0|Here" NICK_GROUP_AWAY = "1|Away" @@ -113,7 +131,7 @@ def encode_to_utf8(data): if isinstance(data, bytes): return data elif isinstance(data, collections.Mapping): - return dict(map(encode_to_utf8, data.iteritems())) + return type(data)(map(encode_to_utf8, data.iteritems())) elif isinstance(data, collections.Iterable): return type(data)(map(encode_to_utf8, data)) else: @@ -126,7 +144,7 @@ def decode_from_utf8(data): if isinstance(data, unicode): return data elif isinstance(data, collections.Mapping): - return dict(map(decode_from_utf8, data.iteritems())) + return type(data)(map(decode_from_utf8, data.iteritems())) elif isinstance(data, collections.Iterable): return type(data)(map(decode_from_utf8, data)) else: @@ -137,19 +155,31 @@ class WeechatWrapper(object): def __init__(self, wrapped_class): self.wrapped_class = wrapped_class + # Helper method used to encode/decode method calls. + def wrap_for_utf8(self, method): + def hooked(*args, **kwargs): + result = method(*encode_to_utf8(args), **encode_to_utf8(kwargs)) + # Prevent wrapped_class from becoming unwrapped + if result == self.wrapped_class: + return self + return decode_from_utf8(result) + return hooked + + # Encode and decode everything sent to/received from weechat. We use the + # unicode type internally in wee-slack, but has to send utf8 to weechat. def __getattr__(self, attr): orig_attr = self.wrapped_class.__getattribute__(attr) if callable(orig_attr): - 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 + return self.wrap_for_utf8(orig_attr) else: return decode_from_utf8(orig_attr) + # Ensure all lines sent to weechat specifies a prefix. For lines after the + # first, we want to disable the prefix, which is done by specifying a space. + def prnt_date_tags(self, buffer, date, tags, message): + message = message.replace("\n", "\n \t") + return self.wrap_for_utf8(self.wrapped_class.prnt_date_tags)(buffer, date, tags, message) + ##### Helpers @@ -559,17 +589,18 @@ def local_process_async_slack_api_request(request, event_router): ###### New Callbacks +@utf8_decode 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 +@utf8_decode def receive_ws_callback(*args): """ complete @@ -581,11 +612,13 @@ def receive_ws_callback(*args): return w.WEECHAT_RC_OK +@utf8_decode def reconnect_callback(*args): EVENTROUTER.reconnect_if_disconnected() return w.WEECHAT_RC_OK +@utf8_decode def buffer_closing_callback(signal, sig_type, data): """ complete @@ -594,11 +627,11 @@ def buffer_closing_callback(signal, sig_type, data): 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 +@utf8_decode def buffer_input_callback(signal, buffer_ptr, data): """ incomplete @@ -606,13 +639,12 @@ def buffer_input_callback(signal, buffer_ptr, data): 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) + reaction = re.match("^(\d*)(\+|-):(.*):\s*$", data) substitute = re.match("^(\d*)s/", data) if reaction: if reaction.group(2) == "+": @@ -632,11 +664,27 @@ def buffer_input_callback(signal, buffer_ptr, data): old = old.replace(r'\/', '/') channel.edit_nth_previous_message(msgno, old, new, flags) else: + if data.startswith(('//', ' ')): + data = data[1:] channel.send_message(data) # this is probably wrong channel.mark_read(update_remote=True, force=True) return w.WEECHAT_RC_OK +# Workaround for supporting multiline messages. It intercepts before the input +# callback is called, as this is called with the whole message, while it is +# normally split on newline before being sent to buffer_input_callback +def input_text_for_buffer_cb(data, modifier, current_buffer, string): + if current_buffer not in EVENTROUTER.weechat_controller.buffers: + return string + message = decode_from_utf8(string) + if not message.startswith("/") and "\n" in message: + buffer_input_callback("EVENTROUTER", current_buffer, message) + return "" + return string + + +@utf8_decode def buffer_switch_callback(signal, sig_type, data): """ incomplete @@ -644,7 +692,6 @@ def buffer_switch_callback(signal, sig_type, data): 1) set read marker 2) determine if we have already populated channel history data """ - data = decode_from_utf8(data) eventrouter = eval(signal) prev_buffer_ptr = eventrouter.weechat_controller.get_previous_buffer_ptr() @@ -662,6 +709,7 @@ def buffer_switch_callback(signal, sig_type, data): return w.WEECHAT_RC_OK +@utf8_decode def buffer_list_update_callback(data, somecount): """ incomplete @@ -671,7 +719,6 @@ def buffer_list_update_callback(data, somecount): to indicate typing via "#channel" <-> ">channel" and user presence via " name" <-> "+name". """ - data = decode_from_utf8(data) eventrouter = eval(data) # global buffer_list_update @@ -690,8 +737,8 @@ def quit_notification_callback(signal, sig_type, data): stop_talking_to_slack() +@utf8_decode 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 @@ -707,14 +754,14 @@ def typing_notification_cb(signal, sig_type, data): return w.WEECHAT_RC_OK +@utf8_decode def typing_update_cb(data, remaining_calls): - data = decode_from_utf8(data) w.bar_item_update("slack_typing_notice") return w.WEECHAT_RC_OK +@utf8_decode 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'] @@ -724,6 +771,7 @@ def slack_never_away_cb(data, remaining_calls): return w.WEECHAT_RC_OK +@utf8_decode def typing_bar_item_cb(data, current_buffer, args): """ Privides a bar item indicating who is typing in the current channel AND @@ -758,13 +806,12 @@ def typing_bar_item_cb(data, current_buffer, args): return typing +@utf8_decode 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) @@ -777,13 +824,12 @@ def nick_completion_cb(data, completion_item, current_buffer, completion): return w.WEECHAT_RC_OK +@utf8_decode 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) @@ -794,6 +840,7 @@ def emoji_completion_cb(data, completion_item, current_buffer, completion): return w.WEECHAT_RC_OK +@utf8_decode def complete_next_cb(data, current_buffer, command): """Extract current word, if it is equal to a nick, prefix it with @ and rely on nick_completion_cb adding the @-prefixed versions to the @@ -802,8 +849,6 @@ def complete_next_cb(data, current_buffer, command): """ - 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) @@ -978,9 +1023,9 @@ class SlackTeam(object): self.eventrouter.weechat_controller.register_buffer(self.channel_buffer, self) w.buffer_set(self.channel_buffer, "localvar_set_type", 'server') w.buffer_set(self.channel_buffer, "localvar_set_nick", self.nick) + w.buffer_set(self.channel_buffer, "localvar_set_server", self.preferred_name) 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(',')} @@ -995,7 +1040,7 @@ class SlackTeam(object): return self.domain def buffer_prnt(self, data): - w.prnt_date_tags(self.channel_buffer, SlackTS().major, tag("backlog"), data) + w.prnt_date_tags(self.channel_buffer, SlackTS().major, tag("team"), data) def get_channel_map(self): return {v.slack_name: k for k, v in self.channels.iteritems()} @@ -1026,7 +1071,7 @@ class SlackTeam(object): else: return False - def mark_read(self): + def mark_read(self, ts=None, update_remote=True, force=False): pass def connect(self): @@ -1042,7 +1087,7 @@ class SlackTeam(object): self.set_connected() self.connecting = False except Exception as e: - dbg("websocket connection error: {}".format(e)) + dbg("websocket connection error: {}".format(decode_from_utf8(e))) self.connecting = False return False else: @@ -1107,8 +1152,8 @@ class SlackChannel(object): 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.topic = kwargs.get("topic", {}).get("value", "") self.identifier = kwargs["id"] self.last_read = SlackTS(kwargs.get("last_read", SlackTS())) self.channel_buffer = None @@ -1149,8 +1194,12 @@ class SlackChannel(object): return True return False + def get_members(self): + return self.members + def set_unread_count_display(self, count): self.unread_count_display = count + self.new_messages = bool(self.unread_count_display) for c in range(self.unread_count_display): if self.type == "im": w.buffer_set(self.channel_buffer, "hotlist", "2") @@ -1158,11 +1207,10 @@ class SlackChannel(object): 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 = ">" + if typing and config.channel_name_typing_indicator: + prepend = ">" + elif self.type == "group": + prepend = config.group_name_prefix else: prepend = "#" select = { @@ -1174,15 +1222,18 @@ class SlackChannel(object): } return select[style] - def render_topic(self, topic=None): + def render_topic(self): if self.channel_buffer: - if not topic: - if self.slack_topic['value'] != "": - topic = self.slack_topic['value'] - else: - topic = self.slack_purpose['value'] + if self.topic != "": + topic = self.topic + else: + topic = self.slack_purpose['value'] w.buffer_set(self.channel_buffer, "title", topic) + def set_topic(self, value): + self.topic = value + self.render_topic() + def update_from_message_json(self, message_json): for key, value in message_json.items(): setattr(self, key, value) @@ -1190,7 +1241,7 @@ class SlackChannel(object): 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) + s = SlackRequest(self.team.token, SLACK_API_TRANSLATOR[self.type]["join"], {"channel": self.identifier}, team_hash=self.team.team_hash, channel_identifier=self.identifier) self.eventrouter.receive(s) self.create_buffer() self.active = True @@ -1198,22 +1249,20 @@ class SlackChannel(object): # self.create_buffer() def check_should_open(self, force=False): - try: - if self.is_archived: - return - except: - pass + if hasattr(self, "is_archived") and self.is_archived: + return + 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 + return + + # Only check is_member if is_open is not set, because in some cases + # (e.g. group DMs), is_member should be ignored in favor of is_open. + is_open = self.is_open if hasattr(self, "is_open") else self.is_member + if is_open or self.unread_count_display: + self.create_buffer() + if config.background_load_all_history: + self.get_history(slow_queue=True) def set_related_server(self, team): self.team = team @@ -1221,7 +1270,7 @@ class SlackChannel(object): 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"}) + highlights = self.team.highlight_words.union({'@' + self.team.nick, self.team.myidentifier, "!here", "!channel", "!everyone"}) h_str = ",".join(highlights) w.buffer_set(self.channel_buffer, "highlight_words", h_str) @@ -1258,7 +1307,7 @@ class SlackChannel(object): 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) + s = SlackRequest(self.team.token, SLACK_API_TRANSLATOR[self.type]["join"], {"users": 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): @@ -1468,7 +1517,7 @@ class SlackChannel(object): def update_nicklist(self, user=None): if not self.channel_buffer: return - if self.type not in ["channel", "group"]: + if self.type not in ["channel", "group", "mpim"]: return w.buffer_set(self.channel_buffer, "nicklist", "1") # create nicklists for the current channel if they don't exist @@ -1505,7 +1554,7 @@ class SlackChannel(object): 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)) + dbg("DEBUG: {} {} {}".format(self.identifier, self.name, decode_from_utf8(e))) else: w.nicklist_remove_all(self.channel_buffer) for fn in ["1| too", "2| many", "3| users", "4| to", "5| show"]: @@ -1556,6 +1605,9 @@ class SlackDMChannel(SlackChannel): def set_name(self, slack_name): self.name = slack_name + def get_members(self): + return {self.user} + def create_buffer(self): if not self.channel_buffer: super(SlackDMChannel, self).create_buffer() @@ -1596,7 +1648,7 @@ class SlackDMChannel(SlackChannel): 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) + s = SlackRequest(self.team.token, SLACK_API_TRANSLATOR[self.type]["join"], {"users": self.user, "return_im": True}, team_hash=self.team.team_hash, channel_identifier=self.identifier) self.eventrouter.receive(s) self.create_buffer() @@ -1620,12 +1672,11 @@ class SlackGroupChannel(SlackChannel): 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 + self.name = config.group_name_prefix + slack_name # def formatted_name(self, prepend="#", enable_color=True, basic=False): # return prepend + self.slack_name @@ -1641,29 +1692,33 @@ class SlackMPDMChannel(SlackChannel): super(SlackMPDMChannel, self).__init__(eventrouter, **kwargs) n = kwargs.get('name') self.set_name(n) - self.type = "group" + self.type = "mpim" - def open(self, update_remote=False): + 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) + 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 update_remote and 'join' in SLACK_API_TRANSLATOR[self.type]: + s = SlackRequest(self.team.token, SLACK_API_TRANSLATOR[self.type]['join'], {'users': ','.join(self.members)}, team_hash=self.team.team_hash, channel_identifier=self.identifier) self.eventrouter.receive(s) # self.create_buffer() + @staticmethod + def adjust_name(n): + return "|".join("-".join(n.split("-")[1:-1]).split("--")) + def set_name(self, n): - self.name = "|".join("-".join(n.split("-")[1:-1]).split("--")) + self.name = self.adjust_name(n) 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 = ">" + adjusted_name = self.adjust_name(self.slack_name) + if typing and config.channel_name_typing_indicator: + prepend = ">" else: - prepend = "#" + prepend = "@" select = { "default": adjusted_name, "sidebar": prepend + adjusted_name, @@ -2108,12 +2163,20 @@ def handle_groupsinfo(group_json, eventrouter, **kwargs): 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_conversationsopen(conversation_json, eventrouter, object_name='channel', **kwargs): + request_metadata = pickle.loads(conversation_json["wee_slack_request_metadata"]) + # Set unread count if the channel isn't new (channel_identifier exists) + if hasattr(request_metadata, 'channel_identifier'): + channel_id = request_metadata.channel_identifier + team = eventrouter.teams[request_metadata.team_hash] + conversation = team.channels[channel_id] + unread_count_display = conversation_json[object_name]['unread_count_display'] + conversation.set_unread_count_display(unread_count_display) + + +def handle_mpimopen(mpim_json, eventrouter, object_name='group', **kwargs): + handle_conversationsopen(mpim_json, eventrouter, object_name, **kwargs) + def handle_groupshistory(message_json, eventrouter, **kwargs): handle_history(message_json, eventrouter, **kwargs) @@ -2127,6 +2190,10 @@ def handle_imhistory(message_json, eventrouter, **kwargs): handle_history(message_json, eventrouter, **kwargs) +def handle_mpimhistory(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] @@ -2335,7 +2402,7 @@ def subprocess_message_deleted(message_json, eventrouter, channel, team): 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"])) + channel.set_topic(unhtmlescape(message_json["topic"])) def process_reply(message_json, eventrouter, **kwargs): @@ -2788,20 +2855,20 @@ def tag(tagset, user=None): else: default_tag = 'nick_unknown' tagsets = { + # messages in the team/server buffer, e.g. "new channel created" + "team": "irc_notice,notify_private,log3", # when replaying something old - "backlog": "no_highlight,notify_none,logger_backlog_end", + "backlog": "irc_privmsg,no_highlight,notify_none,logger_backlog", # 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", + "muted": "irc_privmsg,no_highlight,notify_none,log1", # when receiving a direct message - "dm": "notify_private,notify_message,log1,irc_privmsg", - "dmfromme": "notify_none,log1,irc_privmsg", + "dm": "irc_privmsg,notify_private,log1", + "dmfromme": "irc_privmsg,no_highlight,notify_none,log1", # 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", + "joinleave": "irc_smart_filter,no_highlight,log4", # catchall ? - "default": "notify_message,log1", + "default": "irc_privmsg,notify_message,log1", } return default_tag + "," + tagsets[tagset] @@ -2809,9 +2876,8 @@ def tag(tagset, user=None): @slack_buffer_or_ignore +@utf8_decode 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: @@ -2826,64 +2892,67 @@ def part_command_cb(data, current_buffer, args): 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 +def parse_topic_command(command): + args = command.split()[1:] + channel_name = None + topic = None + + if args: + if args[0].startswith('#'): + channel_name = args[0][1:] + topic = args[1:] + else: + topic = args + if topic == []: + topic = None + if topic: + topic = ' '.join(topic) + if topic == '-delete': + topic = '' -@slack_buffer_required -def command_topic(data, current_buffer, args): + return channel_name, topic + + +@slack_buffer_or_ignore +@utf8_decode +def topic_command_cb(data, current_buffer, command): """ Change the topic of a channel - /slack topic [<channel>] [<topic>|-delete] + /topic [<channel>] [<topic>|-delete] """ - data = decode_from_utf8(data) - args = decode_from_utf8(args) - e = EVENTROUTER - team = e.weechat_controller.buffers[current_buffer].team - # server = servers.find(current_domain_name()) - args = args.split(' ') - if len(args) > 2 and args[1].startswith('#'): - cmap = team.get_channel_map() - channel_name = args[1][1:] - channel = team.channels[cmap[channel_name]] - topic = " ".join(args[2:]) + + channel_name, topic = parse_topic_command(command) + + team = EVENTROUTER.weechat_controller.buffers[current_buffer].team + if channel_name: + channel = team.channels.get(team.get_channel_map().get(channel_name)) else: - channel = e.weechat_controller.buffers[current_buffer] - topic = " ".join(args[1:]) + channel = EVENTROUTER.weechat_controller.buffers[current_buffer] - 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) + if not channel: + w.prnt(team.channel_buffer, "#{}: No such channel".format(channel_name)) return w.WEECHAT_RC_OK_EAT + + if topic is None: + w.prnt(channel.channel_buffer, 'Topic for {} is "{}"'.format(channel.name, channel.topic)) else: - return w.WEECHAT_RC_ERROR_EAT + 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 @slack_buffer_or_ignore +@utf8_decode 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 +@utf8_decode 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] @@ -2902,32 +2971,80 @@ def msg_command_cb(data, current_buffer, args): return w.WEECHAT_RC_OK_EAT +@slack_buffer_required +@utf8_decode +def command_channels(data, current_buffer, args): + e = EVENTROUTER + team = e.weechat_controller.buffers[current_buffer].team + + team.buffer_prnt("Channels:") + for channel in team.get_channel_map(): + team.buffer_prnt(" {}".format(channel)) + return w.WEECHAT_RC_OK_EAT + + +@slack_buffer_required +@utf8_decode +def command_users(data, current_buffer, args): + e = EVENTROUTER + team = e.weechat_controller.buffers[current_buffer].team + + team.buffer_prnt("Users:") + for user in team.users.values(): + team.buffer_prnt(" {:<25}({})".format(user.name, user.presence)) + return w.WEECHAT_RC_OK_EAT + + @slack_buffer_or_ignore +@utf8_decode def command_talk(data, current_buffer, args): """ - Open a chat with the specified user - /slack talk [user] + Open a chat with the specified user(s) + /slack talk <user>[,<user2>[,<user3>...]] """ - 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]] + + # Try finding the channel by name + chan = team.channels.get(team.get_channel_map().get(channel_name)) + + # If the channel doesn't exist, try finding a DM or MPDM instead + if not chan: + # Get the IDs of the users + u = team.get_username_map() + users = set() + for user in channel_name.split(','): + if user.startswith('@'): + user = user[1:] + if user in u: + users.add(u[user]) + + if users: + if len(users) > 1: + channel_type = 'mpim' + # Add the current user since MPDMs include them as a member + users.add(team.myidentifier) + else: + channel_type = 'im' + + # Try finding the channel by type and members + for channel in team.channels.itervalues(): + if (channel.type == channel_type and + channel.get_members() == users): + chan = channel + break + + # If the DM or MPDM doesn't exist, create it + if not chan: + s = SlackRequest(team.token, SLACK_API_TRANSLATOR[channel_type]['join'], {'users': ','.join(users)}, team_hash=team.team_hash) + EVENTROUTER.receive(s) + + if chan: chan.open() if config.switch_buffer_on_join: w.buffer_set(chan.channel_buffer, "display", "1") @@ -2940,9 +3057,8 @@ def command_showmuted(data, current_buffer, args): w.prnt(EVENTROUTER.weechat_controller.buffers[current].team.channel_buffer, str(EVENTROUTER.weechat_controller.buffers[current].team.muted_channels)) +@utf8_decode 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: @@ -2957,6 +3073,8 @@ def thread_command_callback(data, current_buffer, args): pm.thread_channel = tc tc.open() # tc.create_buffer() + if config.switch_buffer_on_join: + w.buffer_set(tc.channel_buffer, "display", "1") return w.WEECHAT_RC_OK_EAT elif args[0] == '/reply': count = int(args[1]) @@ -2970,9 +3088,8 @@ def thread_command_callback(data, current_buffer, args): return w.WEECHAT_RC_OK_EAT +@utf8_decode 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 @@ -2982,9 +3099,8 @@ def rehistory_command_callback(data, current_buffer, args): @slack_buffer_required +@utf8_decode 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') @@ -2993,9 +3109,8 @@ def hide_command_callback(data, current_buffer, args): return w.WEECHAT_RC_OK_EAT +@utf8_decode 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 @@ -3104,9 +3219,8 @@ def command_upload(data, current_buffer, args): w.hook_process(command, config.slack_timeout, '', '') +@utf8_decode 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: @@ -3164,9 +3278,8 @@ def command_back(data, current_buffer, args): @slack_buffer_required +@utf8_decode 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) @@ -3175,6 +3288,21 @@ def label_command_cb(data, current_buffer, args): w.buffer_set(channel.channel_buffer, "short_name", new_name) +@utf8_decode +def set_unread_cb(data, current_buffer, command): + for channel in EVENTROUTER.weechat_controller.buffers.values(): + channel.mark_read() + return w.WEECHAT_RC_OK + + +@slack_buffer_or_ignore +@utf8_decode +def set_unread_current_buffer_cb(data, current_buffer, command): + channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer) + channel.mark_read() + return w.WEECHAT_RC_OK + + def command_p(data, current_buffer, args): args = args.split(' ', 1)[1] w.prnt("", "{}".format(eval(args))) @@ -3246,7 +3374,8 @@ def setup_hooks(): 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", "") + if config.send_typing_notice: + w.hook_signal('input_text_changed', "typing_notification_cb", "") w.hook_command( # Command name and description @@ -3268,7 +3397,7 @@ def setup_hooks(): 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('/topic', 'topic_command_cb', '') w.hook_command_run('/thread', 'thread_command_callback', '') w.hook_command_run('/reply', 'thread_command_callback', '') w.hook_command_run('/rehistory', 'rehistory_command_callback', '') @@ -3276,6 +3405,8 @@ def setup_hooks(): 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("/input set_unread", "set_unread_cb", "") + w.hook_command_run("/input set_unread_current_buffer", "set_unread_current_buffer_cb", "") w.hook_command_run('/away', 'away_command_cb', '') w.hook_completion("nicks", "complete @-nicks for slack", "nick_completion_cb", "") @@ -3346,6 +3477,9 @@ class PluginConfig(object): 'distracting_channels': Setting( default='', desc='List of channels to hide.'), + 'group_name_prefix': Setting( + default='&', + desc='The prefix of buffer names for groups (private channels).'), 'map_underline_to': Setting( default='_', desc='When sending underlined text to slack, use this formatting' @@ -3364,6 +3498,10 @@ class PluginConfig(object): default='italic', desc='When receiving bold text from Slack, render it as this in weechat.' ' If your terminal lacks italic support, consider using "underline" instead.'), + 'send_typing_notice': Setting( + default='true', + desc='Alert Slack users when you are typing a message in the input bar ' + '(Requires reload)'), 'server_aliases': Setting( default='', desc='A comma separated list of `subdomain:alias` pairs. The alias' @@ -3456,6 +3594,7 @@ class PluginConfig(object): return int(w.config_get_plugin(key)) get_debug_level = get_int + get_group_name_prefix = get_string get_map_underline_to = get_string get_render_bold_as = get_string get_render_italic_as = get_string @@ -3556,6 +3695,7 @@ if __name__ == "__main__": # main_weechat_buffer = w.info_get("irc_buffer", "{}.{}".format(domain, "DOESNOTEXIST!@#$")) w.hook_config("plugins.var.python." + SCRIPT_NAME + ".*", "config_changed_cb", "") + w.hook_modifier("input_text_for_buffer", "input_text_for_buffer_cb", "") load_emoji() setup_hooks() |