aboutsummaryrefslogtreecommitdiffstats
path: root/wee_slack.py
diff options
context:
space:
mode:
Diffstat (limited to 'wee_slack.py')
-rw-r--r--wee_slack.py1011
1 files changed, 636 insertions, 375 deletions
diff --git a/wee_slack.py b/wee_slack.py
index 8c84b54..9e1015a 100644
--- a/wee_slack.py
+++ b/wee_slack.py
@@ -42,6 +42,12 @@ except NameError: # Python 3
basestring = unicode = str
try:
+ from collections.abc import Mapping, Reversible, KeysView, ItemsView, ValuesView
+except:
+ from collections import Mapping, KeysView, ItemsView, ValuesView
+ Reversible = object
+
+try:
from urllib.parse import quote, urlencode
except ImportError:
from urllib import quote, urlencode
@@ -64,8 +70,6 @@ SCRIPT_LICENSE = "MIT"
SCRIPT_DESC = "Extends weechat for typing notification/search/etc on slack.com"
REPO_URL = "https://github.com/wee-slack/wee-slack"
-BACKLOG_SIZE = 200
-SCROLLBACK_SIZE = 500
TYPING_DURATION = 6
RECORD_DIR = "/tmp/weeslack-debug"
@@ -280,6 +284,34 @@ class ProxyWrapper(object):
return "-x{}{}{}".format(user, self.proxy_address, port)
+class MappingReversible(Mapping, Reversible):
+ def keys(self):
+ return KeysViewReversible(self)
+
+ def items(self):
+ return ItemsViewReversible(self)
+
+ def values(self):
+ return ValuesViewReversible(self)
+
+
+class KeysViewReversible(KeysView, Reversible):
+ def __reversed__(self):
+ return reversed(self._mapping)
+
+
+class ItemsViewReversible(ItemsView, Reversible):
+ def __reversed__(self):
+ for key in reversed(self._mapping):
+ yield (key, self._mapping[key])
+
+
+class ValuesViewReversible(ValuesView, Reversible):
+ def __reversed__(self):
+ for key in reversed(self._mapping):
+ yield self._mapping[key]
+
+
##### Helpers
@@ -295,6 +327,14 @@ def print_error(message, buffer='', warning=False):
w.prnt(buffer, '{}{}: {}'.format(w.prefix('error'), prefix, message))
+def print_message_not_found_error(msg_id):
+ if msg_id:
+ print_error("Invalid id given, must be an existing id or a number greater " +
+ "than 0 and less than the number of messages in the channel")
+ else:
+ print_error("No messages found in channel")
+
+
def token_for_print(token):
return '{}...{}'.format(token[:15], token[-10:])
@@ -508,7 +548,7 @@ class EventRouter(object):
team.domain))
team.set_disconnected()
if not team.connected:
- team.connect()
+ team.connect(reconnect=True)
dbg("reconnecting {}".format(team))
@utf8_decode
@@ -601,25 +641,17 @@ class EventRouter(object):
self.receive(request_metadata)
return w.WEECHAT_RC_OK
- def receive(self, dataobj):
+ def receive(self, dataobj, slow=False):
"""
- complete
Receives a raw object and places it on the queue for
processing. Object must be known to handle_next or
be JSON.
"""
dbg("RECEIVED FROM QUEUE")
- self.queue.append(dataobj)
-
- def receive_slow(self, dataobj):
- """
- complete
- Receives a raw object and places it on the slow queue for
- processing. Object must be known to handle_next or
- be JSON.
- """
- dbg("RECEIVED FROM QUEUE")
- self.slow_queue.append(dataobj)
+ if slow:
+ self.slow_queue.append(dataobj)
+ else:
+ self.queue.append(dataobj)
def handle_next(self):
"""
@@ -821,24 +853,15 @@ def buffer_input_callback(signal, buffer_ptr, data):
if not channel:
return w.WEECHAT_RC_ERROR
- def get_id(message_id):
- if not message_id:
- return 1
- elif message_id[0] == "$":
- return message_id[1:]
- else:
- return int(message_id)
-
reaction = re.match(r"{}{}\s*$".format(REACTION_PREFIX_REGEX_STRING, EMOJI_CHAR_OR_NAME_REGEX_STRING), data)
substitute = re.match("{}?s/".format(MESSAGE_ID_REGEX_STRING), data)
if reaction:
emoji = reaction.group("emoji_char") or reaction.group("emoji_name")
if reaction.group("reaction_change") == "+":
- channel.send_add_reaction(get_id(reaction.group("msg_id")), emoji)
+ channel.send_add_reaction(reaction.group("msg_id"), emoji)
elif reaction.group("reaction_change") == "-":
- channel.send_remove_reaction(get_id(reaction.group("msg_id")), emoji)
+ channel.send_remove_reaction(reaction.group("msg_id"), emoji)
elif substitute:
- msg_id = get_id(substitute.group("msg_id"))
try:
old, new, flags = re.split(r'(?<!\\)/', data)[1:]
except ValueError:
@@ -849,7 +872,7 @@ def buffer_input_callback(signal, buffer_ptr, data):
# rid of escapes.
new = new.replace(r'\/', '/')
old = old.replace(r'\/', '/')
- channel.edit_nth_previous_message(msg_id, old, new, flags)
+ channel.edit_nth_previous_message(substitute.group("msg_id"), old, new, flags)
else:
if data.startswith(('//', ' ')):
data = data[1:]
@@ -886,7 +909,7 @@ def buffer_switch_callback(signal, sig_type, data):
new_channel = eventrouter.weechat_controller.get_channel_from_buffer_ptr(data)
if new_channel:
- if not new_channel.got_history:
+ if not new_channel.got_history or new_channel.history_needs_update:
new_channel.get_history()
set_own_presence_active(new_channel.team)
@@ -1080,7 +1103,7 @@ def thread_completion_cb(data, completion_item, current_buffer, completion):
if current_channel is None or not hasattr(current_channel, 'hashed_messages'):
return w.WEECHAT_RC_OK
- threads = current_channel.hashed_messages.items()
+ threads = (x for x in current_channel.hashed_messages.items() if isinstance(x[0], str))
for thread_id, message_ts in sorted(threads, key=lambda item: item[1]):
message = current_channel.messages.get(message_ts)
if message and message.number_of_replies():
@@ -1292,6 +1315,7 @@ class SlackTeam(object):
self.name = self.domain
self.channel_buffer = None
self.got_history = True
+ self.history_needs_update = False
self.create_buffer()
self.set_muted_channels(kwargs.get('muted_channels', ""))
self.set_highlight_words(kwargs.get('highlight_words', ""))
@@ -1406,7 +1430,7 @@ class SlackTeam(object):
def mark_read(self, ts=None, update_remote=True, force=False):
pass
- def connect(self):
+ def connect(self, reconnect=False):
if not self.connected and not self.connecting_ws:
if self.ws_url:
self.connecting_ws = True
@@ -1420,31 +1444,40 @@ class SlackTeam(object):
self.hook = w.hook_fd(ws.sock.fileno(), 1, 0, 0, "receive_ws_callback", self.get_team_hash())
ws.sock.setblocking(0)
- self.ws = ws
- self.set_reconnect_url(None)
- self.set_connected()
- self.connecting_ws = False
except:
w.prnt(self.channel_buffer,
'Failed connecting to slack team {}, retrying.'.format(self.domain))
dbg('connect failed with exception:\n{}'.format(format_exc_tb()), level=5)
- self.connecting_ws = False
return False
+ finally:
+ self.connecting_ws = False
+ self.ws = ws
+ self.set_reconnect_url(None)
+ self.set_connected(reconnect)
elif not self.connecting_rtm:
# The fast reconnect failed, so start over-ish
for chan in self.channels:
- self.channels[chan].got_history = False
- s = initiate_connection(self.token, retries=999, team=self)
+ self.channels[chan].history_needs_update = True
+ s = initiate_connection(self.token, retries=999, team=self, reconnect=reconnect)
self.eventrouter.receive(s)
self.connecting_rtm = True
- def set_connected(self):
+ def set_connected(self, reconnect):
self.connected = True
self.last_pong_time = time.time()
self.buffer_prnt('Connected to Slack team {} ({}) with username {}'.format(
self.team_info["name"], self.domain, self.nick))
dbg("connected to {}".format(self.domain))
+ if not config.background_load_all_history:
+ current_channel = self.eventrouter.weechat_controller.buffers.get(w.current_buffer())
+ if isinstance(current_channel, SlackChannelCommon) and current_channel.team == self:
+ current_channel.get_history(slow_queue=True)
+ elif reconnect:
+ for channel in self.channels.values():
+ if channel.channel_buffer:
+ channel.get_history(slow_queue=True)
+
def set_disconnected(self):
w.unhook(self.hook)
self.connected = False
@@ -1491,6 +1524,56 @@ class SlackTeam(object):
class SlackChannelCommon(object):
+ def prnt_message(self, message, history_message=False, no_log=False, force_render=False):
+ text = self.render(message, force_render)
+ thread_channel = isinstance(self, SlackThreadChannel)
+
+ if message.subtype == "join":
+ tagset = "join"
+ prefix = w.prefix("join").strip()
+ elif message.subtype == "leave":
+ tagset = "leave"
+ prefix = w.prefix("quit").strip()
+ elif message.subtype == "topic":
+ tagset = "topic"
+ prefix = w.prefix("network").strip()
+ else:
+ channel_type = self.parent_channel.type if thread_channel else self.type
+ if channel_type in ["im", "mpim"]:
+ tagset = "dm"
+ else:
+ tagset = "channel"
+
+ if message.subtype == "me_message":
+ prefix = w.prefix("action").rstrip()
+ else:
+ prefix = message.sender
+
+ extra_tags = None
+ if isinstance(message, SlackThreadMessage) and not thread_channel:
+ if config.thread_messages_in_channel or message.subtype == "thread_broadcast":
+ extra_tags = [message.subtype]
+ else:
+ return
+
+ self.buffer_prnt(prefix, text, message.ts, tagset=tagset,
+ tag_nick=message.sender_plain, history_message=history_message,
+ no_log=no_log, extra_tags=extra_tags)
+
+ def print_getting_history(self):
+ if self.channel_buffer:
+ w.prnt_date_tags(self.channel_buffer, SlackTS().major,
+ tag(backlog=True, no_log=True), '\tgetting channel history...')
+
+ def reprint_messages(self, history_message=False, no_log=True, force_render=False):
+ if self.channel_buffer:
+ w.buffer_clear(self.channel_buffer)
+ for message in self.visible_messages.values():
+ self.prnt_message(message, history_message, no_log, force_render)
+ if (self.identifier in self.pending_history_requests or
+ config.thread_messages_in_channel and self.pending_history_requests):
+ self.print_getting_history()
+
def send_add_reaction(self, msg_id, reaction):
self.send_change_reaction("reactions.add", msg_id, reaction)
@@ -1498,36 +1581,37 @@ class SlackChannelCommon(object):
self.send_change_reaction("reactions.remove", msg_id, reaction)
def send_change_reaction(self, method, msg_id, reaction):
- if type(msg_id) is not int:
- if msg_id in self.hashed_messages:
- timestamp = self.hashed_messages[msg_id]
- else:
- return
- elif 0 < msg_id <= len(self.messages):
- keys = self.main_message_keys_reversed()
- timestamp = next(islice(keys, msg_id - 1, None))
- else:
+ message = self.message_from_hash_or_index(msg_id)
+ if message is None:
+ print_message_not_found_error(msg_id)
return
reaction_name = replace_emoji_with_string(reaction)
if method == "toggle":
- message = self.messages.get(timestamp)
reaction = message.get_reaction(reaction_name)
if reaction and self.team.myidentifier in reaction["users"]:
method = "reactions.remove"
else:
method = "reactions.add"
- data = {"channel": self.identifier, "timestamp": timestamp, "name": reaction_name}
+ data = {"channel": self.identifier, "timestamp": message.ts, "name": reaction_name}
s = SlackRequest(self.team, method, data, channel=self, metadata={'reaction': reaction})
self.eventrouter.receive(s)
def edit_nth_previous_message(self, msg_id, old, new, flags):
- message = self.my_last_message(msg_id)
+ message_filter = lambda message: message.user_identifier == self.team.myidentifier
+ message = self.message_from_hash_or_index(msg_id, message_filter)
if message is None:
+ if msg_id:
+ print_error("Invalid id given, must be an existing id to one of your " +
+ "messages or a number greater than 0 and less than the number " +
+ "of your messages in the channel")
+ else:
+ print_error("You don't have any messages in this channel")
return
if new == "" and old == "":
- s = SlackRequest(self.team, "chat.delete", {"channel": self.identifier, "ts": message['ts']}, channel=self)
+ post_data = {"channel": self.identifier, "ts": message.ts}
+ s = SlackRequest(self.team, "chat.delete", post_data, channel=self)
self.eventrouter.receive(s)
else:
num_replace = 0 if 'g' in flags else 1
@@ -1535,27 +1619,46 @@ class SlackChannelCommon(object):
f |= re.IGNORECASE if 'i' in flags else 0
f |= re.MULTILINE if 'm' in flags else 0
f |= re.DOTALL if 's' in flags else 0
- new_message = re.sub(old, new, message["text"], num_replace, f)
- if new_message != message["text"]:
- s = SlackRequest(self.team, "chat.update",
- {"channel": self.identifier, "ts": message['ts'], "text": new_message}, channel=self)
+ old_message_text = message.message_json["text"]
+ new_message_text = re.sub(old, new, old_message_text, num_replace, f)
+ if new_message_text != old_message_text:
+ post_data = {"channel": self.identifier, "ts": message.ts, "text": new_message_text}
+ s = SlackRequest(self.team, "chat.update", post_data, channel=self)
self.eventrouter.receive(s)
else:
print_error("The regex didn't match any part of the message")
- def my_last_message(self, msg_id):
- if type(msg_id) is not int:
- ts = self.hashed_messages.get(msg_id)
- m = self.messages.get(ts)
- if m is not None and m.message_json.get("user") == self.team.myidentifier:
- return m.message_json
- else:
- for key in self.main_message_keys_reversed():
- m = self.messages[key]
- if m.message_json.get("user") == self.team.myidentifier:
- msg_id -= 1
- if msg_id == 0:
- return m.message_json
+ def message_from_hash(self, ts_hash, message_filter=None):
+ if not ts_hash:
+ return
+ ts_hash_without_prefix = ts_hash[1:] if ts_hash[0] == "$" else ts_hash
+ ts = self.hashed_messages.get(ts_hash_without_prefix)
+ message = self.messages.get(ts)
+ if message is None:
+ return
+ if message_filter and not message_filter(message):
+ return
+ return message
+
+ def message_from_index(self, index, message_filter=None, reverse=True):
+ for ts in (reversed(self.visible_messages) if reverse else self.visible_messages):
+ message = self.messages[ts]
+ if not message_filter or message_filter(message):
+ index -= 1
+ if index == 0:
+ return message
+
+ def message_from_hash_or_index(self, hash_or_index=None, message_filter=None, reverse=True):
+ message = self.message_from_hash(hash_or_index, message_filter)
+ if not message:
+ if not hash_or_index:
+ index = 1
+ elif hash_or_index.isdigit():
+ index = int(hash_or_index)
+ else:
+ return
+ message = self.message_from_index(index, message_filter, reverse)
+ return message
def change_message(self, ts, message_json=None, text=None):
ts = SlackTS(ts)
@@ -1567,54 +1670,17 @@ class SlackChannelCommon(object):
if text:
m.change_text(text)
- if type(m) == SlackMessage or config.thread_messages_in_channel:
+ if (type(m) == SlackMessage or m.subtype == "thread_broadcast"
+ or config.thread_messages_in_channel):
new_text = self.render(m, force=True)
modify_buffer_line(self.channel_buffer, ts, new_text)
- if type(m) == SlackThreadMessage:
- thread_channel = m.parent_message.thread_channel
+ if type(m) == SlackThreadMessage or m.thread_channel is not None:
+ thread_channel = (m.parent_message.thread_channel
+ if isinstance(m, SlackThreadMessage) else m.thread_channel)
if thread_channel and thread_channel.active:
new_text = thread_channel.render(m, force=True)
modify_buffer_line(thread_channel.channel_buffer, ts, new_text)
- def hash_message(self, ts):
- ts = SlackTS(ts)
-
- def calc_hash(ts):
- return sha1_hex(str(ts))
-
- if ts in self.messages and not self.messages[ts].hash:
- message = self.messages[ts]
- tshash = calc_hash(message.ts)
- hl = 3
-
- for i in range(hl, len(tshash) + 1):
- shorthash = tshash[:i]
- if self.hashed_messages.get(shorthash) == ts:
- message.hash = shorthash
- return shorthash
-
- shorthash = tshash[:hl]
- while any(x.startswith(shorthash) for x in self.hashed_messages):
- hl += 1
- shorthash = tshash[:hl]
-
- if shorthash[:-1] in self.hashed_messages:
- col_ts = self.hashed_messages.pop(shorthash[:-1])
- col_new_hash = calc_hash(col_ts)[:hl]
- self.hashed_messages[col_new_hash] = col_ts
- col_msg = self.messages.get(col_ts)
- if col_msg:
- col_msg.hash = col_new_hash
- self.change_message(str(col_msg.ts))
- if col_msg.thread_channel:
- col_msg.thread_channel.rename()
-
- self.hashed_messages[shorthash] = message.ts
- message.hash = shorthash
- return shorthash
- elif ts in self.messages:
- return self.messages[ts].hash
-
def mark_read(self, ts=None, update_remote=True, force=False, post_data={}):
if self.new_messages or force:
if self.channel_buffer:
@@ -1623,7 +1689,7 @@ class SlackChannelCommon(object):
if not ts:
ts = next(reversed(self.messages), SlackTS())
if ts > self.last_read:
- self.last_read = ts
+ self.last_read = SlackTS(ts)
if update_remote:
args = {"channel": self.identifier, "ts": ts}
args.update(post_data)
@@ -1650,11 +1716,14 @@ class SlackChannel(SlackChannelCommon):
self.set_name(kwargs["name"])
self.slack_purpose = kwargs.get("purpose", {"value": ""})
self.topic = kwargs.get("topic", {"value": ""})
- self.last_read = SlackTS(kwargs.get("last_read", SlackTS()))
+ self.last_read = SlackTS(kwargs.get("last_read", 0))
self.channel_buffer = None
self.got_history = False
+ self.history_needs_update = False
+ self.pending_history_requests = set()
self.messages = OrderedDict()
- self.hashed_messages = {}
+ self.visible_messages = SlackChannelVisibleMessages(self)
+ self.hashed_messages = SlackChannelHashedMessages(self)
self.thread_channels = {}
self.new_messages = False
self.typing = {}
@@ -1842,13 +1911,9 @@ class SlackChannel(SlackChannelCommon):
s = SlackRequest(self.team, join_method, {"users": self.user, "return_im": True}, channel=self)
self.eventrouter.receive(s)
- def clear_messages(self):
- w.buffer_clear(self.channel_buffer)
+ def destroy_buffer(self, update_remote):
self.messages = OrderedDict()
self.got_history = False
-
- def destroy_buffer(self, update_remote):
- self.clear_messages()
self.channel_buffer = None
self.active = False
if update_remote and not self.eventrouter.shutting_down:
@@ -1856,27 +1921,20 @@ class SlackChannel(SlackChannelCommon):
{"channel": self.identifier}, channel=self)
self.eventrouter.receive(s)
- def buffer_prnt(self, nick, text, timestamp=str(time.time()), tagset=None, tag_nick=None, history_message=False, extra_tags=None):
+ def buffer_prnt(self, nick, text, timestamp, tagset, tag_nick=None, history_message=False, no_log=False, extra_tags=None):
data = "{}\t{}".format(format_nick(nick, self.last_line_from), text)
self.last_line_from = nick
ts = SlackTS(timestamp)
- last_read = SlackTS(self.last_read)
# without this, DMs won't open automatically
- if not self.channel_buffer and ts > last_read:
+ if not self.channel_buffer and ts > self.last_read:
self.open(update_remote=False)
if self.channel_buffer:
# backlog messages - we will update the read marker as we print these
- backlog = ts <= last_read
+ backlog = ts <= self.last_read
if not backlog:
self.new_messages = True
- if not tagset:
- if self.type in ["im", "mpim"]:
- tagset = "dm"
- else:
- tagset = "channel"
-
- no_log = history_message and backlog
+ no_log = no_log or history_message and backlog
self_msg = tag_nick == self.team.nick
tags = tag(tagset, user=tag_nick, self_msg=self_msg, backlog=backlog, no_log=no_log, extra_tags=extra_tags)
@@ -1904,42 +1962,79 @@ class SlackChannel(SlackChannelCommon):
request.update(request_dict_ext)
self.team.send_to_websocket(request)
- def store_message(self, message, team, from_me=False):
+ def store_message(self, message_to_store):
if not self.active:
return
- if from_me:
- message.message_json["user"] = team.myidentifier
- self.messages[SlackTS(message.ts)] = message
-
- sorted_messages = sorted(self.messages.items())
- messages_to_delete = sorted_messages[:-SCROLLBACK_SIZE]
- messages_to_keep = sorted_messages[-SCROLLBACK_SIZE:]
- for message_hash in [m[1].hash for m in messages_to_delete]:
- if message_hash in self.hashed_messages:
+
+ old_message = self.messages.get(message_to_store.ts)
+ if old_message and old_message.submessages and not message_to_store.submessages:
+ message_to_store.submessages = old_message.submessages
+
+ self.messages[message_to_store.ts] = message_to_store
+ self.messages = OrderedDict(sorted(self.messages.items()))
+
+ max_history = w.config_integer(w.config_get("weechat.history.max_buffer_lines_number"))
+ messages_to_check = islice(self.messages.items(),
+ max(0, len(self.messages) - max_history))
+ messages_to_delete = []
+ for (ts, message) in messages_to_check:
+ if ts == message_to_store.ts:
+ pass
+ elif isinstance(message, SlackThreadMessage):
+ thread_channel = self.thread_channels.get(message.thread_ts)
+ if thread_channel is None or not thread_channel.active:
+ messages_to_delete.append(ts)
+ elif message.number_of_replies():
+ if ((message.thread_channel is None or not message.thread_channel.active) and
+ not any(submessage in self.messages for submessage in message.submessages)):
+ messages_to_delete.append(ts)
+ else:
+ messages_to_delete.append(ts)
+
+ for ts in messages_to_delete:
+ message_hash = self.hashed_messages.get(ts)
+ if message_hash:
+ del self.hashed_messages[ts]
del self.hashed_messages[message_hash]
- self.messages = OrderedDict(messages_to_keep)
+ del self.messages[ts]
def is_visible(self):
return w.buffer_get_integer(self.channel_buffer, "hidden") == 0
- def get_history(self, slow_queue=False):
- if not self.got_history:
- # we have probably reconnected. flush the buffer
- if self.team.connected:
- self.clear_messages()
- w.prnt_date_tags(self.channel_buffer, SlackTS().major,
- tag(backlog=True, no_log=True), '\tgetting channel history...')
- s = SlackRequest(self.team, self.team.slack_api_translator[self.type]["history"],
- {"channel": self.identifier, "count": BACKLOG_SIZE}, channel=self, metadata={'clear': True})
- if not slow_queue:
- self.eventrouter.receive(s)
- else:
- self.eventrouter.receive_slow(s)
- self.got_history = True
+ def get_history(self, slow_queue=False, full=False, no_log=False):
+ if self.identifier in self.pending_history_requests:
+ return
+
+ self.print_getting_history()
+ self.pending_history_requests.add(self.identifier)
+
+ post_data = {"channel": self.identifier, "count": config.history_fetch_count}
+ if self.got_history and self.messages and not full:
+ post_data["oldest"] = next(reversed(self.messages))
+
+ s = SlackRequest(self.team, self.team.slack_api_translator[self.type]["history"],
+ post_data, channel=self, metadata={"slow_queue": slow_queue, "no_log": no_log})
+ self.eventrouter.receive(s, slow_queue)
+ self.got_history = True
+ self.history_needs_update = False
+
+ def get_thread_history(self, thread_ts, slow_queue=False, no_log=False):
+ if thread_ts in self.pending_history_requests:
+ return
- def main_message_keys_reversed(self):
- return (key for key in reversed(self.messages)
- if type(self.messages[key]) == SlackMessage)
+ if config.thread_messages_in_channel:
+ self.print_getting_history()
+ thread_channel = self.thread_channels.get(thread_ts)
+ if thread_channel and thread_channel.active:
+ thread_channel.print_getting_history()
+ self.pending_history_requests.add(thread_ts)
+
+ post_data = {"channel": self.identifier, "ts": thread_ts,
+ "limit": config.history_fetch_count}
+ s = SlackRequest(self.team, "conversations.replies",
+ post_data, channel=self,
+ metadata={"thread_ts": thread_ts, "no_log": no_log})
+ self.eventrouter.receive(s, slow_queue)
# Typing related
def set_typing(self, user):
@@ -2046,12 +2141,96 @@ class SlackChannel(SlackChannelCommon):
def render(self, message, force=False):
text = message.render(force)
if isinstance(message, SlackThreadMessage):
- thread_id = message.parent_message.hash or message.parent_message.ts
- return colorize_string(get_thread_color(thread_id), '[{}]'.format(thread_id)) + ' {}'.format(text)
+ thread_hash = self.hashed_messages[message.thread_ts]
+ hash_str = colorize_string(
+ get_thread_color(str(thread_hash)), '[{}]'.format(thread_hash))
+ return '{} {}'.format(hash_str, text)
return text
+class SlackChannelVisibleMessages(MappingReversible):
+ """
+ Class with a reversible mapping interface (like a read-only OrderedDict)
+ which doesn't include the messages older than first_ts_to_display.
+ """
+
+ def __init__(self, channel):
+ self.channel = channel
+ self.first_ts_to_display = SlackTS(0)
+
+ def __getitem__(self, key):
+ if key < self.first_ts_to_display:
+ raise KeyError(key)
+ return self.channel.messages[key]
+
+ def _is_visible(self, ts):
+ if ts < self.first_ts_to_display:
+ return False
+
+ if (not config.thread_messages_in_channel and
+ isinstance(self.get(ts), SlackThreadMessage)):
+ return False
+
+ return True
+
+ def __iter__(self):
+ for ts in self.channel.messages:
+ if self._is_visible(ts):
+ yield ts
+
+ def __len__(self):
+ i = 0
+ for _ in self:
+ i += 1
+ return i
+
+ def __reversed__(self):
+ for ts in reversed(self.channel.messages):
+ if self._is_visible(ts):
+ yield ts
+
+
+class SlackChannelHashedMessages(dict):
+ def __init__(self, channel):
+ self.channel = channel
+
+ def __missing__(self, key):
+ if not isinstance(key, SlackTS):
+ raise KeyError(key)
+
+ hash_len = 3
+ full_hash = sha1_hex(str(key))
+ short_hash = full_hash[:hash_len]
+
+ while any(x.startswith(short_hash) for x in self if isinstance(x, str)):
+ hash_len += 1
+ short_hash = full_hash[:hash_len]
+
+ if short_hash[:-1] in self:
+ ts_with_same_hash = self.pop(short_hash[:-1])
+ other_full_hash = sha1_hex(str(ts_with_same_hash))
+ other_short_hash = other_full_hash[:hash_len]
+ while short_hash == other_short_hash:
+ hash_len += 1
+ short_hash = full_hash[:hash_len]
+ other_short_hash = other_full_hash[:hash_len]
+ self[other_short_hash] = ts_with_same_hash
+ self[ts_with_same_hash] = other_short_hash
+
+ other_message = self.channel.messages.get(ts_with_same_hash)
+ if other_message:
+ self.channel.change_message(other_message.ts)
+ if other_message.thread_channel:
+ other_message.thread_channel.rename()
+ for thread_message in other_message.submessages:
+ self.channel.change_message(thread_message)
+
+ self[short_hash] = key
+ self[key] = short_hash
+ return self[key]
+
+
class SlackDMChannel(SlackChannel):
"""
Subclass of a normal channel for person-to-person communication, which
@@ -2191,7 +2370,7 @@ class SlackSharedChannel(SlackChannel):
def __init__(self, eventrouter, **kwargs):
super(SlackSharedChannel, self).__init__(eventrouter, "shared", **kwargs)
- def get_history(self, slow_queue=False):
+ def get_history(self, slow_queue=False, full=False, no_log=False):
# Get info for external users in the channel
for user in self.members - set(self.team.users.keys()):
s = SlackRequest(self.team, 'users.info', {'user': user}, channel=self)
@@ -2199,7 +2378,7 @@ class SlackSharedChannel(SlackChannel):
# Fetch members since they aren't included in rtm.start
s = SlackRequest(self.team, 'conversations.members', {'channel': self.identifier}, channel=self)
self.eventrouter.receive(s)
- super(SlackSharedChannel, self).get_history(slow_queue)
+ super(SlackSharedChannel, self).get_history(slow_queue, full, no_log)
class SlackThreadChannel(SlackChannelCommon):
@@ -2208,22 +2387,33 @@ class SlackThreadChannel(SlackChannelCommon):
SlackChannel, because most of how it operates will be different.
"""
- def __init__(self, eventrouter, parent_message):
+ def __init__(self, eventrouter, parent_channel, thread_ts):
+ self.active = False
self.eventrouter = eventrouter
- self.parent_message = parent_message
- self.hashed_messages = {}
+ self.parent_channel = parent_channel
+ self.thread_ts = thread_ts
+ self.messages = SlackThreadChannelMessages(self)
self.channel_buffer = None
self.type = "thread"
self.got_history = False
+ self.history_needs_update = False
self.label = None
- self.team = self.parent_message.team
+ self.team = self.parent_channel.team
self.last_line_from = None
self.new_messages = False
self.buffer_name_needs_update = False
@property
def members(self):
- return self.parent_message.channel.members
+ return self.parent_channel.members
+
+ @property
+ def parent_message(self):
+ return self.parent_channel.messages[self.thread_ts]
+
+ @property
+ def hashed_messages(self):
+ return self.parent_channel.hashed_messages
@property
def last_read(self):
@@ -2235,31 +2425,40 @@ class SlackThreadChannel(SlackChannelCommon):
@property
def identifier(self):
- return self.parent_message.channel.identifier
+ return self.parent_channel.identifier
@property
- def messages(self):
- return self.parent_message.channel.messages
+ def visible_messages(self):
+ return self.messages
@property
def muted(self):
- return self.parent_message.channel.muted
+ return self.parent_channel.muted
+
+ @property
+ def pending_history_requests(self):
+ if self.thread_ts in self.parent_channel.pending_history_requests:
+ return {self.identifier, self.thread_ts}
+ else:
+ return set()
def formatted_name(self, style="default"):
- hash_or_ts = self.parent_message.hash or self.parent_message.ts
+ thread_hash = self.parent_message.hash
styles = {
- "default": " +{}".format(hash_or_ts),
- "long_default": "{}.{}".format(self.parent_message.channel.formatted_name(style="long_default"), hash_or_ts),
- "sidebar": " +{}".format(hash_or_ts),
+ "default": " +{}".format(thread_hash),
+ "long_default": "{}.{}".format(self.parent_channel.formatted_name(style="long_default"), thread_hash),
+ "sidebar": " +{}".format(thread_hash),
}
return styles[style]
def mark_read(self, ts=None, update_remote=True, force=False, post_data={}):
- args = {"thread_ts": self.parent_message.ts}
+ if not self.parent_message.subscribed:
+ return
+ args = {"thread_ts": self.thread_ts}
args.update(post_data)
super(SlackThreadChannel, self).mark_read(ts=ts, update_remote=update_remote, force=force, post_data=args)
- def buffer_prnt(self, nick, text, timestamp, history_message=False, tag_nick=None):
+ def buffer_prnt(self, nick, text, timestamp, tagset, tag_nick=None, history_message=False, no_log=False, extra_tags=None):
data = "{}\t{}".format(format_nick(nick, self.last_line_from), text)
self.last_line_from = nick
ts = SlackTS(timestamp)
@@ -2269,33 +2468,26 @@ class SlackThreadChannel(SlackChannelCommon):
if not backlog:
self.new_messages = True
- if self.parent_message.channel.type in ["im", "mpim"]:
- tagset = "dm"
- else:
- tagset = "channel"
-
- no_log = history_message and backlog
+ no_log = no_log or history_message and backlog
self_msg = tag_nick == self.team.nick
- tags = tag(tagset, user=tag_nick, self_msg=self_msg, backlog=backlog, no_log=no_log)
+ tags = tag(tagset, user=tag_nick, self_msg=self_msg, backlog=backlog, no_log=no_log, extra_tags=extra_tags)
w.prnt_date_tags(self.channel_buffer, ts.major, tags, data)
modify_last_print_time(self.channel_buffer, ts.minor)
if backlog or self_msg:
self.mark_read(ts, update_remote=False, force=True)
- def get_history(self):
+ def get_history(self, slow_queue=False, full=False, no_log=False):
self.got_history = True
- for message in chain([self.parent_message], self.parent_message.submessages):
- text = self.render(message)
- self.buffer_prnt(message.sender, text, message.ts, history_message=True, tag_nick=message.sender_plain)
- if len(self.parent_message.submessages) < self.parent_message.number_of_replies():
- s = SlackRequest(self.team, "conversations.replies",
- {"channel": self.identifier, "ts": self.parent_message.ts},
- channel=self.parent_message.channel)
- self.eventrouter.receive(s)
+ self.history_needs_update = False
- def main_message_keys_reversed(self):
- return (message.ts for message in reversed(self.parent_message.submessages))
+ any_msg_is_none = any(message is None for message in self.messages.values())
+ if not any_msg_is_none:
+ self.reprint_messages(history_message=True, no_log=no_log)
+
+ if (full or any_msg_is_none or
+ len(self.parent_message.submessages) < self.parent_message.number_of_replies()):
+ self.parent_channel.get_thread_history(self.thread_ts, slow_queue, no_log)
def send_message(self, message, subtype=None, request_dict_ext={}):
if subtype == 'me_message':
@@ -2304,8 +2496,8 @@ class SlackThreadChannel(SlackChannelCommon):
message = linkify_text(message, self.team)
dbg(message)
request = {"type": "message", "text": message,
- "channel": self.parent_message.channel.identifier,
- "thread_ts": str(self.parent_message.ts),
+ "channel": self.parent_channel.identifier,
+ "thread_ts": str(self.thread_ts),
"user": self.team.myidentifier}
request.update(request_dict_ext)
self.team.send_to_websocket(request)
@@ -2328,7 +2520,7 @@ class SlackThreadChannel(SlackChannelCommon):
def set_highlights(self, highlight_string=None):
if self.channel_buffer:
if highlight_string is None:
- highlight_string = ",".join(self.parent_message.channel.highlights())
+ highlight_string = ",".join(self.parent_channel.highlights())
w.buffer_set(self.channel_buffer, "highlight_words", highlight_string)
def create_buffer(self):
@@ -2345,7 +2537,7 @@ class SlackThreadChannel(SlackChannelCommon):
w.buffer_set(self.channel_buffer, "short_name", self.formatted_name(style="sidebar"))
self.set_highlights()
time_format = w.config_string(w.config_get("weechat.look.buffer_time_format"))
- parent_time = time.localtime(SlackTS(self.parent_message.ts).major)
+ parent_time = time.localtime(SlackTS(self.thread_ts).major)
topic = '{} {} | {}'.format(time.strftime(time_format, parent_time),
self.parent_message.sender, self.render(self.parent_message))
w.buffer_set(self.channel_buffer, "title", topic)
@@ -2354,11 +2546,45 @@ class SlackThreadChannel(SlackChannelCommon):
self.channel_buffer = None
self.got_history = False
self.active = False
+ if update_remote and not self.eventrouter.shutting_down:
+ self.mark_read()
def render(self, message, force=False):
return message.render(force)
+class SlackThreadChannelMessages(MappingReversible):
+ """
+ Class with a reversible mapping interface (like a read-only OrderedDict)
+ which looks up messages using the parent channel and parent message.
+ """
+
+ def __init__(self, thread_channel):
+ self.thread_channel = thread_channel
+
+ @property
+ def _parent_message(self):
+ return self.thread_channel.parent_message
+
+ def __getitem__(self, key):
+ if key != self._parent_message.ts and key not in self._parent_message.submessages:
+ raise KeyError(key)
+ return self.thread_channel.parent_channel.messages[key]
+
+ def __iter__(self):
+ yield self._parent_message.ts
+ for ts in self._parent_message.submessages:
+ yield ts
+
+ def __len__(self):
+ return 1 + len(self._parent_message.submessages)
+
+ def __reversed__(self):
+ for ts in reversed(self._parent_message.submessages):
+ yield ts
+ yield self._parent_message.ts
+
+
class SlackUser(object):
"""
Represends an individual slack user. Also where you set their name formatting.
@@ -2420,32 +2646,32 @@ class SlackMessage(object):
Note: these can't be tied to a SlackUser object because users
can be deleted, so we have to store sender in each one.
"""
- def __init__(self, message_json, team, channel, override_sender=None):
+ def __init__(self, subtype, message_json, team, channel):
self.team = team
self.channel = channel
+ self.subtype = subtype
+ self.user_identifier = message_json.get('user')
self.message_json = message_json
self.submessages = []
- self.hash = None
- if override_sender:
- self.sender = override_sender
- self.sender_plain = override_sender
- else:
- senders = self.get_sender()
- self.sender, self.sender_plain = senders[0], senders[1]
self.ts = SlackTS(message_json['ts'])
self.subscribed = message_json.get("subscribed", False)
- self.last_read = message_json.get("last_read", SlackTS())
+ self.last_read = SlackTS(message_json.get("last_read", 0))
+ self.last_notify = SlackTS(0)
def __hash__(self):
return hash(self.ts)
@property
+ def hash(self):
+ return self.channel.hashed_messages[self.ts]
+
+ @property
def thread_channel(self):
return self.channel.thread_channels.get(self.ts)
def open_thread(self, switch=False):
if not self.thread_channel or not self.thread_channel.active:
- self.channel.thread_channels[self.ts] = SlackThreadChannel(EVENTROUTER, self)
+ self.channel.thread_channels[self.ts] = SlackThreadChannel(EVENTROUTER, self.channel, self.ts)
self.thread_channel.open()
if switch:
w.buffer_set(self.thread_channel.channel_buffer, "display", "1")
@@ -2479,7 +2705,7 @@ class SlackMessage(object):
text = unfurl_refs(text)
- if (self.message_json.get('subtype') == 'me_message' and
+ if (self.subtype == 'me_message' and
not self.message_json['text'].startswith(self.sender)):
text = "{} {}".format(self.sender, text)
@@ -2494,7 +2720,6 @@ class SlackMessage(object):
self.message_json.get("reactions", ""), self.team.myidentifier)
if self.number_of_replies():
- self.channel.hash_message(self.ts)
text += " " + colorize_string(get_thread_color(self.hash), "[ Thread: {} Replies: {}{} ]".format(
self.hash, self.number_of_replies(), " Subscribed" if self.subscribed else ""))
@@ -2507,31 +2732,43 @@ class SlackMessage(object):
self.message_json["text"] = new_text
dbg(self.message_json)
- def get_sender(self):
- name = ""
- name_plain = ""
- user = self.team.users.get(self.message_json.get('user'))
+ def get_sender(self, plain):
+ user = self.team.users.get(self.user_identifier)
if user:
- name = "{}".format(user.formatted_name())
- name_plain = "{}".format(user.formatted_name(enable_color=False))
+ name = "{}".format(user.formatted_name(enable_color=not plain))
if user.is_external:
name += config.external_user_suffix
- name_plain += config.external_user_suffix
+ return name
elif 'username' in self.message_json:
username = self.message_json["username"]
- if self.message_json.get("subtype") == "bot_message":
- name = "{} :]".format(username)
- name_plain = "{}".format(username)
+ if plain:
+ return username
+ elif self.message_json.get("subtype") == "bot_message":
+ return "{} :]".format(username)
else:
- name = "-{}-".format(username)
- name_plain = "{}".format(username)
+ return "-{}-".format(username)
elif 'service_name' in self.message_json:
- name = "-{}-".format(self.message_json["service_name"])
- name_plain = "{}".format(self.message_json["service_name"])
+ service_name = self.message_json["service_name"]
+ if plain:
+ return service_name
+ else:
+ return "-{}-".format(service_name)
elif self.message_json.get('bot_id') in self.team.bots:
- name = "{} :]".format(self.team.bots[self.message_json["bot_id"]].formatted_name())
- name_plain = "{}".format(self.team.bots[self.message_json["bot_id"]].formatted_name(enable_color=False))
- return (name, name_plain)
+ bot = self.team.bots[self.message_json["bot_id"]]
+ name = bot.formatted_name(enable_color=not plain)
+ if plain:
+ return name
+ else:
+ return "{} :]".format(name)
+ return ""
+
+ @property
+ def sender(self):
+ return self.get_sender(False)
+
+ @property
+ def sender_plain(self):
+ return self.get_sender(True)
def get_reaction(self, reaction_name):
for reaction in self.message_json.get("reactions", []):
@@ -2561,27 +2798,44 @@ class SlackMessage(object):
def number_of_replies(self):
return max(len(self.submessages), self.message_json.get("reply_count", 0))
- def notify_thread(self, action=None, sender_id=None):
+ def notify_thread(self, message=None):
+ if message is None:
+ if not self.submessages:
+ return
+ message = self.channel.messages.get(self.submessages[-1])
+
+ if (self.thread_channel and self.thread_channel.active or
+ message.ts <= self.last_read or message.ts <= self.last_notify):
+ return
+
+ if message.has_mention():
+ template = "You were mentioned in thread {hash}, channel {channel}"
+ elif self.subscribed:
+ template = "New message in thread {hash}, channel {channel} to which you are subscribed"
+ else:
+ return
+
+ self.last_notify = max(message.ts, SlackTS())
+
if config.auto_open_threads:
self.open_thread()
- if sender_id != self.team.myidentifier and (config.notify_subscribed_threads == True or
+
+ if message.user_identifier != self.team.myidentifier and (config.notify_subscribed_threads == True or
config.notify_subscribed_threads == "auto" and not config.auto_open_threads and
not config.thread_messages_in_channel):
- if action == "mention":
- template = "You were mentioned in thread {hash}, channel {channel}"
- elif action == "subscribed":
- template = "New message in thread {hash}, channel {channel} to which you are subscribed"
- else:
- template = "Notification for message in thread {hash}, channel {channel}"
message = template.format(hash=self.hash, channel=self.channel.formatted_name())
-
self.team.buffer_prnt(message, message=True)
class SlackThreadMessage(SlackMessage):
- def __init__(self, parent_message, *args):
- super(SlackThreadMessage, self).__init__(*args)
- self.parent_message = parent_message
+ def __init__(self, parent_channel, thread_ts, message_json, *args):
+ super(SlackThreadMessage, self).__init__(message_json['subtype'], message_json, *args)
+ self.parent_channel = parent_channel
+ self.thread_ts = thread_ts
+
+ @property
+ def parent_message(self):
+ return self.parent_channel.messages.get(self.thread_ts)
class Hdata(object):
@@ -2595,7 +2849,10 @@ class Hdata(object):
class SlackTS(object):
def __init__(self, ts=None):
- if ts:
+ if isinstance(ts, int):
+ self.major = ts
+ self.minor = 0
+ elif ts is not None:
self.major, self.minor = [int(x) for x in ts.split('.', 1)]
else:
self.major = int(time.time())
@@ -2742,7 +2999,7 @@ def handle_rtmstart(login_data, eventrouter, team, channel, metadata):
token_for_print(metadata.token), self_nick)
)
return
- elif metadata.metadata.get('initial_connection'):
+ elif not metadata.metadata.get('reconnect'):
print_error(
'Ignoring duplicate Slack tokens for the same team ({}) and user ({}). The two '
'tokens are {} and {}.'.format(t.team_info["name"], t.nick,
@@ -2754,7 +3011,7 @@ def handle_rtmstart(login_data, eventrouter, team, channel, metadata):
t.set_reconnect_url(login_data['url'])
t.connecting_rtm = False
- t.connect()
+ t.connect(metadata.metadata['reconnect'])
def handle_rtmconnect(login_data, eventrouter, team, channel, metadata):
metadata = login_data["wee_slack_request_metadata"]
@@ -2767,7 +3024,7 @@ def handle_rtmconnect(login_data, eventrouter, team, channel, metadata):
return
team.set_reconnect_url(login_data['url'])
- team.connect()
+ team.connect(metadata.metadata['reconnect'])
def handle_emojilist(emoji_json, eventrouter, team, channel, metadata):
@@ -2796,24 +3053,42 @@ def handle_mpimopen(mpim_json, eventrouter, team, channel, metadata, object_name
handle_conversationsopen(mpim_json, eventrouter, team, channel, metadata, object_name)
-def handle_history(message_json, eventrouter, team, channel, metadata):
- if metadata['clear']:
- channel.clear_messages()
+def handle_history(message_json, eventrouter, team, channel, metadata, includes_threads=True):
channel.got_history = True
for message in reversed(message_json["messages"]):
- process_message(message, eventrouter, team, channel, metadata, history_message=True)
+ message = process_message(message, eventrouter, team, channel, metadata, history_message=True)
+ if (not includes_threads and message and message.number_of_replies() and
+ (config.thread_messages_in_channel or message.subscribed and
+ SlackTS(message.message_json.get("latest_reply", 0)) > message.last_read)):
+ channel.get_thread_history(message.ts, metadata["slow_queue"], metadata["no_log"])
+
+ channel.pending_history_requests.discard(channel.identifier)
+ if channel.visible_messages.first_ts_to_display.major == 0 and message_json["messages"]:
+ channel.visible_messages.first_ts_to_display = SlackTS(message_json["messages"][-1]["ts"])
+ channel.reprint_messages(history_message=True, no_log=metadata["no_log"])
+ for thread_channel in channel.thread_channels.values():
+ thread_channel.reprint_messages(history_message=True, no_log=metadata["no_log"])
handle_channelshistory = handle_history
-handle_conversationshistory = handle_history
handle_groupshistory = handle_history
handle_imhistory = handle_history
handle_mpimhistory = handle_history
+def handle_conversationshistory(message_json, eventrouter, team, channel, metadata, includes_threads=True):
+ handle_history(message_json, eventrouter, team, channel, metadata, False)
+
+
def handle_conversationsreplies(message_json, eventrouter, team, channel, metadata):
for message in message_json['messages']:
- process_message(message, eventrouter, team, channel, metadata)
+ process_message(message, eventrouter, team, channel, metadata, history_message=True)
+ channel.pending_history_requests.discard(metadata.get('thread_ts'))
+ thread_channel = channel.thread_channels.get(metadata.get('thread_ts'))
+ if thread_channel and thread_channel.active:
+ thread_channel.reprint_messages(history_message=True, no_log=metadata["no_log"])
+ if config.thread_messages_in_channel:
+ channel.reprint_messages(history_message=True, no_log=metadata["no_log"])
def handle_conversationsmembers(members_json, eventrouter, team, channel, metadata):
@@ -2977,7 +3252,7 @@ def process_pong(message_json, eventrouter, team, channel, metadata):
def process_message(message_json, eventrouter, team, channel, metadata, history_message=False):
- if "ts" in message_json and SlackTS(message_json["ts"]) in channel.messages:
+ if not history_message and "ts" in message_json and SlackTS(message_json["ts"]) in channel.messages:
return
if "thread_ts" in message_json and "reply_count" not in message_json and "subtype" not in message_json:
@@ -2990,27 +3265,20 @@ def process_message(message_json, eventrouter, team, channel, metadata, history_
subtype_functions = get_functions_with_prefix("subprocess_")
if subtype in subtype_functions:
- subtype_functions[subtype](message_json, eventrouter, team, channel, history_message)
+ message = subtype_functions[subtype](message_json, eventrouter, team, channel, history_message)
else:
- message = SlackMessage(message_json, team, channel)
- channel.store_message(message, team)
-
- text = channel.render(message)
- dbg("Rendered message: %s" % text)
- dbg("Sender: %s (%s)" % (message.sender, message.sender_plain))
-
- if subtype == 'me_message':
- prefix = w.prefix("action").rstrip()
- else:
- prefix = message.sender
-
- channel.buffer_prnt(prefix, text, message.ts, tag_nick=message.sender_plain, history_message=history_message)
+ message = SlackMessage(subtype or "normal", message_json, team, channel)
+ channel.store_message(message)
channel.unread_count_display += 1
- dbg("NORMAL REPLY {}".format(message_json))
+
+ if message and not history_message:
+ channel.prnt_message(message, history_message)
if not history_message:
download_files(message_json, team)
+ return message
+
def download_files(message_json, team):
download_location = config.files_download_location
@@ -3051,62 +3319,51 @@ def download_files(message_json, team):
def subprocess_thread_message(message_json, eventrouter, team, channel, history_message):
- dbg("THREAD MESSAGE {}".format(message_json))
- parent_ts = message_json.get('thread_ts')
- if parent_ts:
- parent_message = channel.messages.get(SlackTS(parent_ts))
- if parent_message:
- message = SlackThreadMessage(
- parent_message, message_json, team, channel)
- parent_message.submessages.append(message)
- channel.hash_message(parent_ts)
- channel.store_message(message, team)
- channel.change_message(parent_ts)
-
- if parent_message.thread_channel and parent_message.thread_channel.active:
- parent_message.thread_channel.buffer_prnt(message.sender, parent_message.thread_channel.render(message), message.ts, history_message=history_message, tag_nick=message.sender_plain)
- elif message.ts > channel.last_read and message.has_mention():
- parent_message.notify_thread(action="mention", sender_id=message_json["user"])
- elif message.ts > parent_message.last_read and parent_message.subscribed:
- parent_message.notify_thread(action="subscribed", sender_id=message_json["user"])
-
- if config.thread_messages_in_channel or message_json["subtype"] == "thread_broadcast":
- thread_tag = "thread_broadcast" if message_json["subtype"] == "thread_broadcast" else "thread_message"
- channel.buffer_prnt(
- message.sender,
- channel.render(message),
- message.ts,
- tag_nick=message.sender_plain,
- history_message=history_message,
- extra_tags=[thread_tag],
- )
+ parent_ts = SlackTS(message_json['thread_ts'])
+ message = SlackThreadMessage(channel, parent_ts, message_json, team, channel)
+
+ parent_message = message.parent_message
+ if parent_message and message.ts not in parent_message.submessages:
+ parent_message.submessages.append(message.ts)
+ parent_message.submessages.sort()
+
+ channel.store_message(message)
+
+ if parent_message:
+ channel.change_message(parent_ts)
+ if parent_message.thread_channel and parent_message.thread_channel.active:
+ if not history_message:
+ parent_message.thread_channel.prnt_message(message, history_message)
+ else:
+ parent_message.notify_thread(message)
+ else:
+ channel.get_thread_history(parent_ts)
+
+ return message
subprocess_thread_broadcast = subprocess_thread_message
def subprocess_channel_join(message_json, eventrouter, team, channel, history_message):
- prefix_join = w.prefix("join").strip()
- message = SlackMessage(message_json, team, channel, override_sender=prefix_join)
- channel.buffer_prnt(prefix_join, channel.render(message), message_json["ts"], tagset='join', tag_nick=message.get_sender()[1], history_message=history_message)
- channel.user_joined(message_json['user'])
- channel.store_message(message, team)
+ message = SlackMessage("join", message_json, team, channel)
+ channel.store_message(message)
+ channel.user_joined(message_json["user"])
+ return message
def subprocess_channel_leave(message_json, eventrouter, team, channel, history_message):
- prefix_leave = w.prefix("quit").strip()
- message = SlackMessage(message_json, team, channel, override_sender=prefix_leave)
- channel.buffer_prnt(prefix_leave, channel.render(message), message_json["ts"], tagset='leave', tag_nick=message.get_sender()[1], history_message=history_message)
- channel.user_left(message_json['user'])
- channel.store_message(message, team)
+ message = SlackMessage("leave", message_json, team, channel)
+ channel.store_message(message)
+ channel.user_left(message_json["user"])
+ return message
def subprocess_channel_topic(message_json, eventrouter, team, channel, history_message):
- prefix_topic = w.prefix("network").strip()
- message = SlackMessage(message_json, team, channel, override_sender=prefix_topic)
- channel.buffer_prnt(prefix_topic, channel.render(message), message_json["ts"], tagset="topic", tag_nick=message.get_sender()[1], history_message=history_message)
+ message = SlackMessage("topic", message_json, team, channel)
+ channel.store_message(message)
channel.set_topic(message_json["topic"])
- channel.store_message(message, team)
+ return message
subprocess_group_join = subprocess_channel_join
@@ -3267,17 +3524,25 @@ def process_emoji_changed(message_json, eventrouter, team, channel, metadata):
def process_thread_subscribed(message_json, eventrouter, team, channel, metadata):
dbg("THREAD SUBSCRIBED {}".format(message_json))
channel = team.channels[message_json["subscription"]["channel"]]
- parent_ts = message_json["subscription"]["thread_ts"]
- channel.messages.get(SlackTS(parent_ts)).subscribed = True
- channel.change_message(parent_ts)
+ parent_ts = SlackTS(message_json["subscription"]["thread_ts"])
+ parent_message = channel.messages.get(parent_ts)
+ if parent_message:
+ parent_message.last_read = SlackTS(message_json["subscription"]["last_read"])
+ parent_message.subscribed = True
+ channel.change_message(parent_ts)
+ parent_message.notify_thread()
+ else:
+ channel.get_thread_history(parent_ts)
def process_thread_unsubscribed(message_json, eventrouter, team, channel, metadata):
dbg("THREAD UNSUBSCRIBED {}".format(message_json))
channel = team.channels[message_json["subscription"]["channel"]]
- parent_ts = message_json["subscription"]["thread_ts"]
- channel.messages.get(SlackTS(parent_ts)).subscribed = False
- channel.change_message(parent_ts)
+ parent_ts = SlackTS(message_json["subscription"]["thread_ts"])
+ parent_message = channel.messages.get(parent_ts)
+ if parent_message:
+ parent_message.subscribed = False
+ channel.change_message(parent_ts)
###### New module/global methods
@@ -4172,13 +4437,6 @@ def command_showmuted(data, current_buffer, args):
return w.WEECHAT_RC_OK_EAT
-def get_msg_from_id(channel, msg_id):
- if msg_id[0] == '$':
- msg_id = msg_id[1:]
- ts = channel.hashed_messages.get(msg_id)
- return channel.messages.get(ts)
-
-
@slack_buffer_required
@utf8_decode
def command_thread(data, current_buffer, args):
@@ -4193,21 +4451,17 @@ def command_thread(data, current_buffer, args):
print_error('/thread can not be used in the team buffer, only in a channel')
return w.WEECHAT_RC_ERROR
- if args:
- msg = get_msg_from_id(channel, args)
- if not msg:
- w.prnt('', 'ERROR: Invalid id given, must be an existing id')
- return w.WEECHAT_RC_OK_EAT
+ message_filter = lambda message: message.number_of_replies()
+ message = channel.message_from_hash_or_index(args, message_filter)
+
+ if message:
+ message.open_thread(switch=config.switch_buffer_on_join)
+ elif args:
+ print_error("Invalid id given, must be an existing id or a number greater " +
+ "than 0 and less than the number of thread messages in the channel")
else:
- for message in reversed(channel.messages.values()):
- if type(message) == SlackMessage and message.number_of_replies():
- msg = message
- break
- else:
- w.prnt('', 'ERROR: No threads found in channel')
- return w.WEECHAT_RC_OK_EAT
+ print_error("No threads found in channel")
- msg.open_thread(switch=config.switch_buffer_on_join)
return w.WEECHAT_RC_OK_EAT
command_thread.completion = '%(threads) %-'
@@ -4217,16 +4471,19 @@ def subscribe_helper(current_buffer, args, usage, api):
channel = EVENTROUTER.weechat_controller.buffers[current_buffer]
team = channel.team
- if args:
- msg = get_msg_from_id(channel, args)
- elif isinstance(channel, SlackThreadChannel):
- msg = channel.parent_message
+ if isinstance(channel, SlackThreadChannel) and not args:
+ message = channel.parent_message
else:
- w.prnt('', usage)
- return w.WEECHAT_RC_ERROR
+ message_filter = lambda message: message.number_of_replies()
+ message = channel.message_from_hash_or_index(args, message_filter)
- s = SlackRequest(team, api,
- {"channel": channel.identifier, "thread_ts": msg.ts}, channel=channel)
+ if not message:
+ print_message_not_found_error(args)
+ return w.WEECHAT_RC_OK_EAT
+
+ last_read = next(reversed(message.submessages), message.ts)
+ post_data = {"channel": channel.identifier, "thread_ts": message.ts, "last_read": last_read}
+ s = SlackRequest(team, api, post_data, channel=channel)
EVENTROUTER.receive(s)
return w.WEECHAT_RC_OK_EAT
@@ -4290,26 +4547,23 @@ def command_reply(data, current_buffer, args):
if isinstance(channel, SlackThreadChannel):
text = args
- msg = channel.parent_message
+ message = channel.parent_message
else:
try:
msg_id, text = args.split(None, 1)
except ValueError:
w.prnt('', 'Usage (when in a channel buffer): /reply [-alsochannel] <count/message_id> <message>')
return w.WEECHAT_RC_OK_EAT
- msg = get_msg_from_id(channel, msg_id)
+ message = channel.message_from_hash_or_index(msg_id)
- if msg:
- if isinstance(msg, SlackThreadMessage):
- parent_id = str(msg.parent_message.ts)
- else:
- parent_id = str(msg.ts)
- elif msg_id.isdigit() and int(msg_id) >= 1:
- mkeys = channel.main_message_keys_reversed()
- parent_id = str(next(islice(mkeys, int(msg_id) - 1, None)))
- else:
- w.prnt('', 'ERROR: Invalid id given, must be a number greater than 0 or an existing id')
- return w.WEECHAT_RC_OK_EAT
+ if not message:
+ print_message_not_found_error(args)
+ return w.WEECHAT_RC_OK_EAT
+
+ if isinstance(message, SlackThreadMessage):
+ parent_id = str(message.parent_message.ts)
+ elif message:
+ parent_id = str(message.ts)
channel.send_message(text, request_dict_ext={'thread_ts': parent_id, 'reply_broadcast': broadcast})
return w.WEECHAT_RC_OK_EAT
@@ -4321,14 +4575,19 @@ command_reply.completion = '%(threads)|-alsochannel %(threads)'
@utf8_decode
def command_rehistory(data, current_buffer, args):
"""
- /rehistory
+ /rehistory [-remote]
Reload the history in the current channel.
+ With -remote the history will be downloaded again from Slack.
"""
channel = EVENTROUTER.weechat_controller.buffers[current_buffer]
- channel.clear_messages()
- channel.get_history()
+ if args == "-remote":
+ channel.get_history(full=True, no_log=True)
+ else:
+ channel.reprint_messages(force_render=True)
return w.WEECHAT_RC_OK_EAT
+command_rehistory.completion = '-remote'
+
@slack_buffer_required
@utf8_decode
@@ -4454,18 +4713,13 @@ def command_linkarchive(data, current_buffer, args):
if isinstance(channel, SlackChannelCommon):
url += 'archives/{}/'.format(channel.identifier)
if args:
- if args[0] == '$':
- message_id = args[1:]
- else:
- message_id = args
- ts = channel.hashed_messages.get(message_id)
- message = channel.messages.get(ts)
+ message = channel.message_from_hash_or_index(args)
if message:
url += 'p{}{:0>6}'.format(message.ts.majorstr(), message.ts.minorstr())
if isinstance(message, SlackThreadMessage):
url += "?thread_ts={}&cid={}".format(message.parent_message.ts, channel.identifier)
else:
- w.prnt('', 'ERROR: Invalid id given, must be an existing id')
+ print_message_not_found_error(args)
return w.WEECHAT_RC_OK_EAT
w.command(current_buffer, "/input insert {}".format(url))
@@ -4516,7 +4770,7 @@ def command_upload(data, current_buffer, args):
'channels': channel.identifier,
}
if isinstance(channel, SlackThreadChannel):
- post_data['thread_ts'] = channel.parent_message.ts
+ post_data['thread_ts'] = channel.thread_ts
url = SlackRequest(channel.team, 'files.upload', post_data, channel=channel).request_string()
options = [
@@ -4623,7 +4877,7 @@ def line_event_cb(data, signal, hashtable):
if line_timestamp and line_time_id and isinstance(channel, SlackChannelCommon):
ts = SlackTS("{}.{}".format(line_timestamp, line_time_id))
- message_hash = channel.hash_message(ts)
+ message_hash = channel.hashed_messages[ts]
if message_hash is None:
return w.WEECHAT_RC_OK
message_hash = "$" + message_hash
@@ -4632,7 +4886,7 @@ def line_event_cb(data, signal, hashtable):
reaction = EMOJI_CHAR_OR_NAME_REGEX.match(hashtable["_chat_eol"])
if reaction:
emoji = reaction.group("emoji_char") or reaction.group("emoji_name")
- channel.send_change_reaction("toggle", message_hash[1:], emoji)
+ channel.send_change_reaction("toggle", message_hash, emoji)
else:
data = "message"
if data == "message":
@@ -4642,7 +4896,7 @@ def line_event_cb(data, signal, hashtable):
w.command(buffer_pointer, "/input send {}s///".format(message_hash))
elif data == "linkarchive":
w.command(buffer_pointer, "/cursor stop")
- w.command(buffer_pointer, "/slack linkarchive {}".format(message_hash[1:]))
+ w.command(buffer_pointer, "/slack linkarchive {}".format(message_hash))
elif data == "reply":
w.command(buffer_pointer, "/cursor stop")
w.command(buffer_pointer, "/input insert /reply {}\\x20".format(message_hash))
@@ -4881,9 +5135,11 @@ class PluginConfig(object):
desc='Automatically open threads when mentioned or in'
'response to own messages.'),
'background_load_all_history': Setting(
- default='false',
- desc='Load history for each channel in the background as soon as it'
- ' opens, rather than waiting for the user to look at it.'),
+ default='true',
+ desc='Load the history for all channels in the background when the script is loaded,'
+ ' rather than waiting until the buffer is switched to. You can set this to false if'
+ ' you experience performance issues, however that causes some loss of functionality,'
+ ' see known issues in the readme.'),
'channel_name_typing_indicator': Setting(
default='true',
desc='Change the prefix of a channel from # to > when someone is'
@@ -4938,6 +5194,10 @@ class PluginConfig(object):
'group_name_prefix': Setting(
default='&',
desc='The prefix of buffer names for groups (private channels).'),
+ 'history_fetch_count': Setting(
+ default='200',
+ desc='The number of messages to fetch for each channel when fetching'
+ ' history, between 1 and 1000.'),
'map_underline_to': Setting(
default='_',
desc='When sending underlined text to slack, use this formatting'
@@ -5118,6 +5378,7 @@ class PluginConfig(object):
get_external_user_suffix = get_string
get_files_download_location = get_string
get_group_name_prefix = get_string
+ get_history_fetch_count = get_int
get_map_underline_to = get_string
get_muted_channels_activity = get_string
get_render_bold_as = get_string
@@ -5206,13 +5467,13 @@ def trace_calls(frame, event, arg):
return
-def initiate_connection(token, retries=3, team=None):
+def initiate_connection(token, retries=3, team=None, reconnect=False):
return SlackRequest(team,
'rtm.{}'.format('connect' if team else 'start'),
{"batch_presence_aware": 1},
retries=retries,
token=token,
- metadata={'initial_connection': True})
+ metadata={'reconnect': reconnect})
if __name__ == "__main__":