diff options
Diffstat (limited to 'wee_slack.py')
-rw-r--r-- | wee_slack.py | 510 |
1 files changed, 343 insertions, 167 deletions
diff --git a/wee_slack.py b/wee_slack.py index e4157bb..90b56ac 100644 --- a/wee_slack.py +++ b/wee_slack.py @@ -56,6 +56,7 @@ SLACK_API_TRANSLATOR = { "join": "channels.join", "leave": "groups.leave", "mark": "groups.mark", + "info": "groups.info" }, "thread": { "history": None, @@ -150,6 +151,13 @@ class WeechatWrapper(object): return decode_from_utf8(orig_attr) +##### Helpers + +def get_nick_color_name(nick): + info_name_prefix = "irc_" if int(weechat_version) < 0x1050000 else "" + return w.info_get(info_name_prefix + "nick_color_name", nick) + + ##### BEGIN NEW IGNORED_EVENTS = [ @@ -602,15 +610,17 @@ def buffer_input_callback(signal, buffer_ptr, data): eventrouter = eval(signal) channel = eventrouter.weechat_controller.get_channel_from_buffer_ptr(buffer_ptr) if not channel: - return w.WEECHAT_RC_OK_EAT + return w.WEECHAT_RC_ERROR reaction = re.match("^\s*(\d*)(\+|-):(.*):\s*$", data) + substitute = re.match("^(\d*)s/", data) if reaction: if reaction.group(2) == "+": channel.send_add_reaction(int(reaction.group(1) or 1), reaction.group(3)) elif reaction.group(2) == "-": channel.send_remove_reaction(int(reaction.group(1) or 1), reaction.group(3)) - elif data.startswith('s/'): + elif substitute: + msgno = int(substitute.group(1) or 1) try: old, new, flags = re.split(r'(?<!\\)/', data)[1:] except ValueError: @@ -620,11 +630,11 @@ def buffer_input_callback(signal, buffer_ptr, data): # rid of escapes. new = new.replace(r'\/', '/') old = old.replace(r'\/', '/') - channel.edit_previous_message(old, new, flags) + channel.edit_nth_previous_message(msgno, old, new, flags) else: channel.send_message(data) # this is probably wrong channel.mark_read(update_remote=True, force=True) - return w.WEECHAT_RC_ERROR + return w.WEECHAT_RC_OK def buffer_switch_callback(signal, sig_type, data): @@ -689,7 +699,7 @@ def typing_notification_cb(signal, sig_type, data): if typing_timer + 4 < now: current_buffer = w.current_buffer() channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer, None) - if channel: + if channel and channel.type != "thread": identifier = channel.identifier request = {"type": "typing", "channel": identifier} channel.team.send_to_websocket(request, expect_reply=False) @@ -967,6 +977,7 @@ class SlackTeam(object): self.channel_buffer = w.buffer_new("{}".format(self.preferred_name), "buffer_input_callback", "EVENTROUTER", "", "") 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) 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") @@ -1024,7 +1035,7 @@ class SlackTeam(object): if self.ws_url: try: ws = create_connection(self.ws_url, sslopt=sslopt_ca_certs) - w.hook_fd(ws.sock._sock.fileno(), 1, 0, 0, "receive_ws_callback", self.get_team_hash()) + self.hook = w.hook_fd(ws.sock._sock.fileno(), 1, 0, 0, "receive_ws_callback", self.get_team_hash()) ws.sock.setblocking(0) self.ws = ws # self.attach_websocket(ws) @@ -1048,6 +1059,7 @@ class SlackTeam(object): self.connected = True def set_disconnected(self): + w.unhook(self.hook) self.connected = False def set_reconnect_url(self, url): @@ -1072,6 +1084,14 @@ class SlackTeam(object): dbg("Unexpected error: {}\nSent: {}".format(sys.exc_info()[0], data)) self.set_connected() + def update_member_presence(self, user, presence): + user.presence = presence + + for c in self.channels: + c = self.channels[c] + if user.id in c.members: + c.update_nicklist(user.id) + class SlackChannel(object): """ @@ -1103,6 +1123,7 @@ class SlackChannel(object): # short name relates to the localvar we change for typing indication self.current_short_name = self.name self.update_nicklist() + self.unread_count_display = 0 def __eq__(self, compare_str): if compare_str == self.slack_name or compare_str == self.formatted_name() or compare_str == self.formatted_name(style="long_default"): @@ -1128,6 +1149,14 @@ class SlackChannel(object): return True return False + def set_unread_count_display(self, count): + self.unread_count_display = count + for c in range(self.unread_count_display): + if self.type == "im": + w.buffer_set(self.channel_buffer, "hotlist", "2") + else: + 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: @@ -1166,9 +1195,6 @@ class SlackChannel(object): 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) - self.eventrouter.receive(s) # self.create_buffer() def check_should_open(self, force=False): @@ -1213,6 +1239,7 @@ class SlackChannel(object): else: w.buffer_set(self.channel_buffer, "localvar_set_type", 'channel') w.buffer_set(self.channel_buffer, "localvar_set_channel", self.formatted_name()) + w.buffer_set(self.channel_buffer, "localvar_set_nick", self.team.nick) w.buffer_set(self.channel_buffer, "short_name", self.formatted_name(style="sidebar", enable_color=True)) self.render_topic() self.eventrouter.weechat_controller.set_refresh_buffer_list(True) @@ -1223,22 +1250,16 @@ class SlackChannel(object): w.buffer_set(self.channel_buffer, "localvar_set_server", self.team.preferred_name) # else: # self.eventrouter.weechat_controller.register_buffer(self.channel_buffer, self) - try: - for c in range(self.unread_count_display): - if self.type == "im": - w.buffer_set(self.channel_buffer, "hotlist", "2") - else: - w.buffer_set(self.channel_buffer, "hotlist", "1") - else: - pass - # dbg("no unread in {}".format(self.name)) - except: - pass - self.update_nicklist() - # dbg("exception no unread count") - # if self.unread_count != 0 and not self.muted: - # w.buffer_set(self.channel_buffer, "hotlist", "1") + + if "info" in SLACK_API_TRANSLATOR[self.type]: + 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 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) + self.eventrouter.receive(s) def destroy_buffer(self, update_remote): if self.channel_buffer is not None: @@ -1326,8 +1347,8 @@ class SlackChannel(object): modify_buffer_line(self.channel_buffer, text, ts.major, ts.minor) return True - def edit_previous_message(self, old, new, flags): - message = self.my_last_message() + def edit_nth_previous_message(self, n, old, new, flags): + message = self.my_last_message(n) if new == "" and old == "": s = SlackRequest(self.team.token, "chat.delete", {"channel": self.identifier, "ts": message['ts']}, team_hash=self.team.team_hash, channel_identifier=self.identifier) self.eventrouter.receive(s) @@ -1340,11 +1361,13 @@ class SlackChannel(object): s = SlackRequest(self.team.token, "chat.update", {"channel": self.identifier, "ts": message['ts'], "text": new_message}, team_hash=self.team.team_hash, channel_identifier=self.identifier) self.eventrouter.receive(s) - def my_last_message(self): + def my_last_message(self, msgno): for message in reversed(self.sorted_message_keys()): m = self.messages[message] if "user" in m.message_json and "text" in m.message_json and m.message_json["user"] == self.team.myidentifier: - return m.message_json + msgno -= 1 + if msgno == 0: + return m.message_json def is_visible(self): return w.buffer_get_integer(self.channel_buffer, "hidden") == 0 @@ -1450,13 +1473,12 @@ class SlackChannel(object): w.buffer_set(self.channel_buffer, "nicklist", "1") # create nicklists for the current channel if they don't exist # if they do, use the existing pointer - # TODO: put this back for mithrandir - # here = w.nicklist_search_group(self.channel_buffer, '', NICK_GROUP_HERE) - # if not here: - # here = w.nicklist_add_group(self.channel_buffer, '', NICK_GROUP_HERE, "weechat.color.nicklist_group", 1) - # afk = w.nicklist_search_group(self.channel_buffer, '', NICK_GROUP_AWAY) - # if not afk: - # afk = w.nicklist_add_group(self.channel_buffer, '', NICK_GROUP_AWAY, "weechat.color.nicklist_group", 1) + here = w.nicklist_search_group(self.channel_buffer, '', NICK_GROUP_HERE) + if not here: + here = w.nicklist_add_group(self.channel_buffer, '', NICK_GROUP_HERE, "weechat.color.nicklist_group", 1) + afk = w.nicklist_search_group(self.channel_buffer, '', NICK_GROUP_AWAY) + if not afk: + afk = w.nicklist_add_group(self.channel_buffer, '', NICK_GROUP_AWAY, "weechat.color.nicklist_group", 1) if user and len(self.members) < 1000: user = self.team.users[user] @@ -1464,9 +1486,11 @@ class SlackChannel(object): # since this is a change just remove it regardless of where it is w.nicklist_remove_nick(self.channel_buffer, nick) # now add it back in to whichever.. + nick_group = afk + if self.team.is_user_present(user.identifier): + nick_group = here if user.identifier in self.members: - w.nicklist_add_nick(self.channel_buffer, "", user.name, user.color_name, "", "", 1) - # w.nicklist_add_nick(self.channel_buffer, here, user.name, user.color_name, "", "", 1) + w.nicklist_add_nick(self.channel_buffer, nick_group, user.name, user.color_name, "", "", 1) # if we didn't get a user, build a complete list. this is expensive. else: @@ -1476,11 +1500,14 @@ class SlackChannel(object): user = self.team.users[user] if user.deleted: continue - w.nicklist_add_nick(self.channel_buffer, "", user.name, user.color_name, "", "", 1) - # w.nicklist_add_nick(self.channel_buffer, here, user.name, user.color_name, "", "", 1) + nick_group = afk + if self.team.is_user_present(user.identifier): + 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)) else: + w.nicklist_remove_all(self.channel_buffer) for fn in ["1| too", "2| many", "3| users", "4| to", "5| show"]: w.nicklist_add_group(self.channel_buffer, '', fn, w.color('white'), 1) @@ -1536,7 +1563,7 @@ class SlackDMChannel(SlackChannel): def update_color(self): if config.colorize_private_chats: - self.color_name = w.info_get('irc_nick_color_name', self.name) + self.color_name = get_nick_color_name(self.name) self.color = w.color(self.color_name) else: self.color = "" @@ -1757,6 +1784,7 @@ class SlackThreadChannel(object): self.channel_buffer = w.buffer_new(self.formatted_name(style="long_default"), "buffer_input_callback", "EVENTROUTER", "", "") self.eventrouter.weechat_controller.register_buffer(self.channel_buffer, self) w.buffer_set(self.channel_buffer, "localvar_set_type", 'channel') + w.buffer_set(self.channel_buffer, "localvar_set_nick", self.parent_message.team.nick) w.buffer_set(self.channel_buffer, "localvar_set_channel", self.formatted_name()) w.buffer_set(self.channel_buffer, "short_name", self.formatted_name(style="sidebar", enable_color=True)) time_format = w.config_string(w.config_get("weechat.look.buffer_time_format")) @@ -1815,7 +1843,7 @@ class SlackUser(object): def update_color(self): # This will automatically be none/"" if the user has disabled nick # colourization. - self.color_name = w.info_get('nick_color_name', self.name) + self.color_name = get_nick_color_name(self.name) self.color = w.color(self.color_name) def formatted_name(self, prepend="", enable_color=True): @@ -1883,7 +1911,7 @@ class SlackMessage(object): def get_sender(self): name = "" name_plain = "" - if 'bot_id' in self.message_json and self.message_json['bot_id'] is not None: + if 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)) elif 'user' in self.message_json: @@ -2003,80 +2031,89 @@ 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): - - users = {} - for item in login_data["users"]: - users[item["id"]] = SlackUser(**item) - # users.append(SlackUser(**item)) + metadata = pickle.loads(login_data["wee_slack_request_metadata"]) - bots = {} - for item in login_data["bots"]: - bots[item["id"]] = SlackBot(**item) - - channels = {} - for item in login_data["channels"]: - channels[item["id"]] = SlackChannel(eventrouter, **item) - - for item in login_data["ims"]: - channels[item["id"]] = SlackDMChannel(eventrouter, users, **item) + if not login_data["ok"]: + w.prnt("", "ERROR: Failed connecting to Slack with token {}: {}" + .format(metadata.token, login_data["error"])) + return - 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) + # 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): - else: - t = eventrouter.teams.get(th) - t.set_reconnect_url(login_data['url']) - t.connect() + users = {} + for item in login_data["users"]: + users[item["id"]] = SlackUser(**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 + bots = {} + for item in login_data["bots"]: + bots[item["id"]] = SlackBot(**item) - 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"])) + channels = {} + for item in login_data["channels"]: + channels[item["id"]] = SlackChannel(eventrouter, **item) - dbg("connected to {}".format(t.domain)) + for item in login_data["ims"]: + channels[item["id"]] = SlackDMChannel(eventrouter, users, **item) - # self.identifier = self.domain + 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) + else: + t = eventrouter.teams.get(th) + t.set_reconnect_url(login_data['url']) + t.connect() + + 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"])) + + dbg("connected to {}".format(t.domain)) + +def handle_channelsinfo(channel_json, eventrouter, **kwargs): + request_metadata = pickle.loads(channel_json["wee_slack_request_metadata"]) + team = eventrouter.teams[request_metadata.team_hash] + channel = team.channels[request_metadata.channel_identifier] + unread_count_display = channel_json['channel']['unread_count_display'] + channel.set_unread_count_display(unread_count_display) + +def handle_groupsinfo(group_json, eventrouter, **kwargs): + request_metadata = pickle.loads(group_json["wee_slack_request_metadata"]) + team = eventrouter.teams[request_metadata.team_hash] + group = team.channels[request_metadata.channel_identifier] + unread_count_display = group_json['group']['unread_count_display'] + 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_groupshistory(message_json, eventrouter, **kwargs): handle_history(message_json, eventrouter, **kwargs) @@ -2117,7 +2154,10 @@ def process_manual_presence_change(message_json, eventrouter, **kwargs): def process_presence_change(message_json, eventrouter, **kwargs): - kwargs["user"].presence = message_json["presence"] + if "user" in kwargs: + user = kwargs["user"] + team = kwargs["team"] + team.update_member_presence(user, message_json["presence"]) def process_pref_change(message_json, eventrouter, **kwargs): @@ -2293,9 +2333,9 @@ def subprocess_message_deleted(message_json, eventrouter, channel, team): def subprocess_channel_topic(message_json, eventrouter, channel, team): - text = unfurl_refs(message_json["text"], ignore_alt_text=False) + 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(message_json["topic"]) + channel.render_topic(unhtmlescape(message_json["topic"])) def process_reply(message_json, eventrouter, **kwargs): @@ -2429,6 +2469,17 @@ def process_reaction_removed(message_json, eventrouter, **kwargs): ###### New module/global methods +def render_formatting(text): + text = re.sub(r'(^| )\*([^*]+)\*([^a-zA-Z0-9_]|$)', + r'\1{}\2{}\3'.format(w.color(config.render_bold_as), + w.color('-' + config.render_bold_as)), + text) + text = re.sub(r'(^| )_([^_]+)_([^a-zA-Z0-9_]|$)', + r'\1{}\2{}\3'.format(w.color(config.render_italic_as), + w.color('-' + config.render_italic_as)), + text) + return text + def render(message_json, team, channel, force=False): # If we already have a rendered version in the object, just return that. @@ -2452,14 +2503,9 @@ def render(message_json, team, channel, force=False): text += unfurl_refs(unwrap_attachments(message_json, text), ignore_alt_text=config.unfurl_ignore_alt_text) text = text.lstrip() - text = text.replace("\t", " ") - text = text.replace("<", "<") - text = text.replace(">", ">") - text = text.replace("&", "&") - text = re.sub(r'(^| )\*([^*]+)\*([^a-zA-Z0-9_]|$)', - r'\1{}\2{}\3'.format(w.color('bold'), w.color('-bold')), text) - text = re.sub(r'(^| )_([^_]+)_([^a-zA-Z0-9_]|$)', - r'\1{}\2{}\3'.format(w.color('underline'), w.color('-underline')), text) + text = unhtmlescape(text.replace("\t", " ")) + if message_json.get('mrkdwn', True): + text = render_formatting(text) # if self.threads: # text += " [Replies: {} Thread ID: {} ] ".format(len(self.threads), self.thread_id) @@ -2475,7 +2521,18 @@ def linkify_text(message, team, channel): # function is only called on message send.. usernames = team.get_username_map() channels = team.get_channel_map() - message = message.replace('\x02', '*').replace('\x1F', '_').split(' ') + message = (message + # Replace IRC formatting chars with Slack formatting chars. + .replace('\x02', '*') + .replace('\x1D', '_') + .replace('\x1F', config.map_underline_to) + # Escape chars that have special meaning to Slack. Note that we do not + # (and should not) perform full HTML entity-encoding here. + # See https://api.slack.com/docs/message-formatting for details. + .replace('&', '&') + .replace('<', '<') + .replace('>', '>') + .split(' ')) for item in enumerate(message): targets = re.match('^\s*([@#])([\w.-]+[\w. -])(\W*)', item[1]) if targets and targets.groups()[0] == '@': @@ -2510,7 +2567,7 @@ 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) + 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)) @@ -2536,6 +2593,12 @@ def unfurl_ref(ref, ignore_alt_text=False): return display_text +def unhtmlescape(text): + return text.replace("<", "<") \ + .replace(">", ">") \ + .replace("&", "&") + + def unwrap_attachments(message_json, text_before): attachment_text = '' a = message_json.get("attachments", None) @@ -2643,6 +2706,8 @@ def modify_buffer_line(buffer, new_line, timestamp, time_id): # hold the structure of a line and of line data struct_hdata_line = w.hdata_get('line') struct_hdata_line_data = w.hdata_get('line_data') + # keep track of the number of lines with the matching time and id + number_of_matching_lines = 0 while line_pointer: # get a pointer to the data in line_pointer via layout of struct_hdata_line @@ -2653,13 +2718,32 @@ def modify_buffer_line(buffer, new_line, timestamp, time_id): # prefix = w.hdata_string(struct_hdata_line_data, data, 'prefix') if timestamp == int(line_timestamp) and int(time_id) == line_time_id: - # w.prnt("", "found matching time date is {}, time is {} ".format(timestamp, line_timestamp)) - w.hdata_update(struct_hdata_line_data, data, {"message": new_line}) + number_of_matching_lines += 1 + elif number_of_matching_lines > 0: + # since number_of_matching_lines is non-zero, we have + # already reached the message and can stop traversing break - else: - pass + else: + dbg(('Encountered line without any data while trying to modify ' + 'line. This is not handled, so aborting modification.')) + return w.WEECHAT_RC_ERROR # move backwards one line and try again - exit the while if you hit the end line_pointer = w.hdata_move(struct_hdata_line, line_pointer, -1) + + # split the message into at most the number of existing lines + lines = new_line.split('\n', number_of_matching_lines - 1) + # updating a line with a string containing newlines causes the lines to + # be broken when viewed in bare display mode + lines = [line.replace('\n', ' | ') for line in lines] + # pad the list with empty strings until the number of elements equals + # number_of_matching_lines + lines += [''] * (number_of_matching_lines - len(lines)) + + if line_pointer: + for line in lines: + line_pointer = w.hdata_move(struct_hdata_line, line_pointer, 1) + data = w.hdata_pointer(struct_hdata_line, line_pointer, 'data') + w.hdata_update(struct_hdata_line_data, data, {"message": line}) return w.WEECHAT_RC_OK @@ -2678,10 +2762,20 @@ def modify_print_time(buffer, new_id, time): struct_hdata_line = w.hdata_get('line') struct_hdata_line_data = w.hdata_get('line_data') - # get a pointer to the data in line_pointer via layout of struct_hdata_line - data = w.hdata_pointer(struct_hdata_line, line_pointer, 'data') - if data: - w.hdata_update(struct_hdata_line_data, data, {"date_printed": new_id}) + prefix = '' + while not prefix and line_pointer: + # get a pointer to the data in line_pointer via layout of struct_hdata_line + data = w.hdata_pointer(struct_hdata_line, line_pointer, 'data') + if data: + prefix = w.hdata_string(struct_hdata_line_data, data, 'prefix') + w.hdata_update(struct_hdata_line_data, data, {"date_printed": new_id}) + else: + dbg('Encountered line without any data while setting message id.') + return w.WEECHAT_RC_ERROR + # move backwards one line and repeat, so all the lines of the message are set + # exit when you reach a prefix, which means you have reached the + # first line of the message, or if you hit the end + line_pointer = w.hdata_move(struct_hdata_line, line_pointer, -1) return w.WEECHAT_RC_OK @@ -2792,14 +2886,17 @@ def msg_command_cb(data, current_buffer, args): dbg("msg_command_cb") aargs = args.split(None, 2) who = aargs[1] - command_talk(data, current_buffer, who) + if who == "*": + who = EVENTROUTER.weechat_controller.buffers[current_buffer].slack_name + else: + command_talk(data, current_buffer, who) if len(aargs) > 2: message = aargs[2] team = EVENTROUTER.weechat_controller.buffers[current_buffer].team cmap = team.get_channel_map() if who in cmap: - channel = team.channels[cmap[channel]] + channel = team.channels[cmap[who]] channel.send_message(message) return w.WEECHAT_RC_OK_EAT @@ -3050,7 +3147,7 @@ def command_status(data, current_buffer, args): profile = {"status_text":text,"status_emoji":emoji} - s = SlackRequest(team.token, "users.profile.set", {"profile": profile}, team_hash=team.team_hash, channel_identifier=channel.identifier) + s = SlackRequest(team.token, "users.profile.set", {"profile": profile}, team_hash=team.team_hash) EVENTROUTER.receive(s) @@ -3212,39 +3309,114 @@ def dbg(message, level=0, main_buffer=False, fout=False): ###### Config code +Setting = collections.namedtuple('Setting', ['default', 'desc']) class PluginConfig(object): # Default settings. - # These are in the (string) format that weechat expects; at __init__ time - # this value will be used to set the default for any settings not already - # defined, and then the real (python) values of the settings will be - # extracted. - # TODO: setting descriptions. - settings = { - 'colorize_private_chats': 'false', - 'debug_mode': 'false', - 'debug_level': '3', - 'distracting_channels': '', - 'show_reaction_nicks': 'false', - 'slack_api_token': 'INSERT VALID KEY HERE!', - 'slack_timeout': '20000', - 'switch_buffer_on_join': 'true', - 'trigger_value': 'false', - 'unfurl_ignore_alt_text': 'false', - 'record_events': 'false', - 'thread_suffix_color': 'lightcyan', - 'unhide_buffers_with_activity': 'false', - 'short_buffer_names': 'false', - 'channel_name_typing_indicator': 'true', - 'background_load_all_history': 'false', - 'never_away': 'false', - 'server_aliases': '', + # These are, initially, each a (default, desc) tuple; the former is the + # default value of the setting, in the (string) format that weechat + # expects, and the latter is the user-friendly description of the setting. + # At __init__ time these values are extracted, the description is used to + # set or update the setting description for use with /help, and the default + # value is used to set the default for any settings not already defined. + # Following this procedure, the keys remain the same, but the values are + # the real (python) values of the settings. + default_settings = { + '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.'), + 'channel_name_typing_indicator': Setting( + default='true', + desc='Change the prefix of a channel from # to > when someone is' + ' typing in it. Note that this will (temporarily) affect the sort' + ' order if you sort buffers by name rather than by number.'), + 'colorize_private_chats': Setting( + default='false', + desc='Whether to use nick-colors in DM windows.'), + 'debug_mode': Setting( + default='false', + desc='Open a dedicated buffer for debug messages and start logging' + ' to it. How verbose the logging is depends on log_level.'), + 'debug_level': Setting( + default='3', + desc='Show only this level of debug info (or higher) when' + ' debug_mode is on. Lower levels -> more messages.'), + 'distracting_channels': Setting( + default='', + desc='List of channels to hide.'), + 'map_underline_to': Setting( + default='_', + desc='When sending underlined text to slack, use this formatting' + ' character for it. The default ("_") sends it as italics. Use' + ' "*" to send bold instead.'), + 'never_away': Setting( + default='false', + desc='Poke Slack every five minutes so that it never marks you "away".'), + 'record_events': Setting( + default='false', + desc='Log all traffic from Slack to disk as JSON.'), + 'render_bold_as': Setting( + default='bold', + desc='When receiving bold text from Slack, render it as this in weechat.'), + 'render_italic_as': Setting( + 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.'), + 'server_aliases': Setting( + default='', + desc='A comma separated list of `subdomain:alias` pairs. The alias' + ' will be used instead of the actual name of the slack (in buffer' + ' names, logging, etc). E.g `work:no_fun_allowed` would make your' + ' work slack show up as `no_fun_allowed` rather than `work.slack.com`.'), + 'short_buffer_names': Setting( + default='false', + desc='Use `foo.#channel` rather than `foo.slack.com.#channel` as the' + ' internal name for Slack buffers. Overrides server_aliases.'), + 'show_reaction_nicks': Setting( + default='false', + desc='Display the name of the reacting user(s) alongside each reactji.'), + 'slack_api_token': Setting( + default='INSERT VALID KEY HERE!', + desc='List of Slack API tokens, one per Slack instance you want to' + ' connect to. See the README for details on how to get these.'), + 'slack_timeout': Setting( + default='20000', + desc='How long (ms) to wait when communicating with Slack.'), + 'switch_buffer_on_join': Setting( + default='true', + desc='When /joining a channel, automatically switch to it as well.'), + 'thread_suffix_color': Setting( + default='lightcyan', + desc='Color to use for the [thread: XXX] suffix on messages that' + ' have threads attached to them.'), + 'unfurl_ignore_alt_text': Setting( + default='false', + 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.'), + 'unhide_buffers_with_activity': Setting( + default='false', + desc='When activity occurs on a buffer, unhide it even if it was' + ' previously hidden (whether by the user or by the' + ' distracting_channels setting).'), } # Set missing settings to their defaults. Load non-missing settings from # weechat configs. def __init__(self): + self.settings = {} + # Set all descriptions, replace the values in the dict with the + # default setting value rather than the (setting,desc) tuple. + # Use items() rather than iteritems() so we don't need to worry about + # invalidating the iterator. + for key, (default, desc) in self.default_settings.items(): + w.config_set_desc_plugin(key, desc) + self.settings[key] = default + + # Migrate settings from old versions of Weeslack... self.migrate() + # ...and then set anything left over from the defaults. for key, default in self.settings.iteritems(): if not w.config_get_plugin(key): w.config_set_plugin(key, default) @@ -3276,6 +3448,19 @@ class PluginConfig(object): def get_boolean(self, key): return w.config_string_to_boolean(w.config_get_plugin(key)) + def get_string(self, key): + return w.config_get_plugin(key) + + def get_int(self, key): + return int(w.config_get_plugin(key)) + + get_debug_level = get_int + 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 + def get_distracting_channels(self, key): return [x.strip() for x in w.config_get_plugin(key).split(',')] @@ -3291,15 +3476,6 @@ class PluginConfig(object): else: return token - def get_thread_suffix_color(self, key): - return w.config_get_plugin("thread_suffix_color") - - def get_debug_level(self, key): - return int(w.config_get_plugin(key)) - - def get_slack_timeout(self, key): - return int(w.config_get_plugin(key)) - def migrate(self): """ This is to migrate the extension name from slack_extension to slack @@ -3351,8 +3527,8 @@ if __name__ == "__main__": if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "script_unloaded", ""): - version = w.info_get("version_number", "") or 0 - if int(version) < 0x1030000: + weechat_version = w.info_get("version_number", "") or 0 + if int(weechat_version) < 0x1030000: w.prnt("", "\nERROR: Weechat version 1.3+ is required to use {}.\n\n".format(SCRIPT_NAME)) else: |