aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md24
-rw-r--r--_pytest/conftest.py4
-rw-r--r--_pytest/test_topic_command.py96
-rw-r--r--_pytest/test_utf8_helpers.py72
-rw-r--r--wee_slack.py466
5 files changed, 487 insertions, 175 deletions
diff --git a/README.md b/README.md
index 0ab5ce5..ace8980 100644
--- a/README.md
+++ b/README.md
@@ -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()