aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore3
-rw-r--r--README.md16
-rw-r--r--_pytest/conftest.py10
-rw-r--r--_pytest/test_everything.py2
-rw-r--r--_pytest/test_linkifytext.py9
-rw-r--r--_pytest/test_presencechange.py2
-rw-r--r--_pytest/test_process_message.py2
-rw-r--r--_pytest/test_processreply.py2
-rw-r--r--_pytest/test_processteamjoin.py2
-rw-r--r--_pytest/test_sendmessage.py2
-rw-r--r--_pytest/test_unfurl.py36
-rw-r--r--wee_slack.py510
-rw-r--r--weemoji.json1349
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-*
diff --git a/README.md b/README.md
index b162d14..0ab5ce5 100644
--- a/README.md
+++ b/README.md
@@ -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 == '&amp; &lt; &gt; \' "'
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("&lt;", "<")
- text = text.replace("&gt;", ">")
- text = text.replace("&amp;", "&")
- 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('&', '&amp;')
+ .replace('<', '&lt;')
+ .replace('>', '&gt;')
+ .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("&lt;", "<") \
+ .replace("&gt;", ">") \
+ .replace("&amp;", "&")
+
+
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"
+ ]
+}