From 7d919f3897deb322f13428127b1ba91731917f4c Mon Sep 17 00:00:00 2001 From: Nana Amfo Date: Mon, 8 Apr 2019 06:03:35 -0500 Subject: Add @user-groups support (#680) Add @user-groups with tab-completion. @user-groups will be unfurl into format message before sending message via linktext method. --- README.md | 7 ++ _pytest/data/http/rtm.start.json | 36 +++++- .../websocket/1483975206.59-subteam_created.json | 24 ++++ .../websocket/1483975206.59-subteam_updated.json | 34 ++++++ _pytest/test_linkifytext.py | 11 ++ _pytest/test_processsubteamcreated.py | 22 ++++ _pytest/test_processsubteamupdated.py | 29 +++++ _pytest/test_unfurl.py | 4 + wee_slack.py | 130 +++++++++++++++++++-- 9 files changed, 287 insertions(+), 10 deletions(-) create mode 100644 _pytest/data/websocket/1483975206.59-subteam_created.json create mode 100644 _pytest/data/websocket/1483975206.59-subteam_updated.json create mode 100644 _pytest/test_processsubteamcreated.py create mode 100644 _pytest/test_processsubteamupdated.py diff --git a/README.md b/README.md index 88076bc..42b3c9d 100644 --- a/README.md +++ b/README.md @@ -207,6 +207,13 @@ e.g. like this: /set weechat.completion.default_template "%(nicks)|%(irc_channels)|%(emoji)" ``` +#### User group tab completions +To enable tab completions for usergroups append `|%(usergroups)` +``` +/set weechat.completion.default_template "%(nicks)|%(irc_channels)|%(usergroups)" +``` +The usergroup will appear in the same formats as nicks like the following `@marketing` where marketing is the handle + #### Cursor and mouse mode The cursor mode and mouse mode can be used to interact with older messages, for editing, deleting, reacting and replying to a message. Mouse mode can be toggled by pressing `Alt`+`m` and cursor mode can be entered by running `/cursor` (see `/help cursor`). diff --git a/_pytest/data/http/rtm.start.json b/_pytest/data/http/rtm.start.json index 34d875a..2d0a36a 100644 --- a/_pytest/data/http/rtm.start.json +++ b/_pytest/data/http/rtm.start.json @@ -673,8 +673,40 @@ "read_only_channels": [], "can_manage_shared_channels": false, "subteams": { - "self": [], - "all": [] + "all": [ + { + "auto_provision": false, + "auto_type": null, + "created_by": "X786CTH5L", + "date_create": 1541837133, + "date_delete": 0, + "date_update": 1511887378, + "deleted_by": null, + "description": "Sample subteam / subgroup", + "enterprise_subteam_id": "S01", + "handle": "test", + "id": "TGX0ALBK3", + "is_external": false, + "is_subteam": true, + "is_usergroup": true, + "name": "Sample Usergroup", + "prefs": { + "channels": [ + "A7BC8UX4Z" + ], + "groups": [ + "I78T9MC86" + ] + }, + "team_id": "T6SH3PWP9", + "updated_by": "X786CTH5L", + "user_count": 4 + } + ], + + "self": [ + "TGX0ALBK3" + ] }, "dnd": { "dnd_enabled": true, diff --git a/_pytest/data/websocket/1483975206.59-subteam_created.json b/_pytest/data/websocket/1483975206.59-subteam_created.json new file mode 100644 index 0000000..af42a0f --- /dev/null +++ b/_pytest/data/websocket/1483975206.59-subteam_created.json @@ -0,0 +1,24 @@ +{ + "subteam": { + "auto_type": null, + "created_by": "U060RNRCZ", + "date_create": 1446746793, + "date_delete": 0, + "date_update": 1446746793, + "deleted_by": null, + "description": "Marketing gurus, PR experts and product advocates.", + "handle": "marketing-team", + "id": "S0615G0KT", + "is_external": false, + "is_usergroup": true, + "name": "Marketing Team", + "prefs": { + "channels": [], + "groups": [] + }, + "team_id": "T060RNRCH", + "updated_by": "U060RNRCZ", + "user_count": "0" + }, + "type": "subteam_created" +} diff --git a/_pytest/data/websocket/1483975206.59-subteam_updated.json b/_pytest/data/websocket/1483975206.59-subteam_updated.json new file mode 100644 index 0000000..4b5c03e --- /dev/null +++ b/_pytest/data/websocket/1483975206.59-subteam_updated.json @@ -0,0 +1,34 @@ +{ + "type": "subteam_updated", + "subteam": { + "id": "TGX0ALBK3", + "team_id": "T6SH3PWP9", + "is_usergroup": true, + "name": "Sample Usergroup Modified", + "description": "Sample subteam modified", + "handle": "test_modified", + "is_external": false, + "date_create": 1446598059, + "date_update": 1446670362, + "date_delete": 0, + "auto_type": "admin", + "created_by": "X786CTH5L", + "updated_by": "U060RNRCZ", + "deleted_by": null, + "prefs": { + "channels": [ + + ], + "groups": [ + + ] + }, + "users": [ + "U060RNRCZ", + "U060ULRC0", + "U06129G2V", + "U061309JM" + ], + "user_count": "4" + } +} diff --git a/_pytest/test_linkifytext.py b/_pytest/test_linkifytext.py index da1586e..fea6ab8 100644 --- a/_pytest/test_linkifytext.py +++ b/_pytest/test_linkifytext.py @@ -53,6 +53,17 @@ def test_linkifytext_names_with_apostrophe(realish_eventrouter): assert text == '@O\'Connor: my test message' +def test_linkifytext_names_with_subgroup_notification(realish_eventrouter): + subteam_id = "TGX0ALBK3" + handle = "test" + team = realish_eventrouter.teams.values()[0] + channel = team.channels.values()[0] + + message = 'This is a message for the test team' + text = linkify_text('@test: {}'.format(message), team) + + assert text == ': {}'.format(subteam_id, handle, message) + def test_linkifytext_at_channel(realish_eventrouter): team = realish_eventrouter.teams.values()[0] diff --git a/_pytest/test_processsubteamcreated.py b/_pytest/test_processsubteamcreated.py new file mode 100644 index 0000000..7d403db --- /dev/null +++ b/_pytest/test_processsubteamcreated.py @@ -0,0 +1,22 @@ +import glob +import json + +def test_process_subteam_created(mock_websocket, realish_eventrouter): + + eventrouter = realish_eventrouter + + t = eventrouter.teams.keys()[0] + + assert len(eventrouter.teams[t].subteams) == 1 + + socket = mock_websocket + eventrouter.teams[t].ws = socket + datafiles = glob.glob("_pytest/data/websocket/1483975206.59-subteam_created.json") + + for fname in datafiles: + data = json.loads(open(fname, 'r').read()) + socket.add(data) + eventrouter.receive_ws_callback(t) + eventrouter.handle_next() + + assert len(eventrouter.teams[t].subteams) == 2 diff --git a/_pytest/test_processsubteamupdated.py b/_pytest/test_processsubteamupdated.py new file mode 100644 index 0000000..34fd302 --- /dev/null +++ b/_pytest/test_processsubteamupdated.py @@ -0,0 +1,29 @@ +import glob +from mock import patch, call +from wee_slack import SlackTeam, SlackSubteam +import json + +def test_process_subteam_self_updated(mock_websocket, realish_eventrouter): + + eventrouter = realish_eventrouter + + t = eventrouter.teams.keys()[0] + + assert len(eventrouter.teams[t].subteams) == 1 + + socket = mock_websocket + eventrouter.teams[t].ws = socket + datafiles = glob.glob("_pytest/data/websocket/1483975206.59-subteam_updated.json") + + for fname in datafiles: + data = json.loads(open(fname, 'r').read()) + socket.add(data) + eventrouter.receive_ws_callback(t) + eventrouter.handle_next() + team = eventrouter.teams[t] + subteam = team.subteams.values()[0] + + assert data['subteam']['handle'] == subteam.handle + assert data['subteam']['description'] == subteam.description + assert data['subteam']['name'] == subteam.name + diff --git a/_pytest/test_unfurl.py b/_pytest/test_unfurl.py index 40674b4..6b8e84c 100644 --- a/_pytest/test_unfurl.py +++ b/_pytest/test_unfurl.py @@ -62,6 +62,10 @@ slack = wee_slack 'input': "<@U407ABLLW> I think 3 > 2", 'output': "@alice I think 3 > 2", }, + { + 'input': " This is announcement for the dev team", + 'output': "@dev This is announcement for the dev team" + } )) def test_unfurl_refs(case, realish_eventrouter): slack.EVENTROUTER = realish_eventrouter diff --git a/wee_slack.py b/wee_slack.py index c1e683b..9f1fe6f 100644 --- a/wee_slack.py +++ b/wee_slack.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- + from __future__ import unicode_literals from collections import OrderedDict @@ -278,6 +279,7 @@ class EventRouter(object): self.slow_queue = [] self.slow_queue_timer = 0 self.teams = {} + self.subteams = {} self.context = {} self.weechat_controller = WeechatController(self) self.previous_buffer = "" @@ -560,6 +562,8 @@ class EventRouter(object): kwargs["user"] = self.teams[team].users[j["user"]] if "channel" in j: kwargs["channel"] = self.teams[team].channels[j["channel"]] + if "subteam" in j: + kwargs["subteam"] = self.teams[team].subteams[j["subteam"]] except: dbg("metadata failure") @@ -774,7 +778,6 @@ def input_text_for_buffer_cb(data, modifier, current_buffer, string): return "" return string - @utf8_decode def buffer_switch_callback(signal, sig_type, data): """ @@ -927,6 +930,20 @@ def emoji_completion_cb(data, completion_item, current_buffer, completion): w.hook_completion_list_add(completion, ":" + e + ":", 0, w.WEECHAT_LIST_POS_SORT) return w.WEECHAT_RC_OK +@utf8_decode +def usergroups_completion_cb(data, completion_item, current_buffer, completion): + """ + Adds all @-prefixed usergroups to completion list + """ + + current_channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer, None) + + if current_channel is None: + return w.WEECHAT_RC_OK + for subteam in current_channel.team.subteams.values(): + w.hook_completion_list_add(completion, "@" + subteam.handle, 1, w.WEECHAT_LIST_POS_SORT) + return w.WEECHAT_RC_OK + @utf8_decode def complete_next_cb(data, current_buffer, command): @@ -1031,6 +1048,24 @@ class SlackRequest(object): def retry_ready(self): return (self.start_time + (self.tries**2)) < time.time() +class SlackSubteam(object): + """ + Represents a slack group or subteam + """ + def __init__(self, originating_team_id, **kwargs): + self.handle = kwargs.get('handle', None) + self.identifier = kwargs['id'] + self.name = kwargs.get('name', None) + self.description = kwargs.get('description', None) + self.team_id = originating_team_id + + + def __repr__(self): + return "Name:{} Identifier:{}".format(self.name, self.identifier) + + def __eq__(self, compare_str): + return compare_str == self.subteam_id + class SlackTeam(object): """ @@ -1038,7 +1073,7 @@ class SlackTeam(object): Team object under which users and channels live.. Does lots. """ - def __init__(self, eventrouter, token, websocket_url, team_info, nick, myidentifier, users, bots, channels, **kwargs): + def __init__(self, eventrouter, token, websocket_url, team_info, subteams, nick, myidentifier, users, bots, channels, **kwargs): self.identifier = team_info["id"] self.ws_url = websocket_url self.connected = False @@ -1049,6 +1084,7 @@ class SlackTeam(object): self.eventrouter = eventrouter self.token = token self.team = self + self.subteams = subteams self.subdomain = team_info["domain"] self.domain = self.subdomain + ".slack.com" self.preferred_name = self.domain @@ -1072,7 +1108,7 @@ class SlackTeam(object): for c in self.channels.keys(): channels[c].set_related_server(self) channels[c].check_should_open() - # self.channel_set_related_server(c) + # self.channel_set_related_server(c) # Last step is to make sure my nickname is the set color self.users[self.myidentifier].force_color(w.config_string(w.config_get('weechat.color.chat_nick_self'))) # This highlight step must happen after we have set related server @@ -1084,10 +1120,7 @@ class SlackTeam(object): return "domain={} nick={}".format(self.subdomain, self.nick) def __eq__(self, compare_str): - if compare_str == self.token or compare_str == self.domain or compare_str == self.subdomain: - return True - else: - return False + return compare_str == self.token or compare_str == self.domain or compare_str == self.subdomain @property def members(self): @@ -1103,6 +1136,9 @@ class SlackTeam(object): self.channels[channel["id"]] = channel channel.set_related_server(self) + def generate_usergroup_map(self): + return { s.handle: s.identifier for s in self.subteams.values()} + # def connect_request_generate(self): # return SlackRequest(self.token, 'rtm.start', {}) @@ -2410,6 +2446,10 @@ def handle_rtmstart(login_data, eventrouter): for item in login_data["bots"]: bots[item["id"]] = SlackBot(login_data['team']['id'], **item) + subteams = {} + for item in login_data["subteams"]["all"]: + subteams[item['id']] = SlackSubteam(login_data['team']['id'], **item) + channels = {} for item in login_data["channels"]: if item["is_shared"]: @@ -2431,6 +2471,7 @@ def handle_rtmstart(login_data, eventrouter): metadata.token, login_data['url'], login_data["team"], + subteams, login_data["self"]["name"], login_data["self"]["id"], users, @@ -2541,7 +2582,15 @@ def handle_usersinfo(user_json, eventrouter, **kwargs): channel.slack_name = user.name channel.set_topic(create_user_status_string(user.profile)) +def handle_usergroupsuserslist(users_json, eventrouter, **kwargs): + request_metadata = pickle.loads(users_json['wee_slack_request_metadata']) + team = eventrouter.teams[request_metadata.team_hash] + user_identifers = users_json['users'] + team.buffer_prnt("Users:") + for user_identifier in user_identifers: + user = team.users[user_identifier] + team.buffer_prnt(" {:<25}({})".format(user.name, user.presence)) ###### New/converted process_ and subprocess_ methods def process_hello(message_json, eventrouter, **kwargs): @@ -2649,6 +2698,27 @@ def process_message(message_json, eventrouter, store=True, download=True, **kwar if download: download_files(message_json, **kwargs) +def process_subteam_created(subteam_json, eventrouter, **kwargs): + team = kwargs['team'] + subteam_json_info = subteam_json['subteam'] + subteam = SlackSubteam(team.identifier, **subteam_json_info) + team.subteams[subteam_json_info['id']] = subteam + +def process_subteam_updated(subteam_json, eventrouter, **kwargs): + team = kwargs['team'] + usergroups = team.generate_usergroup_map() + new_subteam_info = subteam_json['subteam'] + + current_subteam_info = team.subteams[new_subteam_info.get('id')] + + if config.notify_usergroup_handle_updated and current_subteam_info.handle != new_subteam_info['handle']: + usergroups[new_subteam_info['handle']] = new_subteam_info.get('id') + template = 'User group @{old_handle} has updated its handle to @{new_handle} in team {team}' + message = template.format(old_handle=current_subteam_info.handle, new_handle=new_subteam_info['handle'], + team=team.preferred_name) + team.buffer_prnt(message, message=True) + + team.subteams[new_subteam_info.get('id')] = SlackSubteam(team.identifier, **new_subteam_info) def download_files(message_json, **kwargs): team = kwargs["team"] @@ -2955,6 +3025,7 @@ def linkify_text(message, team): # function is only called on message send.. usernames = team.get_username_map() channels = team.get_channel_map() + usergroups = team.generate_usergroup_map() message = (message # Replace IRC formatting chars with Slack formatting chars. .replace('\x02', '*') @@ -2973,6 +3044,8 @@ def linkify_text(message, team): named = targets.groups() if named[1] in ["group", "channel", "here"]: message[item[0]] = "{}".format(named[1], named[2]) + elif named[1] in list(usergroups.viewkeys()): + message[item[0]] = "{}".format(usergroups[named[1]], named[1], named[2]) else: try: if usernames[named[1]]: @@ -3000,6 +3073,7 @@ def unfurl_refs(text, ignore_alt_text=None, auto_link_display=None): # - # - <#C2147483705|#otherchannel> # - <@U2147483697|@othernick> + # - # Test patterns lives in ./_pytest/test_unfurl.py if ignore_alt_text is None: @@ -3007,7 +3081,7 @@ def unfurl_refs(text, ignore_alt_text=None, auto_link_display=None): if auto_link_display is None: auto_link_display = config.unfurl_auto_link_display - matches = re.findall(r"(<[@#]?(?:[^>]*)>)", text) + matches = re.findall(r"(<[@#!]?(?:[^>]*)>)", text) for m in matches: # Replace them with human readable strings text = text.replace( @@ -3026,6 +3100,12 @@ def unfurl_ref(ref, ignore_alt_text, auto_link_display): display_text = "#{}".format(ref.split('|')[1]) elif id.startswith("@U"): display_text = ref.split('|')[1] + elif id.startswith("!subteam"): + if ref.split('|')[1].startswith('@'): + handle = ref.split('|')[1][1:] + else: + handle = ref.split('|')[1] + display_text = '@{}'.format(handle) else: url, desc = ref.split('|', 1) match_url = r"^\w+:(//)?{}$".format(re.escape(desc)) @@ -3504,6 +3584,35 @@ def command_users(data, current_buffer, args): return w.WEECHAT_RC_OK_EAT +@slack_buffer_required +@utf8_decode +def command_usergroups(data, current_buffer, args): + """ + + /slack usergroups [handle] + List the usergroups in the current team + If handle is given show the members in the usergroup + """ + e = EVENTROUTER + team = e.weechat_controller.buffers[current_buffer].team + usergroups = team.generate_usergroup_map() + handle = args[1:] if args and args.startswith("@") else args + + if handle and handle in usergroups.keys(): + subteam = team.subteams[usergroups[handle]] + s = SlackRequest(team.token, "usergroups.users.list", { "usergroup": subteam.identifier }, team_hash=team.team_hash) + e.receive(s) + elif not handle: + team.buffer_prnt("Usergroups:") + for subteam in team.subteams.values(): + team.buffer_prnt(" {:<25}(@{})".format(subteam.name, subteam.handle)) + else: + w.prnt('', 'ERROR: Unknown usergroup handle: {}'.format(handle)) + return w.WEECHAT_RC_ERROR + + return w.WEECHAT_RC_OK_EAT + + @slack_buffer_required @utf8_decode def command_talk(data, current_buffer, args): @@ -4060,6 +4169,7 @@ def setup_hooks(): w.hook_command(cmd, description, args, '', '', 'command_' + cmd, '') w.hook_completion("nicks", "complete @-nicks for slack", "nick_completion_cb", "") + w.hook_completion("usergroups", "complete @-usergroups for slack", "usergroups_completion_cb", "") w.hook_completion("emoji", "complete :emoji: for slack", "emoji_completion_cb", "") w.key_bind("mouse", { @@ -4185,6 +4295,10 @@ class PluginConfig(object): " highlights, i.e. not @channel and @here. all_highlights: Show" " all highlights, but not other messages. all: Show all activity," " like other channels."), + 'notify_usergroup_handle_updated': Setting( + default='false', + desc="Control if you want to see notification when a usergroup's" + " handle has changed, either true or false"), 'never_away': Setting( default='false', desc='Poke Slack every five minutes so that it never marks you "away".'), -- cgit