diff options
Diffstat (limited to 'wee_slack.py')
-rw-r--r-- | wee_slack.py | 688 |
1 files changed, 421 insertions, 267 deletions
diff --git a/wee_slack.py b/wee_slack.py index 94dce4c..16d5569 100644 --- a/wee_slack.py +++ b/wee_slack.py @@ -2,7 +2,9 @@ from __future__ import unicode_literals +from collections import OrderedDict from functools import wraps +from itertools import islice import time import json @@ -40,21 +42,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 +104,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" @@ -115,7 +135,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: @@ -128,7 +148,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: @@ -139,19 +159,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 @@ -561,17 +593,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 @@ -583,11 +616,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 @@ -596,11 +631,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 @@ -608,13 +643,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) == "+": @@ -634,11 +668,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 @@ -646,7 +696,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() @@ -664,6 +713,7 @@ def buffer_switch_callback(signal, sig_type, data): return w.WEECHAT_RC_OK +@utf8_decode def buffer_list_update_callback(data, somecount): """ incomplete @@ -673,7 +723,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 @@ -692,8 +741,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 @@ -709,14 +758,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'] @@ -726,6 +775,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 @@ -760,13 +810,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) @@ -779,13 +828,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) @@ -796,6 +844,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 @@ -804,8 +853,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) @@ -987,9 +1034,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(',')} @@ -1004,7 +1051,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()} @@ -1035,7 +1082,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): @@ -1051,7 +1098,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: @@ -1116,14 +1163,14 @@ 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 self.team = kwargs.get('team', None) self.got_history = False - self.messages = {} + self.messages = OrderedDict() self.hashed_messages = {} self.new_messages = False self.typing = {} @@ -1158,8 +1205,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") @@ -1167,11 +1218,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 = { @@ -1183,15 +1233,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) @@ -1199,7 +1252,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 @@ -1207,22 +1260,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 @@ -1230,7 +1281,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) @@ -1267,13 +1318,13 @@ 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): if self.channel_buffer is not None: self.channel_buffer = None - self.messages = {} + self.messages = OrderedDict() self.hashed_messages = {} self.got_history = False # if update_remote and not eventrouter.shutting_down: @@ -1335,14 +1386,14 @@ class SlackChannel(object): if from_me: message.message_json["user"] = team.myidentifier self.messages[SlackTS(message.ts)] = message - if len(self.messages.keys()) > SCROLLBACK_SIZE: - mk = self.messages.keys() - mk.sort() - for k in mk[:SCROLLBACK_SIZE]: - msg_to_delete = self.messages[k] - if msg_to_delete.hash: - del self.hashed_messages[msg_to_delete.hash] - del self.messages[k] + + 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: + del self.hashed_messages[message_hash] + self.messages = OrderedDict(messages_to_keep) def change_message(self, ts, text=None, suffix=None): ts = SlackTS(ts) @@ -1371,8 +1422,8 @@ class SlackChannel(object): self.eventrouter.receive(s) def my_last_message(self, msgno): - for message in reversed(self.sorted_message_keys()): - m = self.messages[message] + for key in self.main_message_keys_reversed(): + m = self.messages[key] if "user" in m.message_json and "text" in m.message_json and m.message_json["user"] == self.team.myidentifier: msgno -= 1 if msgno == 0: @@ -1402,17 +1453,15 @@ class SlackChannel(object): def send_change_reaction(self, method, msg_number, reaction): if 0 < msg_number < len(self.messages): - timestamp = self.sorted_message_keys()[-msg_number] + keys = self.main_message_keys_reversed() + timestamp = next(islice(keys, msg_number - 1, None)) data = {"channel": self.identifier, "timestamp": timestamp, "name": reaction} s = SlackRequest(self.team.token, method, data) self.eventrouter.receive(s) - def sorted_message_keys(self): - keys = [] - for k in self.messages: - if type(self.messages[k]) == SlackMessage: - keys.append(k) - return sorted(keys) + def main_message_keys_reversed(self): + return (key for key in reversed(self.messages) + if type(self.messages[key]) == SlackMessage) # Typing related def set_typing(self, user): @@ -1454,7 +1503,7 @@ class SlackChannel(object): def mark_read(self, ts=None, update_remote=True, force=False): if not ts: - ts = SlackTS() + ts = next(self.main_message_keys_reversed(), SlackTS()) if self.new_messages or force: if self.channel_buffer: w.buffer_set(self.channel_buffer, "unread", "") @@ -1477,7 +1526,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 @@ -1514,7 +1563,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"]: @@ -1565,6 +1614,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() @@ -1605,7 +1657,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() @@ -1629,12 +1681,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 @@ -1650,29 +1701,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, @@ -2040,79 +2095,67 @@ def handle_rtmstart(login_data, eventrouter): """ This handles the main entry call to slack, rtm.start """ - if login_data["ok"]: - - metadata = pickle.loads(login_data["wee_slack_request_metadata"]) - - # Let's reuse a team if we have it already. - th = SlackTeam.generate_team_hash(login_data['self']['name'], login_data['team']['domain']) - if not eventrouter.teams.get(th): + metadata = pickle.loads(login_data["wee_slack_request_metadata"]) - users = {} - for item in login_data["users"]: - users[item["id"]] = SlackUser(**item) - # users.append(SlackUser(**item)) + if not login_data["ok"]: + w.prnt("", "ERROR: Failed connecting to Slack with token {}: {}" + .format(metadata.token, login_data["error"])) + return - bots = {} - for item in login_data["bots"]: - bots[item["id"]] = SlackBot(**item) + # Let's reuse a team if we have it already. + th = SlackTeam.generate_team_hash(login_data['self']['name'], login_data['team']['domain']) + if not eventrouter.teams.get(th): - channels = {} - for item in login_data["channels"]: - channels[item["id"]] = SlackChannel(eventrouter, **item) + users = {} + for item in login_data["users"]: + users[item["id"]] = SlackUser(**item) - for item in login_data["ims"]: - channels[item["id"]] = SlackDMChannel(eventrouter, users, **item) + bots = {} + for item in login_data["bots"]: + bots[item["id"]] = SlackBot(**item) - for item in login_data["groups"]: - if item["name"].startswith('mpdm-'): - channels[item["id"]] = SlackMPDMChannel(eventrouter, **item) - else: - channels[item["id"]] = SlackGroupChannel(eventrouter, **item) - - t = SlackTeam( - eventrouter, - metadata.token, - login_data['url'], - login_data["team"]["domain"], - login_data["self"]["name"], - login_data["self"]["id"], - users, - bots, - channels, - muted_channels=login_data["self"]["prefs"]["muted_channels"], - highlight_words=login_data["self"]["prefs"]["highlight_words"], - ) - eventrouter.register_team(t) + channels = {} + for item in login_data["channels"]: + channels[item["id"]] = SlackChannel(eventrouter, **item) - else: - t = eventrouter.teams.get(th) - t.set_reconnect_url(login_data['url']) - t.connect() + for item in login_data["ims"]: + channels[item["id"]] = SlackDMChannel(eventrouter, users, **item) - # web_socket_url = login_data['url'] - # try: - # ws = create_connection(web_socket_url, sslopt=sslopt_ca_certs) - # w.hook_fd(ws.sock._sock.fileno(), 1, 0, 0, "receive_ws_callback", t.get_team_hash()) - # #ws_hook = w.hook_fd(ws.sock._sock.fileno(), 1, 0, 0, "receive_ws_callback", pickle.dumps(t)) - # ws.sock.setblocking(0) - # t.attach_websocket(ws) - # t.set_connected() - # except Exception as e: - # dbg("websocket connection error: {}".format(e)) - # return False + for item in login_data["groups"]: + if item["name"].startswith('mpdm-'): + channels[item["id"]] = SlackMPDMChannel(eventrouter, **item) + else: + channels[item["id"]] = SlackGroupChannel(eventrouter, **item) + + t = SlackTeam( + eventrouter, + metadata.token, + login_data['url'], + login_data["team"]["domain"], + login_data["self"]["name"], + login_data["self"]["id"], + users, + bots, + channels, + muted_channels=login_data["self"]["prefs"]["muted_channels"], + highlight_words=login_data["self"]["prefs"]["highlight_words"], + ) + eventrouter.register_team(t) - t.buffer_prnt('Connected to Slack') - t.buffer_prnt('{:<20} {}'.format("Websocket URL", login_data["url"])) - t.buffer_prnt('{:<20} {}'.format("User name", login_data["self"]["name"])) - t.buffer_prnt('{:<20} {}'.format("User ID", login_data["self"]["id"])) - t.buffer_prnt('{:<20} {}'.format("Team name", login_data["team"]["name"])) - t.buffer_prnt('{:<20} {}'.format("Team domain", login_data["team"]["domain"])) - t.buffer_prnt('{:<20} {}'.format("Team id", login_data["team"]["id"])) + else: + t = eventrouter.teams.get(th) + t.set_reconnect_url(login_data['url']) + t.connect() - dbg("connected to {}".format(t.domain)) + t.buffer_prnt('Connected to Slack') + t.buffer_prnt('{:<20} {}'.format("Websocket URL", login_data["url"])) + t.buffer_prnt('{:<20} {}'.format("User name", login_data["self"]["name"])) + t.buffer_prnt('{:<20} {}'.format("User ID", login_data["self"]["id"])) + t.buffer_prnt('{:<20} {}'.format("Team name", login_data["team"]["name"])) + t.buffer_prnt('{:<20} {}'.format("Team domain", login_data["team"]["domain"])) + t.buffer_prnt('{:<20} {}'.format("Team id", login_data["team"]["id"])) - # self.identifier = self.domain + dbg("connected to {}".format(t.domain)) def handle_emojilist(emoji_json, eventrouter, **kwargs): @@ -2137,13 +2180,21 @@ 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) @@ -2156,6 +2207,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] @@ -2350,8 +2405,7 @@ def subprocess_message_changed(message_json, eventrouter, channel, team): else: message_json["fallback"] = m["fallback"] - text_before = (len(new_message['text']) > 0) - new_message["text"] += unwrap_attachments(message_json, text_before) + new_message["text"] += unwrap_attachments(message_json, new_message["text"]) if "edited" in new_message: channel.change_message(new_message["ts"], new_message["text"], ' (edited)') else: @@ -2365,7 +2419,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): @@ -2533,10 +2587,9 @@ def render(message_json, team, channel, force=False): else: text = "" - text = unfurl_refs(text, ignore_alt_text=config.unfurl_ignore_alt_text) + text = unfurl_refs(text) - text_before = (len(text) > 0) - text += unfurl_refs(unwrap_attachments(message_json, text_before), ignore_alt_text=config.unfurl_ignore_alt_text) + text += unfurl_refs(unwrap_attachments(message_json, text)) text = text.lstrip() text = unhtmlescape(text.replace("\t", " ")) @@ -2593,7 +2646,7 @@ def linkify_text(message, team, channel): return " ".join(message) -def unfurl_refs(text, ignore_alt_text=False): +def unfurl_refs(text, ignore_alt_text=None, auto_link_display=None): """ input : <@U096Q7CQM|someuser> has joined the channel ouput : someuser has joined the channel @@ -2603,14 +2656,21 @@ def unfurl_refs(text, ignore_alt_text=False): # - <#C2147483705|#otherchannel> # - <@U2147483697|@othernick> # Test patterns lives in ./_pytest/test_unfurl.py - matches = re.findall(r"(<[@#]?(?:[^<]*)>)", text) + + if ignore_alt_text is None: + ignore_alt_text = config.unfurl_ignore_alt_text + if auto_link_display is None: + auto_link_display = config.unfurl_auto_link_display + + matches = re.findall(r"(<[@#]?(?:[^>]*)>)", text) for m in matches: # Replace them with human readable strings - text = text.replace(m, unfurl_ref(m[1:-1], ignore_alt_text)) + text = text.replace( + m, unfurl_ref(m[1:-1], ignore_alt_text, auto_link_display)) return text -def unfurl_ref(ref, ignore_alt_text=False): +def unfurl_ref(ref, ignore_alt_text, auto_link_display): id = ref.split('|')[0] display_text = ref if ref.find('|') > -1: @@ -2623,7 +2683,14 @@ def unfurl_ref(ref, ignore_alt_text=False): display_text = ref.split('|')[1] else: url, desc = ref.split('|', 1) - display_text = "{} ({})".format(url, desc) + match_url = r"^\w+:(//)?{}$".format(re.escape(desc)) + url_matches_desc = re.match(match_url, url) + if url_matches_desc and auto_link_display == "text": + display_text = desc + elif url_matches_desc and auto_link_display == "url": + display_text = url + else: + display_text = "{} ({})".format(url, desc) else: display_text = resolve_ref(ref) return display_text @@ -2636,11 +2703,12 @@ def unhtmlescape(text): def unwrap_attachments(message_json, text_before): - attachment_text = '' + text_before_unescaped = unhtmlescape(text_before) + attachment_texts = [] a = message_json.get("attachments", None) if a: if text_before: - attachment_text = '\n' + attachment_texts.append('') for attachment in a: # Attachments should be rendered roughly like: # @@ -2655,14 +2723,18 @@ def unwrap_attachments(message_json, text_before): if 'pretext' in attachment: t.append(attachment['pretext']) title = attachment.get('title', None) - title_link = attachment.get('title_link', None) + title_link = attachment.get('title_link', '') + if title_link in text_before_unescaped: + title_link = '' if title and title_link: t.append('%s%s (%s)' % (prepend_title_text, title, title_link,)) prepend_title_text = '' elif title and not title_link: - t.append(prepend_title_text + title) + t.append('%s%s' % (prepend_title_text, title,)) prepend_title_text = '' - t.append(attachment.get("from_url", "")) + from_url = attachment.get('from_url', '') + if from_url not in text_before_unescaped and from_url != title_link: + t.append(from_url) atext = attachment.get("text", None) if atext: @@ -2679,8 +2751,8 @@ def unwrap_attachments(message_json, text_before): fallback = attachment.get("fallback", None) if t == [] and fallback: t.append(fallback) - attachment_text += "\n".join([x.strip() for x in t if x]) - return attachment_text + attachment_texts.append("\n".join([x.strip() for x in t if x])) + return "\n".join(attachment_texts) def resolve_ref(ref): @@ -2819,20 +2891,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] @@ -2840,9 +2912,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: @@ -2857,64 +2928,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] @@ -2933,32 +3007,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") @@ -2971,9 +3093,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: @@ -2988,22 +3109,22 @@ 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]) msg = " ".join(args[2:]) - mkeys = channel.sorted_message_keys() - mkeys.reverse() - parent_id = str(mkeys[count - 1]) + mkeys = channel.main_message_keys_reversed() + parent_id = str(next(islice(mkeys, count - 1, None))) channel.send_message(msg, request_dict_ext={"thread_ts": parent_id}) return w.WEECHAT_RC_OK_EAT w.prnt(current, "Invalid thread command.") return w.WEECHAT_RC_OK_EAT +@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 @@ -3013,9 +3134,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') @@ -3024,9 +3144,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 @@ -3135,9 +3254,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: @@ -3195,9 +3313,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) @@ -3206,6 +3323,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))) @@ -3273,7 +3405,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 @@ -3295,7 +3428,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', '') @@ -3303,6 +3436,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", "") @@ -3373,6 +3508,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' @@ -3391,6 +3529,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' @@ -3423,6 +3565,15 @@ class PluginConfig(object): desc='When displaying ("unfurling") links to channels/users/etc,' ' ignore the "alt text" present in the message and instead use the' ' canonical name of the thing being linked to.'), + 'unfurl_auto_link_display': Setting( + default='both', + desc='When displaying ("unfurling") links to channels/users/etc,' + ' determine what is displayed when the text matches the url' + ' without the protocol. This happens when Slack automatically' + ' creates links, e.g. from words separated by dots or email' + ' addresses. Set it to "text" to only display the text written by' + ' the user, "url" to only display the url or "both" (the default)' + ' to display both.'), 'unhide_buffers_with_activity': Setting( default='false', desc='When activity occurs on a buffer, unhide it even if it was' @@ -3483,11 +3634,13 @@ 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 get_slack_timeout = get_int get_thread_suffix_color = get_string + get_unfurl_auto_link_display = get_string def get_distracting_channels(self, key): return [x.strip() for x in w.config_get_plugin(key).split(',')] @@ -3583,6 +3736,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", "") EMOJI.extend(load_emoji()) setup_hooks() |