diff options
-rw-r--r-- | .gitignore | 3 | ||||
-rw-r--r-- | README.md | 16 | ||||
-rw-r--r-- | _pytest/conftest.py | 10 | ||||
-rw-r--r-- | _pytest/test_everything.py | 2 | ||||
-rw-r--r-- | _pytest/test_linkifytext.py | 9 | ||||
-rw-r--r-- | _pytest/test_presencechange.py | 2 | ||||
-rw-r--r-- | _pytest/test_process_message.py | 2 | ||||
-rw-r--r-- | _pytest/test_processreply.py | 2 | ||||
-rw-r--r-- | _pytest/test_processteamjoin.py | 2 | ||||
-rw-r--r-- | _pytest/test_sendmessage.py | 2 | ||||
-rw-r--r-- | _pytest/test_unfurl.py | 36 | ||||
-rw-r--r-- | wee_slack.py | 510 | ||||
-rw-r--r-- | weemoji.json | 1349 |
13 files changed, 1739 insertions, 206 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8673f9c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.pyc +.cache/ +*.sublime-* @@ -119,7 +119,6 @@ Commands Join a channel: ``` /join [channel] -/slack join [channel] ``` Start a direct chat with someone: @@ -156,6 +155,11 @@ Modify previous message: s/old text/new text/ ``` +Modify 3rd previous message: +``` +3s/old text/new text/ +``` + Replace all instances of text in previous message: ``` s/old text/new text/g @@ -225,16 +229,6 @@ Example: Optional settings ----------------- -Turn off colorized nicks: -``` -/set plugins.var.python.slack.colorize_nicks 0 -``` - -Turn on colorized messages (messages match nick color): -``` -/set plugins.var.python.slack.colorize_nicks 1 -``` - Set channel prefix to something other than my-slack-subdomain.slack.com (e.g. when using buffers.pl): ``` /set plugins.var.python.slack.server_aliases "my-slack-subdomain:mysub,other-domain:coolbeans" diff --git a/_pytest/conftest.py b/_pytest/conftest.py index 7a6f39a..232814f 100644 --- a/_pytest/conftest.py +++ b/_pytest/conftest.py @@ -26,12 +26,13 @@ def mock_websocket(): return fakewebsocket() @pytest.fixture -def realish_eventrouter(): +def realish_eventrouter(mock_weechat): e = EventRouter() context = e.store_context(SlackRequest('xoxoxoxox', "rtm.start", {"meh": "blah"})) rtmstartdata = open('_pytest/data/http/rtm.start.json', 'r').read() e.receive_httprequest_callback(context, 1, 0, rtmstartdata, 4) - e.handle_next() + while len(e.queue): + e.handle_next() #e.sc is just shortcuts to these items e.sc = {} e.sc["team_id"] = e.teams.keys()[0] @@ -71,6 +72,10 @@ class FakeWeechat(): return "0x8a8a8a8b" def prefix(self, type): return "" + def config_get_plugin(self, key): + return "" + def color(self, name): + return "" def __getattr__(self, name): def method(*args): pass @@ -87,6 +92,7 @@ def mock_weechat(): wee_slack.slack_debug = "debug_buffer_ptr" wee_slack.STOP_TALKING_TO_SLACK = False wee_slack.proc = {} + wee_slack.weechat_version = 0x10500000 pass diff --git a/_pytest/test_everything.py b/_pytest/test_everything.py index a121541..c85fc15 100644 --- a/_pytest/test_everything.py +++ b/_pytest/test_everything.py @@ -4,7 +4,7 @@ import json #from wee_slack import render from wee_slack import ProcessNotImplemented -def test_process_message(monkeypatch, realish_eventrouter, mock_websocket): +def test_everything(realish_eventrouter, mock_websocket): eventrouter = realish_eventrouter diff --git a/_pytest/test_linkifytext.py b/_pytest/test_linkifytext.py index f9da3f9..56bf1b5 100644 --- a/_pytest/test_linkifytext.py +++ b/_pytest/test_linkifytext.py @@ -4,3 +4,12 @@ from wee_slack import linkify_text # linkify_text('@ryan') # assert False + + +def test_linkifytext_does_partial_html_entity_encoding(realish_eventrouter): + team = realish_eventrouter.teams.values()[0] + channel = team.channels.values()[0] + + text = linkify_text('& < > \' "', team, channel) + + assert text == '& < > \' "' diff --git a/_pytest/test_presencechange.py b/_pytest/test_presencechange.py index b4202fa..4e02640 100644 --- a/_pytest/test_presencechange.py +++ b/_pytest/test_presencechange.py @@ -1,5 +1,5 @@ -def test_PresenceChange(monkeypatch, realish_eventrouter, mock_websocket): +def test_PresenceChange(realish_eventrouter, mock_websocket): e = realish_eventrouter diff --git a/_pytest/test_process_message.py b/_pytest/test_process_message.py index e2447f7..2e0b31e 100644 --- a/_pytest/test_process_message.py +++ b/_pytest/test_process_message.py @@ -2,7 +2,7 @@ import json from wee_slack import render -def test_process_message(monkeypatch, realish_eventrouter, mock_websocket): +def test_process_message(realish_eventrouter, mock_websocket): e = realish_eventrouter diff --git a/_pytest/test_processreply.py b/_pytest/test_processreply.py index a725f23..041a1db 100644 --- a/_pytest/test_processreply.py +++ b/_pytest/test_processreply.py @@ -1,6 +1,6 @@ #from wee_slack import process_reply -def test_process_reply(monkeypatch, realish_eventrouter, mock_websocket): +def test_process_reply(realish_eventrouter, mock_websocket): e = realish_eventrouter diff --git a/_pytest/test_processteamjoin.py b/_pytest/test_processteamjoin.py index 00a8b4c..c7c199f 100644 --- a/_pytest/test_processteamjoin.py +++ b/_pytest/test_processteamjoin.py @@ -3,7 +3,7 @@ import json from wee_slack import ProcessNotImplemented -def test_process_reply(monkeypatch, mock_websocket, realish_eventrouter): +def test_process_team_join(mock_websocket, realish_eventrouter): eventrouter = realish_eventrouter diff --git a/_pytest/test_sendmessage.py b/_pytest/test_sendmessage.py index a87942d..42c22a6 100644 --- a/_pytest/test_sendmessage.py +++ b/_pytest/test_sendmessage.py @@ -1,5 +1,5 @@ -def test_send_message(monkeypatch, realish_eventrouter, mock_websocket): +def test_send_message(realish_eventrouter, mock_websocket): e = realish_eventrouter t = e.teams.keys()[0] diff --git a/_pytest/test_unfurl.py b/_pytest/test_unfurl.py index b631888..eebe446 100644 --- a/_pytest/test_unfurl.py +++ b/_pytest/test_unfurl.py @@ -10,17 +10,17 @@ slack = wee_slack 'output': "foo", }, { - 'input': "<@U2147483697|@othernick>: foo", - 'output': "@testuser: foo", + 'input': "<@U407ABLLW|@othernick>: foo", + 'output': "@alice: foo", 'ignore_alt_text': True, }, { - 'input': "foo <#C2147483705|#otherchannel> foo", + 'input': "foo <#C407ABS94|otherchannel> foo", 'output': "foo #otherchannel foo", }, { - 'input': "foo <#C2147483705> foo", - 'output': "foo #test-chan foo", + 'input': "foo <#C407ABS94> foo", + 'output': "foo #general foo", }, { 'input': "url: <https://example.com|example> suffix", @@ -31,23 +31,21 @@ slack = wee_slack 'output': "url: https://example.com (example with spaces) suffix", }, { - 'input': "<@U2147483697|@othernick> multiple unfurl <https://example.com|example with spaces>", + 'input': "<@U407ABLLW|@othernick> multiple unfurl <https://example.com|example with spaces>", 'output': "@othernick multiple unfurl https://example.com (example with spaces)", }, { - 'input': "try the #test-chan channel", - 'output': "try the #test-chan channel", + 'input': "try the #general channel", + 'output': "try the #general channel", + }, + { + 'input': "<@U407ABLLW> I think 3 > 2", + 'output': "@alice I think 3 > 2", }, )) -def test_unfurl_refs(case): - pass - #print myslack - #slack.servers = myslack.server - #slack.channels = myslack.channel - #slack.users = myslack.user - #slack.message_cache = {} - #slack.servers[0].users = myslack.user - #print myslack.channel[0].identifier - - #assert slack.unfurl_refs(case['input'], ignore_alt_text=case.get('ignore_alt_text', False)) == case['output'] +def test_unfurl_refs(case, realish_eventrouter): + slack.EVENTROUTER = realish_eventrouter + result = slack.unfurl_refs( + case['input'], ignore_alt_text=case.get('ignore_alt_text', False)) + assert result == case['output'] 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: diff --git a/weemoji.json b/weemoji.json index eb19849..3b3e045 100644 --- a/weemoji.json +++ b/weemoji.json @@ -1 +1,1348 @@ -{"emoji": ["wine_glass", "flag-tl", "flag-tn", "clock830", "flag-th", "rabbit", "flag-tj", "european_post_office", "flag-nr", "tram", "wink", "flag-tg", "department_store", "flag-ta", "slightly_smiling_face", "alien", "crocodile", "flag-au", "flag-tz", "bulb", "heavy_heart_exclamation_mark_ornament", "bomb", "flag-tv", "golfer", "hole", "flag-tr", "maple_leaf", "building_construction", "face_with_rolling_eyes", "man_with_gua_pi_mao", "person_with_ball", "e-mail", "tv", "open_hands", "sweat_drops", "pager", "file_cabinet", "laughing", "part_alternation_mark", "flag-td", "tm", "mountain_cableway", "melon", "smile", "snow_cloud", "large_blue_circle", "persevere", "sound", "fax", "woman", "eight_pointed_black_star", "thought_balloon", "end", "oncoming_automobile", "wave", "u7a7a", "woman-woman-boy-boy", "flag-cd", "hammer_and_wrench", "ticket", "flag-tc", "ramen", "twisted_rightwards_arrows", "cool", "four", "school", "small_airplane", "high_brightness", "nerd_face", "upside_down_face", "deciduous_tree", "notes", "white_flower", "biohazard_sign", "gun", "video_game", "saxophone", "car", "flag-ic", "notebook_with_decorative_cover", "triumph", "flag-io", "flag-in", "flag-im", "slightly_frowning_face", "black_right_pointing_double_triangle_with_vertical_bar", "tea", "flag-ls", "flag-bn", "dove_of_peace", "flag-ie", "arrow_left", "old_key", "flag-tt", "zero", "small_orange_diamond", "a", "white_square_button", "flag-is", "hankey", "flag-iq", "cactus", "spaghetti", "white_small_square", "ribbon", "flag-it", "toilet", "mega", "abc", "hocho", "flag-sr", "knife_fork_plate", "flag-fo", "purple_heart", "love_letter", "flag-fk", "file_folder", "flag-fi", "clipboard", "baby_bottle", "new", "bird", "flag-ua", "1234", "peace_symbol", "spock-hand", "couch_and_lamp", "no_smoking", "no_bicycles", "herb", "pouting_cat", "vertical_traffic_light", "leo", "house_with_garden", "flag-pm", "baseball", "busstop", "new_moon", "kissing", "man-woman-boy-boy", "100", "flag-na", "boy", "flag-sl", "capital_abcd", "no_entry", "wheel_of_dharma", "metro", "leaves", "heavy_plus_sign", "roller_coaster", "game_die", "man-man-girl-girl", "classical_building", "hamster", "flag-gy", "pick", "popcorn", "cold_sweat", "massage", "fleur_de_lis", "flag-pr", "chains", "flag-pt", "apple", "family", "scales", "sleeping_accommodation", "rice_cracker", "wind_blowing_face", "inbox_tray", "flag-ma", "flag-pa", "green_heart", "mahjong", "flag-pf", "flag-pg", "flag-ph", "flag-ec", "sleuth_or_spy", "clock330", "flag-ca", "dango", "honey_pot", "eye", "keycap_star", "baby", "sake", "confounded", "hospital", "poodle", "frog", "musical_note", "camera", "sleeping", "crescent_moon", "world_map", "aries", "flag-nl", "ear_of_rice", "flag-si", "video_camera", "mouse2", "chestnut", "flag-mg", "guardsman", "clock230", "baby_symbol", "atom_symbol", "steam_locomotive", "man_in_business_suit_levitating", "motor_boat", "tangerine", "blue_heart", "mantelpiece_clock", "recycle", "train", "beers", "water_buffalo", "flag-cz", "first_quarter_moon_with_face", "mailbox_closed", "curly_loop", "lower_left_fountain_pen", "pouch", "flag-ba", "jack_o_lantern", "izakaya_lantern", "palm_tree", "derelict_house_building", "tired_face", "cat", "dizzy", "nine", "chocolate_bar", "v", "running_shirt_with_sash", "ferry", "arrow_lower_left", "put_litter_in_its_place", "coffin", "abcd", "heart", "chart_with_upwards_trend", "arrow_backward", "hamburger", "pushpin", "lock", "flag-eu", "dolphin", "flag-es", "confused", "accept", "night_with_stars", "studio_microphone", "pig2", "white_medium_small_square", "flag-eh", "flag-eg", "sunglasses", "airplane", "trumpet", "flag-ee", "bow", "flag-bj", "clock12", "earth_americas", "see_no_evil", "scorpius", "flag-bo", "flag-bl", "flag-bm", "flag-bb", "mouse", "speedboat", "six", "snowman_without_snow", "ledger", "flag-bd", "flag-be", "flag-bz", "small_blue_diamond", "leftwards_arrow_with_hook", "amphora", "rewind", "flag-br", "no_bell", "flag-mc", "earth_asia", "flag-bv", "goat", "flag-bt", "pizza", "heavy_check_mark", "trident", "briefcase", "cocktail", "kissing_closed_eyes", "sunny", "star_of_david", "flag-bh", "customs", "motorway", "fork_and_knife", "birthday", "fast_forward", "heartpulse", "mag", "taco", "sparkler", "sparkles", "flag-va", "shirt", "tomato", "womens", "octopus", "wheelchair", "volleyball", "dragon", "mostly_sunny", "tulip", "flag-cu", "truck", "wrench", "flag-je", "ambulance", "sa", "point_up_2", "egg", "small_red_triangle", "umbrella_with_rain_drops", "flag-gp", "shield", "office", "mute", "clapper", "flag-bf", "funeral_urn", "haircut", "soon", "flag-bg", "symbols", "black_square_button", "flag-jp", "keyboard", "japan", "post_office", "last_quarter_moon_with_face", "flag-sb", "rosette", "pray", "linked_paperclips", "flushed", "flag-sa", "dark_sunglasses", "dizzy_face", "rugby_football", "currency_exchange", "flag-by", "paperclip", "moneybag", "mailbox_with_no_mail", "man-woman-girl-girl", "sob", "soccer", "dolls", "flag-gr", "coffee", "tiger2", "flag-la", "flag-lb", "neutral_face", "black_right_pointing_triangle_with_double_vertical_bar", "monorail", "elephant", "flag-li", "open_mouth", "bar_chart", "flag-lt", "european_castle", "flag-lv", "page_with_curl", "woman-heart-woman", "snake", "kiss", "blue_car", "confetti_ball", "flag-ly", "bank", "bread", "minidisc", "flag-mt", "flag-bq", "rice_ball", "oncoming_police_car", "capricorn", "point_left", "flag-gw", "tokyo_tower", "barely_sunny", "weary", "flag-bw", "clock930", "fishing_pole_and_fish", "repeat_one", "bowling", "volcano", "older_woman", "railway_car", "smiley_cat", "flag-er", "information_source", "cry", "telescope", "beginner", "earth_africa", "postal_horn", "house", "fish", "construction_worker", "money_mouth_face", "spider", "u7121", "bride_with_veil", "camera_with_flash", "books", "keycap_ten", "fist", "beetle", "lock_with_ink_pen", "8ball", "worried", "weight_lifter", "sunrise", "exclamation", "no_good", "flag-zm", "lipstick", "lower_left_crayon", "flag-ps", "smirk", "racing_car", "card_file_box", "factory", "baggage_claim", "cherry_blossom", "om_symbol", "sparkle", "fountain", "point_right", "cyclone", "-1", "blue_book", "reminder_ribbon", "dancers", "sheep", "flower_playing_cards", "umbrella", "flag-np", "hatching_chick", "black_circle_for_record", "flag-vi", "free", "traffic_light", "five", "grimacing", "cookie", "poultry_leg", "grapes", "raised_hand_with_fingers_splayed", "smirk_cat", "flag-ws", "diamond_shape_with_a_dot_inside", "lollipop", "flag-id", "man-heart-man", "high_heel", "dagger_knife", "black_medium_small_square", "green_book", "flag-kw", "headphones", "no_mobile_phones", "sun_with_face", "mailbox", "mosque", "passport_control", "bookmark", "+1", "notebook", "yum", "closed_lock_with_key", "heartbeat", "man-woman-girl", "blush", "radioactive_sign", "bullettrain_front", "flag-mh", "ophiuchus", "flag-mp", "bouquet", "sports_medal", "flag-uy", "fire_engine", "one", "feet", "date", "flag-vu", "cow2", "scissors", "ring", "disappointed_relieved", "whale", "zap", "children_crossing", "national_park", "clock430", "horse", "basketball", "monkey", "thinking_face", "blossom", "gift_heart", "top", "flag-il", "spider_web", "clock630", "crossed_swords", "station", "clock730", "man", "banana", "flag-mv", "shaved_ice", "eyes", "shell", "waving_white_flag", "gear", "flag-hn", "radio_button", "memo", "hotel", "small_red_triangle_down", "broken_heart", "suspension_railway", "railway_track", "nut_and_bolt", "aerial_tramway", "flag-hr", "seat", "latin_cross", "flag-hu", "panda_face", "middle_finger", "minibus", "b", "unamused", "flag-af", "flag-ae", "flag-ad", "evergreen_tree", "flag-ao", "mailbox_with_mail", "bee", "scream_cat", "smile_cat", "flag-aq", "flag-ve", "flag-aw", "hourglass_flowing_sand", "clock11", "round_pushpin", "tophat", "six_pointed_star", "dog2", "grinning", "tractor", "flag-vc", "u6709", "u6708", "flag-za", "crying_cat_face", "angel", "nail_care", "runner", "table_tennis_paddle_and_ball", "ram", "writing_hand", "bathtub", "ant", "rat", "flag-hk", "information_desk_person", "flag-ir", "rice_scene", "bookmark_tabs", "milky_way", "pencil2", "mountain", "microphone", "koala", "necktie", "atm", "bullettrain_side", "kissing_cat", "relieved", "thermometer", "flag-xk", "u55b6", "globe_with_meridians", "snowflake", "woman-kiss-woman", "loudspeaker", "princess", "printer", "flag-sy", "flag-sx", "flag-sz", "tornado", "flag-st", "flag-sv", "chart", "flag-ss", "credit_card", "flag-sm", "checkered_flag", "flag-so", "flag-sn", "eight", "flag-sh", "flag-sk", "flag-sj", "handbag", "pensive", "flag-sg", "flag-py", "medal", "arrows_clockwise", "flag-sc", "ballot_box_with_check", "eject", "fried_shrimp", "mans_shoe", "card_index_dividers", "m", "dog", "dollar", "police_car", "new_moon_with_face", "shinto_shrine", "ideograph_advantage", "pineapple", "airplane_arriving", "link", "scream", "bell", "speak_no_evil", "walking", "flag-fm", "golf", "satellite_antenna", "flag-fj", "dromedary_camel", "flag-om", "horse_racing", "three_button_mouse", "lower_left_ballpoint_pen", "radio", "flag-cv", "partly_sunny_rain", "point_down", "chicken", "unicorn_face", "umbrella_on_ground", "flag-tm", "copyright", "arrow_lower_right", "city_sunset", "yen", "waning_crescent_moon", "cupid", "mens", "virgo", "libra", "busts_in_silhouette", "straight_ruler", "flag-fr", "two", "rice", "lips", "flag-gs", "flag-ge", "flag-ac", "alarm_clock", "couplekiss", "sagittarius", "flag-dz", "electric_plug", "circus_tent", "flag-gu", "watch", "arrow_up", "bear", "face_with_head_bandage", "frowning", "flag-dm", "incoming_envelope", "flag-do", "watermelon", "rotating_light", "flag-dj", "wedding", "flag-ag", "flag-dg", "flag-gd", "yellow_heart", "gem", "flag-to", "negative_squared_cross_mark", "girl", "rage", "calling", "flag-at", "microscope", "cheese_wedge", "whale2", "x", "interrobang", "japanese_ogre", "fuelpump", "oncoming_taxi", "man_with_turban", "flag-lk", "arrow_up_small", "art", "smiling_imp", "hear_no_evil", "star_and_crescent", "convenience_store", "up", "flag-ye", "flag-cw", "computer", "arrow_down", "vhs", "flag-ky", "parking", "flag-vn", "pisces", "calendar", "flag-al", "hammer", "hourglass", "hibiscus", "shower", "black_joker", "ferris_wheel", "flag-ar", "camping", "bicyclist", "no_mouth", "postbox", "large_blue_diamond", "non-potable_water", "label", "icecream", "admission_tickets", "lower_left_paintbrush", "flag-hm", "diamonds", "champagne", "email", "older_man", "tent", "flag-ax", "raising_hand", "wc", "bed", "zipper_mouth_face", "joy", "hot_pepper", "aquarius", "waving_black_flag", "couple_with_heart", "guitar", "four_leaf_clover", "key", "flag-az", "flag-tk", "dress", "surfer", "statue_of_liberty", "crystal_ball", "cop", "clock1230", "tropical_drink", "cow", "flag-cp", "no_pedestrians", "oncoming_bus", "moyai", "restroom", "white_large_square", "kaaba", "eggplant", "comet", "low_brightness", "flag-tf", "ok_woman", "space_invader", "pig_nose", "flag-kp", "cancer", "ice_skate", "battery", "man-kiss-man", "wastebasket", "jeans", "cd", "flag-ke", "carousel_horse", "hotsprings", "page_facing_up", "flag-mn", "church", "boar", "black_square_for_stop", "flag-dk", "flag-kn", "flag-ki", "flag-kh", "boat", "turkey", "flag-am", "person_with_blond_hair", "swimmer", "wavy_dash", "three", "oden", "secret", "woman-woman-girl-boy", "stadium", "chipmunk", "stuck_out_tongue_closed_eyes", "helicopter", "heavy_division_sign", "flag-mm", "passenger_ship", "u7981", "mushroom", "fire", "two_hearts", "revolving_hearts", "arrow_down_small", "tiger", "desktop_computer", "flag-de", "foggy", "skin-tone-2", "skin-tone-3", "skin-tone-4", "skin-tone-5", "skin-tone-6", "heart_eyes", "open_file_folder", "dash", "blowfish", "speech_balloon", "wind_chime", "arrow_right_hook", "seedling", "fearful", "envelope_with_arrow", "flag-yt", "closed_umbrella", "film_projector", "bikini", "warning", "taxi", "u5408", "newspaper", "card_index", "raised_hands", "anchor", "loop", "flag-zw", "potable_water", "seven", "pound", "two_women_holding_hands", "timer_clock", "flag-rs", "registered", "sushi", "purse", "monkey_face", "u5272", "rooster", "shamrock", "anger", "rain_cloud", "vs", "flag-ro", "flag-pl", "frame_with_picture", "arrow_forward", "violin", "name_badge", "orthodox_cross", "id", "helmet_with_white_cross", "flag-re", "shopping_bags", "synagogue", "house_buildings", "white_circle", "balloon", "flag-lc", "heart_decoration", "flag-mz", "joy_cat", "kimono", "speaker", "flag-my", "train2", "first_quarter_moon", "dragon_face", "left_luggage", "flag-mx", "meat_on_bone", "light_rail", "bellhop_bell", "satellite", "arrow_heading_up", "snail", "black_small_square", "u6307", "leopard", "hand", "flag-bi", "flag-pn", "badminton_racquet_and_shuttlecock", "barber", "christmas_tree", "cityscape", "slot_machine", "ice_cream", "flag-qa", "euro", "anguished", "crossed_flags", "burrito", "rolled_up_newspaper", "musical_score", "white_frowning_face", "triangular_ruler", "ballot_box_with_ballot", "ocean", "flag-kr", "signal_strength", "flags", "the_horns", "hearts", "joystick", "muscle", "love_hotel", "hotdog", "snowman", "eyeglasses", "flag-lr", "rocket", "camel", "flag-gq", "boot", "u7533", "racehorse", "sleepy", "flag-gt", "heart_eyes_cat", "green_apple", "flag-gi", "flag-gh", "racing_motorcycle", "flag-gm", "flag-gl", "flag-gn", "flag-ga", "bridge_at_night", "flag-pe", "flag-gb", "face_with_thermometer", "clock130", "flag-gg", "flag-gf", "flashlight", "womans_hat", "flag-mf", "sandal", "white_medium_square", "snowboarder", "sunflower", "grey_exclamation", "person_frowning", "rose", "cl", "flag-cf", "cherries", "innocent", "arrow_up_down", "stopwatch", "left_speech_bubble", "ski", "pill", "musical_keyboard", "skier", "full_moon", "hugging_face", "flag-bs", "orange_book", "flag-wf", "flag-ug", "mount_fuji", "couple", "yin_yang", "japanese_goblin", "flag-as", "dart", "clock1", "clock2", "clock3", "clock4", "clock5", "clock6", "clock7", "clock8", "clock9", "doughnut", "flag-kz", "disappointed", "grin", "place_of_worship", "womans_clothes", "flag-vg", "gift", "prayer_beads", "bangbang", "stuck_out_tongue_winking_eye", "flag-kg", "candy", "lightning", "arrows_counterclockwise", "two_men_holding_hands", "dancer", "skull_and_crossbones", "trolleybus", "woman-woman-girl-girl", "bamboo", "flag-um", "trackball", "clap", "outbox_tray", "left_right_arrow", "japanese_castle", "door", "waning_gibbous_moon", "crown", "back", "flag-et", "flag-pw", "flag-us", "sparkling_heart", "clubs", "repeat", "candle", "crab", "man-woman-girl-boy", "smoking", "flag-pk", "man-man-boy-boy", "bento", "robot_face", "moon", "thunder_cloud_and_rain", "tanabata_tree", "fog", "cloud", "large_orange_diamond", "heavy_minus_sign", "o2", "nose", "flag-no", "point_up", "smiley", "facepunch", "zzz", "flag-ni", "flag-nf", "flag-ng", "corn", "flag-ne", "flag-nc", "loud_sound", "kissing_smiling_eyes", "fish_cake", "flag-ms", "flag-nz", "stew", "santa", "kissing_heart", "flag-nu", "tropical_fish", "eight_spoked_asterisk", "trophy", "on", "ok", "city_sunrise", "package", "arrow_right", "school_satchel", "o", "film_frames", "chart_with_downwards_trend", "clock10", "hammer_and_pick", "wolf", "sweat", "ox", "flag-rw", "mountain_railway", "tongue", "speaking_head_in_silhouette", "curry", "angry", "alembic", "baby_chick", "double_vertical_bar", "underage", "do_not_litter", "man-man-boy", "field_hockey_stick_and_ball", "waxing_crescent_moon", "full_moon_with_face", "bath", "flag-se", "sos", "red_circle", "flag-sd", "syringe", "last_quarter_moon", "tada", "ok_hand", "custard", "rowboat", "compression", "clock530", "heavy_multiplication_x", "white_check_mark", "tennis", "question", "beer", "flag-jo", "flag-tw", "lion_face", "flag-ru", "stars", "flag-jm", "stuck_out_tongue", "woman-woman-boy", "iphone", "flag-cm", "sweat_smile", "flag-cl", "flag-uz", "bus", "relaxed", "fireworks", "flag-md", "right_anger_bubble", "level_slider", "construction", "black_circle", "fallen_leaf", "airplane_departure", "astonished", "flag-ci", "turtle", "ear", "black_left_pointing_double_triangle_with_vertical_bar", "bug", "penguin", "arrow_heading_down", "congratulations", "snow_capped_mountain", "flag-ck", "skull", "mobile_phone_off", "flag-ht", "control_knobs", "expressionless", "fries", "grey_question", "arrow_upper_left", "strawberry", "cat2", "athletic_shoe", "unlock", "star2", "cake", "gemini", "man-man-girl-boy", "arrow_double_up", "cricket_bat_and_ball", "flag-me", "ab", "hash", "sweet_potato", "mortar_board", "cinema", "flag-mo", "hatched_chick", "triangular_flag_on_post", "flag-ml", "flag-mk", "flag-ai", "black_nib", "pig", "flag-mw", "floppy_disk", "flag-mu", "black_large_square", "koko", "flag-mr", "flag-mq", "person_with_pouting_face", "flag-ea", "bow_and_arrow", "ship", "ice_hockey_stick_and_puck", "telephone_receiver", "performing_arts", "rainbow", "movie_camera", "lemon", "arrow_double_down", "peach", "arrow_upper_right", "ng", "mountain_bicyclist", "book", "clock1130", "boom", "spiral_calendar_pad", "clock1030", "flag-km", "beach_with_umbrella", "imp", "bust_in_silhouette", "star", "rabbit2", "man-man-girl", "footprints", "football", "pear", "taurus", "articulated_lorry", "no_entry_sign", "u6e80", "money_with_wings", "flag-lu", "bike", "black_medium_square", "closed_book", "desert", "woman-woman-girl", "oil_drum", "ghost", "droplet", "flag-co", "flag-cn", "spades", "flag-ch", "vibration_mode", "phone", "dvd", "flag-cg", "menorah_with_nine_branches", "mask", "flag-cc", "mag_right", "scorpion", "flag-cy", "flag-cx", "hushed", "desert_island", "sunrise_over_mountains", "partly_sunny", "spiral_note_pad", "heavy_dollar_sign", "scroll", "flag-cr"]} +{ + "emoji": [ + "+1", + "-1", + "100", + "1234", + "8ball", + "a", + "ab", + "abc", + "abcd", + "accept", + "admission_tickets", + "aerial_tramway", + "airplane", + "airplane_arriving", + "airplane_departure", + "alarm_clock", + "alembic", + "alien", + "ambulance", + "amphora", + "anchor", + "angel", + "anger", + "angry", + "anguished", + "ant", + "apple", + "aquarius", + "aries", + "arrow_backward", + "arrow_double_down", + "arrow_double_up", + "arrow_down", + "arrow_down_small", + "arrow_forward", + "arrow_heading_down", + "arrow_heading_up", + "arrow_left", + "arrow_lower_left", + "arrow_lower_right", + "arrow_right", + "arrow_right_hook", + "arrow_up", + "arrow_up_down", + "arrow_up_small", + "arrow_upper_left", + "arrow_upper_right", + "arrows_clockwise", + "arrows_counterclockwise", + "art", + "articulated_lorry", + "astonished", + "athletic_shoe", + "atm", + "atom_symbol", + "b", + "baby", + "baby_bottle", + "baby_chick", + "baby_symbol", + "back", + "badminton_racquet_and_shuttlecock", + "baggage_claim", + "balloon", + "ballot_box_with_ballot", + "ballot_box_with_check", + "bamboo", + "banana", + "bangbang", + "bank", + "bar_chart", + "barber", + "barely_sunny", + "baseball", + "basketball", + "bath", + "bathtub", + "battery", + "beach_with_umbrella", + "bear", + "bed", + "bee", + "beer", + "beers", + "beetle", + "beginner", + "bell", + "bellhop_bell", + "bento", + "bicyclist", + "bike", + "bikini", + "biohazard_sign", + "bird", + "birthday", + "black_circle", + "black_circle_for_record", + "black_joker", + "black_large_square", + "black_left_pointing_double_triangle_with_vertical_bar", + "black_medium_small_square", + "black_medium_square", + "black_nib", + "black_right_pointing_double_triangle_with_vertical_bar", + "black_right_pointing_triangle_with_double_vertical_bar", + "black_small_square", + "black_square_button", + "black_square_for_stop", + "blossom", + "blowfish", + "blue_book", + "blue_car", + "blue_heart", + "blush", + "boar", + "boat", + "bomb", + "book", + "bookmark", + "bookmark_tabs", + "books", + "boom", + "boot", + "bouquet", + "bow", + "bow_and_arrow", + "bowling", + "boy", + "bread", + "bride_with_veil", + "bridge_at_night", + "briefcase", + "broken_heart", + "bug", + "building_construction", + "bulb", + "bullettrain_front", + "bullettrain_side", + "burrito", + "bus", + "busstop", + "bust_in_silhouette", + "busts_in_silhouette", + "cactus", + "cake", + "calendar", + "calling", + "camel", + "camera", + "camera_with_flash", + "camping", + "cancer", + "candle", + "candy", + "capital_abcd", + "capricorn", + "car", + "card_file_box", + "card_index", + "card_index_dividers", + "carousel_horse", + "cat", + "cat2", + "cd", + "chains", + "champagne", + "chart", + "chart_with_downwards_trend", + "chart_with_upwards_trend", + "checkered_flag", + "cheese_wedge", + "cherries", + "cherry_blossom", + "chestnut", + "chicken", + "children_crossing", + "chipmunk", + "chocolate_bar", + "christmas_tree", + "church", + "cinema", + "circus_tent", + "city_sunrise", + "city_sunset", + "cityscape", + "cl", + "clap", + "clapper", + "classical_building", + "clipboard", + "clock1", + "clock10", + "clock1030", + "clock11", + "clock1130", + "clock12", + "clock1230", + "clock130", + "clock2", + "clock230", + "clock3", + "clock330", + "clock4", + "clock430", + "clock5", + "clock530", + "clock6", + "clock630", + "clock7", + "clock730", + "clock8", + "clock830", + "clock9", + "clock930", + "closed_book", + "closed_lock_with_key", + "closed_umbrella", + "cloud", + "clubs", + "cn", + "cocktail", + "coffee", + "coffin", + "cold_sweat", + "collision", + "comet", + "compression", + "computer", + "confetti_ball", + "confounded", + "confused", + "congratulations", + "construction", + "construction_worker", + "control_knobs", + "convenience_store", + "cookie", + "cool", + "cop", + "copyright", + "corn", + "couch_and_lamp", + "couple", + "couple_with_heart", + "couplekiss", + "cow", + "cow2", + "crab", + "credit_card", + "crescent_moon", + "cricket_bat_and_ball", + "crocodile", + "crossed_flags", + "crossed_swords", + "crown", + "cry", + "crying_cat_face", + "crystal_ball", + "cupid", + "curly_loop", + "currency_exchange", + "curry", + "custard", + "customs", + "cyclone", + "dagger_knife", + "dancer", + "dancers", + "dango", + "dark_sunglasses", + "dart", + "dash", + "date", + "de", + "deciduous_tree", + "department_store", + "derelict_house_building", + "desert", + "desert_island", + "desktop_computer", + "diamond_shape_with_a_dot_inside", + "diamonds", + "disappointed", + "disappointed_relieved", + "dizzy", + "dizzy_face", + "do_not_litter", + "dog", + "dog2", + "dollar", + "dolls", + "dolphin", + "door", + "double_vertical_bar", + "doughnut", + "dove_of_peace", + "dragon", + "dragon_face", + "dress", + "dromedary_camel", + "droplet", + "dvd", + "e-mail", + "ear", + "ear_of_rice", + "earth_africa", + "earth_americas", + "earth_asia", + "egg", + "eggplant", + "eight", + "eight_pointed_black_star", + "eight_spoked_asterisk", + "eject", + "electric_plug", + "elephant", + "email", + "end", + "envelope", + "envelope_with_arrow", + "es", + "euro", + "european_castle", + "european_post_office", + "evergreen_tree", + "exclamation", + "expressionless", + "eye", + "eyeglasses", + "eyes", + "face_with_head_bandage", + "face_with_rolling_eyes", + "face_with_thermometer", + "facepunch", + "factory", + "fallen_leaf", + "family", + "fast_forward", + "fax", + "fearful", + "feet", + "ferris_wheel", + "ferry", + "field_hockey_stick_and_ball", + "file_cabinet", + "file_folder", + "film_frames", + "film_projector", + "fire", + "fire_engine", + "fireworks", + "first_quarter_moon", + "first_quarter_moon_with_face", + "fish", + "fish_cake", + "fishing_pole_and_fish", + "fist", + "five", + "flag-ac", + "flag-ad", + "flag-ae", + "flag-af", + "flag-ag", + "flag-ai", + "flag-al", + "flag-am", + "flag-ao", + "flag-aq", + "flag-ar", + "flag-as", + "flag-at", + "flag-au", + "flag-aw", + "flag-ax", + "flag-az", + "flag-ba", + "flag-bb", + "flag-bd", + "flag-be", + "flag-bf", + "flag-bg", + "flag-bh", + "flag-bi", + "flag-bj", + "flag-bl", + "flag-bm", + "flag-bn", + "flag-bo", + "flag-bq", + "flag-br", + "flag-bs", + "flag-bt", + "flag-bv", + "flag-bw", + "flag-by", + "flag-bz", + "flag-ca", + "flag-cc", + "flag-cd", + "flag-cf", + "flag-cg", + "flag-ch", + "flag-ci", + "flag-ck", + "flag-cl", + "flag-cm", + "flag-cn", + "flag-co", + "flag-cp", + "flag-cr", + "flag-cu", + "flag-cv", + "flag-cw", + "flag-cx", + "flag-cy", + "flag-cz", + "flag-de", + "flag-dg", + "flag-dj", + "flag-dk", + "flag-dm", + "flag-do", + "flag-dz", + "flag-ea", + "flag-ec", + "flag-ee", + "flag-eg", + "flag-eh", + "flag-er", + "flag-es", + "flag-et", + "flag-eu", + "flag-fi", + "flag-fj", + "flag-fk", + "flag-fm", + "flag-fo", + "flag-fr", + "flag-ga", + "flag-gb", + "flag-gd", + "flag-ge", + "flag-gf", + "flag-gg", + "flag-gh", + "flag-gi", + "flag-gl", + "flag-gm", + "flag-gn", + "flag-gp", + "flag-gq", + "flag-gr", + "flag-gs", + "flag-gt", + "flag-gu", + "flag-gw", + "flag-gy", + "flag-hk", + "flag-hm", + "flag-hn", + "flag-hr", + "flag-ht", + "flag-hu", + "flag-ic", + "flag-id", + "flag-ie", + "flag-il", + "flag-im", + "flag-in", + "flag-io", + "flag-iq", + "flag-ir", + "flag-is", + "flag-it", + "flag-je", + "flag-jm", + "flag-jo", + "flag-jp", + "flag-ke", + "flag-kg", + "flag-kh", + "flag-ki", + "flag-km", + "flag-kn", + "flag-kp", + "flag-kr", + "flag-kw", + "flag-ky", + "flag-kz", + "flag-la", + "flag-lb", + "flag-lc", + "flag-li", + "flag-lk", + "flag-lr", + "flag-ls", + "flag-lt", + "flag-lu", + "flag-lv", + "flag-ly", + "flag-ma", + "flag-mc", + "flag-md", + "flag-me", + "flag-mf", + "flag-mg", + "flag-mh", + "flag-mk", + "flag-ml", + "flag-mm", + "flag-mn", + "flag-mo", + "flag-mp", + "flag-mq", + "flag-mr", + "flag-ms", + "flag-mt", + "flag-mu", + "flag-mv", + "flag-mw", + "flag-mx", + "flag-my", + "flag-mz", + "flag-na", + "flag-nc", + "flag-ne", + "flag-nf", + "flag-ng", + "flag-ni", + "flag-nl", + "flag-no", + "flag-np", + "flag-nr", + "flag-nu", + "flag-nz", + "flag-om", + "flag-pa", + "flag-pe", + "flag-pf", + "flag-pg", + "flag-ph", + "flag-pk", + "flag-pl", + "flag-pm", + "flag-pn", + "flag-pr", + "flag-ps", + "flag-pt", + "flag-pw", + "flag-py", + "flag-qa", + "flag-re", + "flag-ro", + "flag-rs", + "flag-ru", + "flag-rw", + "flag-sa", + "flag-sb", + "flag-sc", + "flag-sd", + "flag-se", + "flag-sg", + "flag-sh", + "flag-si", + "flag-sj", + "flag-sk", + "flag-sl", + "flag-sm", + "flag-sn", + "flag-so", + "flag-sr", + "flag-ss", + "flag-st", + "flag-sv", + "flag-sx", + "flag-sy", + "flag-sz", + "flag-ta", + "flag-tc", + "flag-td", + "flag-tf", + "flag-tg", + "flag-th", + "flag-tj", + "flag-tk", + "flag-tl", + "flag-tm", + "flag-tn", + "flag-to", + "flag-tr", + "flag-tt", + "flag-tv", + "flag-tw", + "flag-tz", + "flag-ua", + "flag-ug", + "flag-um", + "flag-us", + "flag-uy", + "flag-uz", + "flag-va", + "flag-vc", + "flag-ve", + "flag-vg", + "flag-vi", + "flag-vn", + "flag-vu", + "flag-wf", + "flag-ws", + "flag-xk", + "flag-ye", + "flag-yt", + "flag-za", + "flag-zm", + "flag-zw", + "flags", + "flashlight", + "fleur_de_lis", + "flipper", + "floppy_disk", + "flower_playing_cards", + "flushed", + "fog", + "foggy", + "football", + "footprints", + "fork_and_knife", + "fountain", + "four", + "four_leaf_clover", + "fr", + "frame_with_picture", + "free", + "fried_shrimp", + "fries", + "frog", + "frowning", + "fuelpump", + "full_moon", + "full_moon_with_face", + "funeral_urn", + "game_die", + "gb", + "gear", + "gem", + "gemini", + "ghost", + "gift", + "gift_heart", + "girl", + "globe_with_meridians", + "goat", + "golf", + "golfer", + "grapes", + "green_apple", + "green_book", + "green_heart", + "grey_exclamation", + "grey_question", + "grimacing", + "grin", + "grinning", + "guardsman", + "guitar", + "gun", + "haircut", + "hamburger", + "hammer", + "hammer_and_pick", + "hammer_and_wrench", + "hamster", + "hand", + "handbag", + "hankey", + "hash", + "hatched_chick", + "hatching_chick", + "headphones", + "hear_no_evil", + "heart", + "heart_decoration", + "heart_eyes", + "heart_eyes_cat", + "heartbeat", + "heartpulse", + "hearts", + "heavy_check_mark", + "heavy_division_sign", + "heavy_dollar_sign", + "heavy_exclamation_mark", + "heavy_heart_exclamation_mark_ornament", + "heavy_minus_sign", + "heavy_multiplication_x", + "heavy_plus_sign", + "helicopter", + "helmet_with_white_cross", + "herb", + "hibiscus", + "high_brightness", + "high_heel", + "hocho", + "hole", + "honey_pot", + "honeybee", + "horse", + "horse_racing", + "hospital", + "hot_pepper", + "hotdog", + "hotel", + "hotsprings", + "hourglass", + "hourglass_flowing_sand", + "house", + "house_buildings", + "house_with_garden", + "hugging_face", + "hushed", + "ice_cream", + "ice_hockey_stick_and_puck", + "ice_skate", + "icecream", + "id", + "ideograph_advantage", + "imp", + "inbox_tray", + "incoming_envelope", + "information_desk_person", + "information_source", + "innocent", + "interrobang", + "iphone", + "it", + "izakaya_lantern", + "jack_o_lantern", + "japan", + "japanese_castle", + "japanese_goblin", + "japanese_ogre", + "jeans", + "joy", + "joy_cat", + "joystick", + "jp", + "kaaba", + "key", + "keyboard", + "keycap_star", + "keycap_ten", + "kimono", + "kiss", + "kissing", + "kissing_cat", + "kissing_closed_eyes", + "kissing_heart", + "kissing_smiling_eyes", + "knife", + "knife_fork_plate", + "koala", + "koko", + "kr", + "label", + "lantern", + "large_blue_circle", + "large_blue_diamond", + "large_orange_diamond", + "last_quarter_moon", + "last_quarter_moon_with_face", + "latin_cross", + "laughing", + "leaves", + "ledger", + "left_luggage", + "left_right_arrow", + "left_speech_bubble", + "leftwards_arrow_with_hook", + "lemon", + "leo", + "leopard", + "level_slider", + "libra", + "light_rail", + "lightning", + "lightning_cloud", + "link", + "linked_paperclips", + "lion_face", + "lips", + "lipstick", + "lock", + "lock_with_ink_pen", + "lollipop", + "loop", + "loud_sound", + "loudspeaker", + "love_hotel", + "love_letter", + "low_brightness", + "lower_left_ballpoint_pen", + "lower_left_crayon", + "lower_left_fountain_pen", + "lower_left_paintbrush", + "m", + "mag", + "mag_right", + "mahjong", + "mailbox", + "mailbox_closed", + "mailbox_with_mail", + "mailbox_with_no_mail", + "man", + "man-heart-man", + "man-kiss-man", + "man-man-boy", + "man-man-boy-boy", + "man-man-girl", + "man-man-girl-boy", + "man-man-girl-girl", + "man-woman-boy", + "man-woman-boy-boy", + "man-woman-girl", + "man-woman-girl-boy", + "man-woman-girl-girl", + "man_and_woman_holding_hands", + "man_in_business_suit_levitating", + "man_with_gua_pi_mao", + "man_with_turban", + "mans_shoe", + "mantelpiece_clock", + "maple_leaf", + "mask", + "massage", + "meat_on_bone", + "medal", + "mega", + "melon", + "memo", + "menorah_with_nine_branches", + "mens", + "metro", + "microphone", + "microscope", + "middle_finger", + "milky_way", + "minibus", + "minidisc", + "mobile_phone_off", + "money_mouth_face", + "money_with_wings", + "moneybag", + "monkey", + "monkey_face", + "monorail", + "moon", + "mortar_board", + "mosque", + "mostly_sunny", + "motor_boat", + "motorway", + "mount_fuji", + "mountain", + "mountain_bicyclist", + "mountain_cableway", + "mountain_railway", + "mouse", + "mouse2", + "movie_camera", + "moyai", + "muscle", + "mushroom", + "musical_keyboard", + "musical_note", + "musical_score", + "mute", + "nail_care", + "name_badge", + "national_park", + "necktie", + "negative_squared_cross_mark", + "nerd_face", + "neutral_face", + "new", + "new_moon", + "new_moon_with_face", + "newspaper", + "ng", + "night_with_stars", + "nine", + "no_bell", + "no_bicycles", + "no_entry", + "no_entry_sign", + "no_good", + "no_mobile_phones", + "no_mouth", + "no_pedestrians", + "no_smoking", + "non-potable_water", + "nose", + "notebook", + "notebook_with_decorative_cover", + "notes", + "nut_and_bolt", + "o", + "o2", + "ocean", + "octopus", + "oden", + "office", + "oil_drum", + "ok", + "ok_hand", + "ok_woman", + "old_key", + "older_man", + "older_woman", + "om_symbol", + "on", + "oncoming_automobile", + "oncoming_bus", + "oncoming_police_car", + "oncoming_taxi", + "one", + "open_book", + "open_file_folder", + "open_hands", + "open_mouth", + "ophiuchus", + "orange_book", + "orthodox_cross", + "outbox_tray", + "ox", + "package", + "page_facing_up", + "page_with_curl", + "pager", + "palm_tree", + "panda_face", + "paperclip", + "parking", + "part_alternation_mark", + "partly_sunny", + "partly_sunny_rain", + "passenger_ship", + "passport_control", + "paw_prints", + "peace_symbol", + "peach", + "pear", + "pencil", + "pencil2", + "penguin", + "pensive", + "performing_arts", + "persevere", + "person_frowning", + "person_with_ball", + "person_with_blond_hair", + "person_with_pouting_face", + "phone", + "pick", + "pig", + "pig2", + "pig_nose", + "pill", + "pineapple", + "pisces", + "pizza", + "place_of_worship", + "point_down", + "point_left", + "point_right", + "point_up", + "point_up_2", + "police_car", + "poodle", + "poop", + "popcorn", + "post_office", + "postal_horn", + "postbox", + "potable_water", + "pouch", + "poultry_leg", + "pound", + "pouting_cat", + "pray", + "prayer_beads", + "princess", + "printer", + "punch", + "purple_heart", + "purse", + "pushpin", + "put_litter_in_its_place", + "question", + "rabbit", + "rabbit2", + "racehorse", + "racing_car", + "racing_motorcycle", + "radio", + "radio_button", + "radioactive_sign", + "rage", + "railway_car", + "railway_track", + "rain_cloud", + "rainbow", + "raised_hand", + "raised_hand_with_fingers_splayed", + "raised_hands", + "raising_hand", + "ram", + "ramen", + "rat", + "recycle", + "red_car", + "red_circle", + "registered", + "relaxed", + "relieved", + "reminder_ribbon", + "repeat", + "repeat_one", + "restroom", + "reversed_hand_with_middle_finger_extended", + "revolving_hearts", + "rewind", + "ribbon", + "rice", + "rice_ball", + "rice_cracker", + "rice_scene", + "right_anger_bubble", + "ring", + "robot_face", + "rocket", + "rolled_up_newspaper", + "roller_coaster", + "rooster", + "rose", + "rosette", + "rotating_light", + "round_pushpin", + "rowboat", + "ru", + "rugby_football", + "runner", + "running", + "running_shirt_with_sash", + "sa", + "sagittarius", + "sailboat", + "sake", + "sandal", + "santa", + "satellite", + "satellite_antenna", + "satisfied", + "saxophone", + "scales", + "school", + "school_satchel", + "scissors", + "scorpion", + "scorpius", + "scream", + "scream_cat", + "scroll", + "seat", + "secret", + "see_no_evil", + "seedling", + "seven", + "shamrock", + "shaved_ice", + "sheep", + "shell", + "shield", + "shinto_shrine", + "ship", + "shirt", + "shit", + "shoe", + "shopping_bags", + "shower", + "sign_of_the_horns", + "signal_strength", + "six", + "six_pointed_star", + "ski", + "skier", + "skin-tone-2", + "skin-tone-3", + "skin-tone-4", + "skin-tone-5", + "skin-tone-6", + "skull", + "skull_and_crossbones", + "sleeping", + "sleeping_accommodation", + "sleepy", + "sleuth_or_spy", + "slightly_frowning_face", + "slightly_smiling_face", + "slot_machine", + "small_airplane", + "small_blue_diamond", + "small_orange_diamond", + "small_red_triangle", + "small_red_triangle_down", + "smile", + "smile_cat", + "smiley", + "smiley_cat", + "smiling_imp", + "smirk", + "smirk_cat", + "smoking", + "snail", + "snake", + "snow_capped_mountain", + "snow_cloud", + "snowboarder", + "snowflake", + "snowman", + "snowman_without_snow", + "sob", + "soccer", + "soon", + "sos", + "sound", + "space_invader", + "spades", + "spaghetti", + "sparkle", + "sparkler", + "sparkles", + "sparkling_heart", + "speak_no_evil", + "speaker", + "speaking_head_in_silhouette", + "speech_balloon", + "speedboat", + "spider", + "spider_web", + "spiral_calendar_pad", + "spiral_note_pad", + "spock-hand", + "sports_medal", + "stadium", + "star", + "star2", + "star_and_crescent", + "star_of_david", + "stars", + "station", + "statue_of_liberty", + "steam_locomotive", + "stew", + "stopwatch", + "straight_ruler", + "strawberry", + "stuck_out_tongue", + "stuck_out_tongue_closed_eyes", + "stuck_out_tongue_winking_eye", + "studio_microphone", + "sun_behind_cloud", + "sun_behind_rain_cloud", + "sun_small_cloud", + "sun_with_face", + "sunflower", + "sunglasses", + "sunny", + "sunrise", + "sunrise_over_mountains", + "surfer", + "sushi", + "suspension_railway", + "sweat", + "sweat_drops", + "sweat_smile", + "sweet_potato", + "swimmer", + "symbols", + "synagogue", + "syringe", + "table_tennis_paddle_and_ball", + "taco", + "tada", + "tanabata_tree", + "tangerine", + "taurus", + "taxi", + "tea", + "telephone", + "telephone_receiver", + "telescope", + "tennis", + "tent", + "the_horns", + "thermometer", + "thinking_face", + "thought_balloon", + "three", + "three_button_mouse", + "thumbsdown", + "thumbsup", + "thunder_cloud_and_rain", + "ticket", + "tiger", + "tiger2", + "timer_clock", + "tired_face", + "tm", + "toilet", + "tokyo_tower", + "tomato", + "tongue", + "top", + "tophat", + "tornado", + "tornado_cloud", + "trackball", + "tractor", + "traffic_light", + "train", + "train2", + "tram", + "triangular_flag_on_post", + "triangular_ruler", + "trident", + "triumph", + "trolleybus", + "trophy", + "tropical_drink", + "tropical_fish", + "truck", + "trumpet", + "tshirt", + "tulip", + "turkey", + "turtle", + "tv", + "twisted_rightwards_arrows", + "two", + "two_hearts", + "two_men_holding_hands", + "two_women_holding_hands", + "u5272", + "u5408", + "u55b6", + "u6307", + "u6708", + "u6709", + "u6e80", + "u7121", + "u7533", + "u7981", + "u7a7a", + "uk", + "umbrella", + "umbrella_on_ground", + "umbrella_with_rain_drops", + "unamused", + "underage", + "unicorn_face", + "unlock", + "up", + "upside_down_face", + "us", + "v", + "vertical_traffic_light", + "vhs", + "vibration_mode", + "video_camera", + "video_game", + "violin", + "virgo", + "volcano", + "volleyball", + "vs", + "walking", + "waning_crescent_moon", + "waning_gibbous_moon", + "warning", + "wastebasket", + "watch", + "water_buffalo", + "watermelon", + "wave", + "waving_black_flag", + "waving_white_flag", + "wavy_dash", + "waxing_crescent_moon", + "waxing_gibbous_moon", + "wc", + "weary", + "wedding", + "weight_lifter", + "whale", + "whale2", + "wheel_of_dharma", + "wheelchair", + "white_check_mark", + "white_circle", + "white_flower", + "white_frowning_face", + "white_large_square", + "white_medium_small_square", + "white_medium_square", + "white_small_square", + "white_square_button", + "wind_blowing_face", + "wind_chime", + "wine_glass", + "wink", + "wolf", + "woman", + "woman-heart-woman", + "woman-kiss-woman", + "woman-woman-boy", + "woman-woman-boy-boy", + "woman-woman-girl", + "woman-woman-girl-boy", + "woman-woman-girl-girl", + "womans_clothes", + "womans_hat", + "womens", + "world_map", + "worried", + "wrench", + "writing_hand", + "x", + "yellow_heart", + "yen", + "yin_yang", + "yum", + "zap", + "zero", + "zipper_mouth_face", + "zzz" + ] +} |