aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.flake87
-rw-r--r--.gitignore3
-rw-r--r--.travis.yml13
-rw-r--r--README.md119
-rw-r--r--_pytest/conftest.py18
-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_topic_command.py96
-rw-r--r--_pytest/test_unfurl.py59
-rw-r--r--_pytest/test_unwrap_attachments.py150
-rw-r--r--_pytest/test_utf8_helpers.py72
-rw-r--r--wee_slack.py1917
-rw-r--r--weemoji.json1349
18 files changed, 3192 insertions, 632 deletions
diff --git a/.flake8 b/.flake8
new file mode 100644
index 0000000..b4d95b8
--- /dev/null
+++ b/.flake8
@@ -0,0 +1,7 @@
+[flake8]
+select = E901,E999,F821,F822,F823
+count = True
+max-complexity = 10
+max-line-length = 120
+show-source = True
+statistics = True
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/.travis.yml b/.travis.yml
index 6739e1c..a163da4 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,10 +1,19 @@
language: python
python:
- "2.7"
+cache: pip
+
install:
- - 'pip install websocket-client pytest'
+ - pip install flake8 pytest websocket-client
+
+before_script:
+ # stop the build if there are Python syntax errors or undefined names
+ - flake8 .
+ # exit-zero treats all errors as warnings.
+ - flake8 . --exit-zero --select=C,E,F,W
-script: python -m pytest
+script:
+ - python -m pytest
notifications:
irc: "chat.freenode.net#wee-slack-dev"
diff --git a/README.md b/README.md
index 7a207dd..d3a1e22 100644
--- a/README.md
+++ b/README.md
@@ -1,29 +1,24 @@
-
-
wee-slack
=========
-**News:**
- 1.0-RC1 is here. It is a pretty massive refactor, and fixes many of the current issues listed on github. Because there was no good way to do this, it breaks some/many existing PRs. _(please report bugs in #wee-slack on freenode)_
-
A WeeChat native client for Slack.com. Provides supplemental features only available in the web/mobile clients such as: synchronizing read markers, typing notification, threads (and more)! Connects via the Slack API, and maintains a persistent websocket for notification of events.
-![animated screenshot](https://github.com/rawdigits/wee-slack/raw/master/docs/slack.gif)
+![animated screenshot](https://github.com/wee-slack/wee-slack/raw/master/docs/slack.gif)
Features
--------
- * **New** [Threads](#threads) support!
- * Slash commands (including custom ones!)
- * Upload to slack capabilities!
- * Emoji reactions!
+ * [Threads](#threads) support
+ * [Slack Status](#status) support
+ * Slash commands (including custom ones)
+ * Upload to slack capabilities
+ * Emoji reactions
* Edited messages work just like the official clients, where the original message changes and has (edited) appended.
* Unfurled urls dont generate a new message, but replace the original with more info as it is received.
* Regex style message editing (s/oldtext/newtext/)
- * Caches message history, making startup MUCH faster
* Smarter redraw of dynamic buffer info (much lower CPU %)
* beta UTF-8 support
* Doesn't use IRC gateway. Connects directly with Slack via API/Websocket
- * Multiple Teams supported! Just add multiple api tokens separated by commas
+ * Multiple Teams supported. Just add multiple api tokens separated by commas
* Replays history automatically during startup. (and sets read marker to the correct position in history)
* Open channels synchronized with Slack. When you open/close a channel on another client it is reflected in wee-slack
* Colorized nicks in buffer list when used with buffers.pl
@@ -33,7 +28,7 @@ Features
* Away/back status handling
* Expands/shows metadata for things like tweets/links
* Displays edited messages (slack.com irc mode currently doesn't show these)
- * *Super fun* debug mode. See what the websocket is saying with `/slack debug`
+ * *Super fun* debug mode. See what the websocket is saying
In Development
--------------
@@ -75,7 +70,7 @@ pkg install py27-websocket-client py27-six
#### 2. copy wee_slack.py to ~/.weechat/python/autoload
```
-wget https://raw.githubusercontent.com/rawdigits/wee-slack/master/wee_slack.py
+wget https://raw.githubusercontent.com/wee-slack/wee-slack/master/wee_slack.py
cp wee_slack.py ~/.weechat/python/autoload
```
@@ -87,23 +82,48 @@ weechat
**NOTE:** If weechat is already running, the script can be loaded using ``/python load python/autoload/wee_slack.py``
#### 4. Add your Slack API key(s)
+
+Log in to Slack:
+
```
-/set plugins.var.python.slack_extension.slack_api_token [YOUR_SLACK_TOKEN]
+/slack register
+```
+
+This command prints a link you should open in your browser to authorize WeeChat
+with Slack. Once you've accomplished this, copy the "code" portion of the URL in
+the browser and pass it to this command:
+
+```
+/slack register [YOUR_SLACK_TOKEN]
+```
+
+Your Slack team is now added, and you can complete setup by restarting the
+wee-slack plugin.
+
+```
+/python reload slack
+```
+
+Alternatively, you can click the "Request token" button at the
+[Slack legacy token page](https://api.slack.com/custom-integrations/legacy-tokens),
+and paste it directly into your settings:
+
+```
+/set plugins.var.python.slack.slack_api_token [YOUR_SLACK_TOKEN]
```
-^^ (find this at https://api.slack.com/custom-integrations/legacy-tokens using the "Request token" button)
If you don't want to store your API token in plaintext you can use the secure features of weechat:
```
/secure passphrase this is a super secret password
/secure set slack_token [YOUR_SLACK_TOKEN]
-/set plugins.var.python.slack_extension.slack_api_token ${sec.data.slack_token}
+/set plugins.var.python.slack.slack_api_token ${sec.data.slack_token}
```
##### Optional: If you would like to connect to multiple groups, use the above command with multiple tokens separated by commas. (NO SPACES)
```
-/set plugins.var.python.slack_extension.slack_api_token [token1],[token2],[token3]
+/set plugins.var.python.slack.slack_api_token [token1],[token2],[token3]
```
### 5. $PROFIT$
@@ -118,13 +138,12 @@ Commands
Join a channel:
```
/join [channel]
-/slack join [channel]
```
-Start a direct chat with someone:
+Start a direct chat with someone or multiple users:
```
-/query [username]
-/slack talk [username]
+/query <username>[,<username2>[,<username3>...]]
+/slack talk <username>[,<username2>[,<username3>...]]
```
List channels:
@@ -155,6 +174,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
@@ -171,11 +195,6 @@ Add a reaction to the nth last message. The number can be omitted and defaults t
3+:smile:
```
-Set all read markers to a specific time:
-```
-/slack setallreadmarkers (time in epoch)
-```
-
Upload a file to the current slack buffer:
```
/slack upload [file_path]
@@ -186,9 +205,10 @@ Run a Slack slash command. Simply prepend `/slack slash` to what you'd type in t
/slack slash /desiredcommand arg1 arg2 arg3
```
-Debug mode:
+To send a command as a normal message instead of performing the action, prefix it with a slash or a space, like so:
```
-/slack debug
+//slack
+ s/a/b/
```
#### Threads
@@ -209,27 +229,37 @@ Label a thread with a memorable name. The above command will open a channel call
```
_Note: labels do not persist once a thread buffer is closed_
-Optional settings
------------------
+#### Status
-Turn off colorized nicks:
+Set your Slack status on a given team:
```
-/set plugins.var.python.slack_extension.colorize_nicks 0
+/slack status [:emoji:] [Status message]
```
-Turn on colorized messages (messages match nick color):
+Example:
```
-/set plugins.var.python.slack_extension.colorize_nicks 1
+/slack status :ghost: Boo!
```
+#### Emoji tab completions
+
+To enable tab completion of emojis, copy or symlink the `weemoji.json` file to your weechat config directory (e.g. `~/.weechat`). Then append `|%(emoji)` to the `weechat.completion.default_template` config option, e.g. like this:
+
+```
+/set weechat.completion.default_template "%(nicks)|%(irc_channels)|%(emoji)"
+```
+
+Optional settings
+-----------------
+
Set channel prefix to something other than my-slack-subdomain.slack.com (e.g. when using buffers.pl):
```
-/set plugins.var.python.slack_extension.server_alias.my-slack-subdomain "mysub"
+/set plugins.var.python.slack.server_aliases "my-slack-subdomain:mysub,other-domain:coolbeans"
```
Show who added each reaction. Makes reactions appear like `[:smile:(@nick1,@nick2)]` instead of `[:smile:2]`.
```
-/set plugins.var.python.slack_extension.show_reaction_nicks on
+/set plugins.var.python.slack.show_reaction_nicks on
```
Show typing notification in main bar (slack_typing_notice):
@@ -242,6 +272,21 @@ Show channel name in hotlist after activity
/set weechat.look.hotlist_names_level 14
```
+Debugging
+--------------
+
+Enable debug mode and change debug level (default 3, decrease to increase logging and vice versa):
+```
+/set plugins.var.python.slack.debug_mode on
+/set plugins.var.python.slack.debug_level 2
+```
+
+Dump the JSON responses in `/tmp/weeslack-debug/`. Requires a script reload.
+```
+/set plugins.var.python.slack.record_events true
+```
+
+
Support
--------------
diff --git a/_pytest/conftest.py b/_pytest/conftest.py
index 7a6f39a..74ed537 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]
@@ -48,7 +49,9 @@ class FakeWeechat():
this is the thing that acts as "w." everywhere..
basically mock out all of the weechat calls here i guess
"""
- WEECHAT_RC_OK = True
+ WEECHAT_RC_ERROR = 0
+ WEECHAT_RC_OK = 1
+ WEECHAT_RC_OK_EAT = 2
def __init__(self):
pass
@@ -71,6 +74,14 @@ class FakeWeechat():
return "0x8a8a8a8b"
def prefix(self, type):
return ""
+ def config_get_plugin(self, key):
+ return ""
+ def config_get(self, key):
+ return ""
+ def config_string(self, key):
+ return ""
+ def color(self, name):
+ return ""
def __getattr__(self, name):
def method(*args):
pass
@@ -87,6 +98,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_topic_command.py b/_pytest/test_topic_command.py
new file mode 100644
index 0000000..9d9a35e
--- /dev/null
+++ b/_pytest/test_topic_command.py
@@ -0,0 +1,96 @@
+import wee_slack
+from wee_slack import parse_topic_command, topic_command_cb
+from mock import patch
+
+
+def test_parse_topic_without_arguments():
+ channel_name, topic = parse_topic_command('/topic')
+
+ assert channel_name is None
+ assert topic is None
+
+
+def test_parse_topic_with_text():
+ channel_name, topic = parse_topic_command('/topic some topic text')
+
+ assert channel_name is None
+ assert topic == 'some topic text'
+
+
+def test_parse_topic_with_delete():
+ channel_name, topic = parse_topic_command('/topic -delete')
+
+ assert channel_name is None
+ assert topic == ''
+
+
+def test_parse_topic_with_channel():
+ channel_name, topic = parse_topic_command('/topic #general')
+
+ assert channel_name == 'general'
+ assert topic is None
+
+
+def test_parse_topic_with_channel_and_text():
+ channel_name, topic = parse_topic_command(
+ '/topic #general some topic text')
+
+ assert channel_name == 'general'
+ assert topic == 'some topic text'
+
+
+def test_parse_topic_with_channel_and_delete():
+ channel_name, topic = parse_topic_command('/topic #general -delete')
+
+ assert channel_name == 'general'
+ assert topic == ''
+
+
+def test_call_topic_without_arguments(realish_eventrouter):
+ team = realish_eventrouter.teams.values()[-1]
+ channel = team.channels.values()[-1]
+ current_buffer = channel.channel_buffer
+ wee_slack.EVENTROUTER = realish_eventrouter
+
+ command = '/topic'
+
+ with patch('wee_slack.w.prnt') as fake_prnt:
+ result = topic_command_cb(None, current_buffer, command)
+ fake_prnt.assert_called_with(
+ channel.channel_buffer,
+ 'Topic for {} is "{}"'.format(channel.name, channel.topic),
+ )
+ assert result == wee_slack.w.WEECHAT_RC_OK_EAT
+
+
+def test_call_topic_with_unknown_channel(realish_eventrouter):
+ team = realish_eventrouter.teams.values()[-1]
+ channel = team.channels.values()[-1]
+ current_buffer = channel.channel_buffer
+ wee_slack.EVENTROUTER = realish_eventrouter
+
+ command = '/topic #nonexisting'
+
+ with patch('wee_slack.w.prnt') as fake_prnt:
+ result = topic_command_cb(None, current_buffer, command)
+ fake_prnt.assert_called_with(
+ team.channel_buffer,
+ "#nonexisting: No such channel",
+ )
+ assert result == wee_slack.w.WEECHAT_RC_OK_EAT
+
+
+def test_call_topic_with_channel_and_string(realish_eventrouter):
+ team = realish_eventrouter.teams.values()[-1]
+ channel = team.channels.values()[-1]
+ current_buffer = channel.channel_buffer
+ wee_slack.EVENTROUTER = realish_eventrouter
+
+ command = '/topic #general new topic'
+
+ result = topic_command_cb(None, current_buffer, command)
+ request = realish_eventrouter.queue[-1]
+ assert request.request == 'channels.setTopic'
+ assert request.post_data == {
+ 'channel': 'C407ABS94', 'token': 'xoxoxoxox', 'topic': 'new topic'}
+ assert result == wee_slack.w.WEECHAT_RC_OK_EAT
diff --git a/_pytest/test_unfurl.py b/_pytest/test_unfurl.py
index b631888..40674b4 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,44 @@ slack = wee_slack
'output': "url: https://example.com (example with spaces) suffix",
},
{
- 'input': "<@U2147483697|@othernick> multiple unfurl <https://example.com|example with spaces>",
+ 'input': "url: <https://example.com|example.com> suffix",
+ 'output': "url: example.com suffix",
+ 'auto_link_display': 'text',
+ },
+ {
+ 'input': "url: <https://example.com|different text> suffix",
+ 'output': "url: https://example.com (different text) suffix",
+ 'auto_link_display': 'text',
+ },
+ {
+ 'input': "url: <https://example.com|different text> suffix",
+ 'output': "url: https://example.com (different text) suffix",
+ 'auto_link_display': 'url',
+ },
+ {
+ 'input': "url: <https://example.com|example.com> suffix",
+ 'output': "url: https://example.com suffix",
+ 'auto_link_display': 'url',
+ },
+ {
+ '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),
+ auto_link_display=case.get('auto_link_display', 'both'),
+ )
+ assert result == case['output']
diff --git a/_pytest/test_unwrap_attachments.py b/_pytest/test_unwrap_attachments.py
new file mode 100644
index 0000000..ee5bd8b
--- /dev/null
+++ b/_pytest/test_unwrap_attachments.py
@@ -0,0 +1,150 @@
+import wee_slack
+import pytest
+
+
+@pytest.mark.parametrize('case', (
+ {
+ 'input_message': {'attachments': [{
+ 'title': 'Title',
+ }]},
+ 'input_text_before': "Text before",
+ 'output': "\n".join([
+ "",
+ "Title",
+ ]),
+ },
+ {
+ 'input_message': {'attachments': [{
+ 'title': 'Title',
+ 'text': 'Attachment text',
+ 'title_link': 'http://title.link',
+ 'from_url': 'http://from.url',
+ 'fallback': 'Fallback',
+ }]},
+ 'input_text_before': "",
+ 'output': "\n".join([
+ "Title (http://title.link)",
+ "http://from.url",
+ "Attachment text",
+ ]),
+ },
+ {
+ 'input_message': {'attachments': [{
+ 'title': 'Title',
+ 'text': 'Attachment text',
+ 'title_link': 'http://link?a=1&b=2',
+ 'from_url': 'http://link?a=1&b=2',
+ }]},
+ 'input_text_before': "http://link?a=1&amp;b=2",
+ 'output': "\n".join([
+ "",
+ "Title",
+ "Attachment text",
+ ]),
+ },
+ {
+ 'input_message': {'attachments': [{
+ 'title': 'Title',
+ 'text': 'Attachment text',
+ 'title_link': 'http://link',
+ 'from_url': 'http://link',
+ }]},
+ 'input_text_before': "",
+ 'output': "\n".join([
+ "Title (http://link)",
+ "Attachment text",
+ ]),
+ },
+ {
+ 'input_message': {'attachments': [{
+ 'title': 'Title',
+ 'text': 'Attachment text\n\n\nWith multiple lines',
+ }]},
+ 'input_text_before': "",
+ 'output': "\n".join([
+ "Title",
+ "Attachment text\nWith multiple lines",
+ ]),
+ },
+ {
+ 'input_message': {'attachments': [{
+ 'title': 'Title',
+ 'author_name': 'Author',
+ 'pretext': 'Pretext',
+ 'text': 'Attachment text',
+ 'title_link': 'http://title.link',
+ 'from_url': 'http://from.url',
+ }]},
+ 'input_text_before': "",
+ 'output': "\n".join([
+ "Pretext",
+ "Author: Title (http://title.link)",
+ "http://from.url",
+ "Attachment text",
+ ]),
+ },
+ {
+ 'input_message': {'attachments': [{
+ 'author_name': 'Author',
+ 'text': 'Attachment text',
+ 'title_link': 'http://title.link',
+ 'from_url': 'http://from.url',
+ }]},
+ 'input_text_before': "",
+ 'output': "\n".join([
+ "http://from.url",
+ "Author: Attachment text",
+ ]),
+ },
+ {
+ 'input_message': {'attachments': [{
+ 'fallback': 'Fallback',
+ }]},
+ 'input_text_before': "",
+ 'output': "Fallback",
+ },
+ {
+ 'input_message': {'attachments': [{
+ 'title': 'Title',
+ 'fields': [{
+ 'title': 'First field title',
+ 'value': 'First field value',
+ }, {
+ 'title': '',
+ 'value': 'Second field value',
+ }],
+ }]},
+ 'input_text_before': "",
+ 'output': "\n".join([
+ "Title",
+ "First field title First field value",
+ "Second field value",
+ ]),
+ },
+ {
+ 'input_message': {'attachments': [{
+ 'title': 'First attachment title',
+ 'text': 'First attachment text',
+ 'title_link': 'http://title.link.1',
+ 'from_url': 'http://from.url.1',
+ }, {
+ 'title': 'Second attachment title',
+ 'text': 'Second attachment text',
+ 'title_link': 'http://title.link.2',
+ 'from_url': 'http://from.url.2',
+ }]},
+ 'input_text_before': "",
+ 'output': "\n".join([
+ "First attachment title (http://title.link.1)",
+ "http://from.url.1",
+ "First attachment text",
+ "Second attachment title (http://title.link.2)",
+ "http://from.url.2",
+ "Second attachment text",
+ ]),
+ },
+))
+def test_unwrap_attachments(case):
+ result = wee_slack.unwrap_attachments(
+ case['input_message'], case['input_text_before'])
+ assert result == case['output']
diff --git a/_pytest/test_utf8_helpers.py b/_pytest/test_utf8_helpers.py
new file mode 100644
index 0000000..33c66ce
--- /dev/null
+++ b/_pytest/test_utf8_helpers.py
@@ -0,0 +1,72 @@
+# -*- coding: utf-8 -*-
+
+from collections import OrderedDict
+from wee_slack import decode_from_utf8, encode_to_utf8, utf8_decode
+
+
+def test_decode_preserves_string_without_utf8():
+ assert u'test' == decode_from_utf8(b'test')
+
+def test_decode_preserves_unicode_strings():
+ assert u'æøå' == decode_from_utf8(u'æøå')
+
+def test_decode_preserves_mapping_type():
+ value_dict = {'a': 'x', 'b': 'y', 'c': 'z'}
+ value_ord_dict = OrderedDict(value_dict)
+ assert type(value_dict) == type(decode_from_utf8(value_dict))
+ assert type(value_ord_dict) == type(decode_from_utf8(value_ord_dict))
+
+def test_decode_preserves_iterable_type():
+ value_set = {'a', 'b', 'c'}
+ value_tuple = ('a', 'b', 'c')
+ assert type(value_set) == type(decode_from_utf8(value_set))
+ assert type(value_tuple) == type(decode_from_utf8(value_tuple))
+
+def test_decodes_utf8_string_to_unicode():
+ assert u'æøå' == decode_from_utf8(b'æøå')
+
+def test_decodes_utf8_dict_to_unicode():
+ assert {u'æ': u'å', u'ø': u'å'} == decode_from_utf8({b'æ': b'å', b'ø': b'å'})
+
+def test_decodes_utf8_list_to_unicode():
+ assert [u'æ', u'ø', u'å'] == decode_from_utf8([b'æ', b'ø', b'å'])
+
+def test_encode_preserves_string_without_utf8():
+ assert b'test' == encode_to_utf8(u'test')
+
+def test_encode_preserves_byte_strings():
+ assert b'æøå' == encode_to_utf8(b'æøå')
+
+def test_encode_preserves_mapping_type():
+ value_dict = {'a': 'x', 'b': 'y', 'c': 'z'}
+ value_ord_dict = OrderedDict(value_dict)
+ assert type(value_dict) == type(encode_to_utf8(value_dict))
+ assert type(value_ord_dict) == type(encode_to_utf8(value_ord_dict))
+
+def test_encode_preserves_iterable_type():
+ value_set = {'a', 'b', 'c'}
+ value_tuple = ('a', 'b', 'c')
+ assert type(value_set) == type(encode_to_utf8(value_set))
+ assert type(value_tuple) == type(encode_to_utf8(value_tuple))
+
+def test_encodes_utf8_string_to_unicode():
+ assert b'æøå' == encode_to_utf8(u'æøå')
+
+def test_encodes_utf8_dict_to_unicode():
+ assert {b'æ': b'å', b'ø': b'å'} == encode_to_utf8({u'æ': u'å', u'ø': u'å'})
+
+def test_encodes_utf8_list_to_unicode():
+ assert [b'æ', b'ø', b'å'] == encode_to_utf8([u'æ', u'ø', u'å'])
+
+@utf8_decode
+def method_with_utf8_decode(*args, **kwargs):
+ return (args, kwargs)
+
+def test_utf8_decode():
+ args = (b'æ', b'ø', b'å')
+ kwargs = {b'æ': b'å', b'ø': b'å'}
+
+ result_args, result_kwargs = method_with_utf8_decode(*args, **kwargs)
+
+ assert result_args == decode_from_utf8(args)
+ assert result_kwargs == decode_from_utf8(kwargs)
diff --git a/wee_slack.py b/wee_slack.py
index 5cfdeb4..6ebba2e 100644
--- a/wee_slack.py
+++ b/wee_slack.py
@@ -1,8 +1,12 @@
-#-*- coding: utf-8 -*-
-#
+# -*- coding: utf-8 -*-
+
+from __future__ import unicode_literals
+from collections import OrderedDict
from functools import wraps
+from itertools import islice
+import textwrap
import time
import json
import pickle
@@ -12,51 +16,61 @@ import re
import urllib
import sys
import traceback
-#import collections
+import collections
import ssl
import random
import string
+try:
+ from cStringIO import StringIO
+except:
+ from StringIO import StringIO
from websocket import create_connection, WebSocketConnectionClosedException
# hack to make tests possible.. better way?
try:
- import weechat as w
+ import weechat
except:
pass
SCRIPT_NAME = "slack"
SCRIPT_AUTHOR = "Ryan Huber <rhuber@gmail.com>"
-SCRIPT_VERSION = "1.99"
+SCRIPT_VERSION = "2.0.0"
SCRIPT_LICENSE = "MIT"
SCRIPT_DESC = "Extends weechat for typing notification/search/etc on slack.com"
BACKLOG_SIZE = 200
SCROLLBACK_SIZE = 500
-CACHE_VERSION = "4"
-
RECORD_DIR = "/tmp/weeslack-debug"
SLACK_API_TRANSLATOR = {
"channel": {
"history": "channels.history",
- "join": "channels.join",
- "leave": "channels.leave",
+ "join": "conversations.join",
+ "leave": "conversations.leave",
"mark": "channels.mark",
"info": "channels.info",
},
"im": {
"history": "im.history",
- "join": "im.open",
- "leave": "im.close",
+ "join": "conversations.open",
+ "leave": "conversations.close",
"mark": "im.mark",
},
+ "mpim": {
+ "history": "mpim.history",
+ "join": "mpim.open", # conversations.open lacks unread_count_display
+ "leave": "conversations.close",
+ "mark": "mpim.mark",
+ "info": "groups.info",
+ },
"group": {
"history": "groups.history",
- "join": "channels.join",
- "leave": "groups.leave",
+ "join": "conversations.join",
+ "leave": "conversations.leave",
"mark": "groups.mark",
+ "info": "groups.info"
},
"thread": {
"history": None,
@@ -70,6 +84,7 @@ SLACK_API_TRANSLATOR = {
###### Decorators have to be up here
+
def slack_buffer_or_ignore(f):
"""
Only run this function if we're in a slack buffer, else ignore
@@ -81,6 +96,7 @@ def slack_buffer_or_ignore(f):
return f(data, current_buffer, *args, **kwargs)
return wrapper
+
def slack_buffer_required(f):
"""
Only run this function if we're in a slack buffer, else print error
@@ -93,6 +109,17 @@ def slack_buffer_required(f):
return wrapper
+def utf8_decode(f):
+ """
+ Decode all arguments from byte strings to unicode strings. Use this for
+ functions called from outside of this script, e.g. callbacks from weechat.
+ """
+ @wraps(f)
+ def wrapper(*args, **kwargs):
+ return f(*decode_from_utf8(args), **decode_from_utf8(kwargs))
+ return wrapper
+
+
NICK_GROUP_HERE = "0|Here"
NICK_GROUP_AWAY = "1|Away"
@@ -102,16 +129,84 @@ if hasattr(ssl, "get_default_verify_paths") and callable(ssl.get_default_verify_
if ssl_defaults.cafile is not None:
sslopt_ca_certs = {'ca_certs': ssl_defaults.cafile}
+EMOJI = []
+
+###### Unicode handling
+
+
+def encode_to_utf8(data):
+ if isinstance(data, unicode):
+ return data.encode('utf-8')
+ if isinstance(data, bytes):
+ return data
+ elif isinstance(data, collections.Mapping):
+ return type(data)(map(encode_to_utf8, data.iteritems()))
+ elif isinstance(data, collections.Iterable):
+ return type(data)(map(encode_to_utf8, data))
+ else:
+ return data
+
+
+def decode_from_utf8(data):
+ if isinstance(data, bytes):
+ return data.decode('utf-8')
+ if isinstance(data, unicode):
+ return data
+ elif isinstance(data, collections.Mapping):
+ return type(data)(map(decode_from_utf8, data.iteritems()))
+ elif isinstance(data, collections.Iterable):
+ return type(data)(map(decode_from_utf8, data))
+ else:
+ return data
+
+
+class WeechatWrapper(object):
+ def __init__(self, wrapped_class):
+ self.wrapped_class = wrapped_class
+
+ # Helper method used to encode/decode method calls.
+ def wrap_for_utf8(self, method):
+ def hooked(*args, **kwargs):
+ result = method(*encode_to_utf8(args), **encode_to_utf8(kwargs))
+ # Prevent wrapped_class from becoming unwrapped
+ if result == self.wrapped_class:
+ return self
+ return decode_from_utf8(result)
+ return hooked
+
+ # Encode and decode everything sent to/received from weechat. We use the
+ # unicode type internally in wee-slack, but has to send utf8 to weechat.
+ def __getattr__(self, attr):
+ orig_attr = self.wrapped_class.__getattribute__(attr)
+ if callable(orig_attr):
+ return self.wrap_for_utf8(orig_attr)
+ else:
+ return decode_from_utf8(orig_attr)
+
+ # Ensure all lines sent to weechat specifies a prefix. For lines after the
+ # first, we want to disable the prefix, which is done by specifying a space.
+ def prnt_date_tags(self, buffer, date, tags, message):
+ message = message.replace("\n", "\n \t")
+ return self.wrap_for_utf8(self.wrapped_class.prnt_date_tags)(buffer, date, tags, message)
+
+
+##### Helpers
+
+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 = [
- "hello",
- #"pref_change",
- #"reconnect_url",
+ # "pref_change",
+ # "reconnect_url",
]
###### New central Event router
+
class EventRouter(object):
def __init__(self):
@@ -188,7 +283,7 @@ class EventRouter(object):
"""
data = self.context.get(identifier, None)
if data:
- #dbg("retrieved eontext {} ".format(identifier))
+ # dbg("retrieved context {} ".format(identifier))
return data
def delete_context(self, identifier):
@@ -196,7 +291,7 @@ class EventRouter(object):
Requests can span multiple requests, so we may need to delete this as a last step
"""
if identifier in self.context:
- #dbg("deleted eontext {} ".format(identifier))
+ # dbg("deleted eontext {} ".format(identifier))
del self.context[identifier]
def shutdown(self):
@@ -236,7 +331,7 @@ class EventRouter(object):
"""
try:
# Read the data from the websocket associated with this team.
- data = self.teams[team_hash].ws.recv()
+ data = decode_from_utf8(self.teams[team_hash].ws.recv())
message_json = json.loads(data)
metadata = WeeSlackMetadata({
"team": team_hash,
@@ -246,9 +341,12 @@ class EventRouter(object):
self.record_event(message_json, 'type', 'websocket')
self.receive_json(json.dumps(message_json))
except WebSocketConnectionClosedException:
- #TODO: handle reconnect here
+ # TODO: handle reconnect here
self.teams[team_hash].set_disconnected()
return w.WEECHAT_RC_OK
+ except ssl.SSLWantReadError:
+ # Expected to happen occasionally on SSL websockets.
+ return w.WEECHAT_RC_OK
except Exception:
dbg("socket issue: {}\n".format(traceback.format_exc()))
return w.WEECHAT_RC_OK
@@ -270,18 +368,14 @@ class EventRouter(object):
return
if return_code == 0:
if len(out) > 0:
- if request_metadata.response_id in self.reply_buffer:
- #dbg("found response id in reply_buffer", True)
- self.reply_buffer[request_metadata.response_id] += out
- else:
- #dbg("didn't find response id in reply_buffer", True)
- self.reply_buffer[request_metadata.response_id] = ""
- self.reply_buffer[request_metadata.response_id] += out
+ if request_metadata.response_id not in self.reply_buffer:
+ self.reply_buffer[request_metadata.response_id] = StringIO()
+ self.reply_buffer[request_metadata.response_id].write(out)
try:
- j = json.loads(self.reply_buffer[request_metadata.response_id])
+ j = json.loads(self.reply_buffer[request_metadata.response_id].getvalue())
except:
pass
- #dbg("Incomplete json, awaiting more", True)
+ # dbg("Incomplete json, awaiting more", True)
try:
j["wee_slack_process_method"] = request_metadata.request_normalized
j["wee_slack_request_metadata"] = pickle.dumps(request_metadata)
@@ -303,8 +397,8 @@ class EventRouter(object):
self.delete_context(data)
else:
if request_metadata.response_id not in self.reply_buffer:
- self.reply_buffer[request_metadata.response_id] = ""
- self.reply_buffer[request_metadata.response_id] += out
+ self.reply_buffer[request_metadata.response_id] = StringIO()
+ self.reply_buffer[request_metadata.response_id].write(out)
def receive_json(self, data):
"""
@@ -315,6 +409,7 @@ class EventRouter(object):
dbg("RECEIVED JSON of len {}".format(len(data)))
message_json = json.loads(data)
self.queue.append(message_json)
+
def receive(self, dataobj):
"""
complete
@@ -324,6 +419,7 @@ class EventRouter(object):
"""
dbg("RECEIVED FROM QUEUE")
self.queue.append(dataobj)
+
def receive_slow(self, dataobj):
"""
complete
@@ -333,6 +429,7 @@ class EventRouter(object):
"""
dbg("RECEIVED FROM QUEUE")
self.slow_queue.append(dataobj)
+
def handle_next(self):
"""
complete
@@ -341,10 +438,10 @@ class EventRouter(object):
useful metadata and context to events as they are processed.
"""
if len(self.slow_queue) > 0 and ((self.slow_queue_timer + 1) < time.time()):
- #for q in self.slow_queue[0]:
+ # for q in self.slow_queue[0]:
dbg("from slow queue", 0)
self.queue.append(self.slow_queue.pop())
- #self.slow_queue = []
+ # self.slow_queue = []
self.slow_queue_timer = time.time()
if len(self.queue) > 0:
j = self.queue.pop(0)
@@ -378,7 +475,7 @@ class EventRouter(object):
meta = j.get("wee_slack_metadata", None)
if meta:
try:
- if isinstance(meta, str):
+ if isinstance(meta, basestring):
dbg("string of metadata")
team = meta.get("team", None)
if team:
@@ -401,6 +498,7 @@ class EventRouter(object):
else:
raise ProcessNotImplemented(function_name)
+
def handle_next(*args):
"""
complete
@@ -416,33 +514,38 @@ def handle_next(*args):
pass
return w.WEECHAT_RC_OK
+
class WeechatController(object):
"""
Encapsulates our interaction with weechat
"""
+
def __init__(self, eventrouter):
self.eventrouter = eventrouter
self.buffers = {}
self.previous_buffer = None
self.buffer_list_stale = False
+
def iter_buffers(self):
for b in self.buffers:
yield (b, self.buffers[b])
+
def register_buffer(self, buffer_ptr, channel):
"""
complete
Adds a weechat buffer to the list of handled buffers for this EventRouter
"""
- if isinstance(buffer_ptr, str):
+ if isinstance(buffer_ptr, basestring):
self.buffers[buffer_ptr] = channel
else:
raise InvalidType(type(buffer_ptr))
+
def unregister_buffer(self, buffer_ptr, update_remote=False, close_buffer=False):
"""
complete
Adds a weechat buffer to the list of handled buffers for this EventRouter
"""
- if isinstance(buffer_ptr, str):
+ if isinstance(buffer_ptr, basestring):
try:
self.buffers[buffer_ptr].destroy_buffer(update_remote)
if close_buffer:
@@ -452,22 +555,28 @@ class WeechatController(object):
dbg("Tried to close unknown buffer")
else:
raise InvalidType(type(buffer_ptr))
+
def get_channel_from_buffer_ptr(self, buffer_ptr):
return self.buffers.get(buffer_ptr, None)
+
def get_all(self, buffer_ptr):
return self.buffers
+
def get_previous_buffer_ptr(self):
return self.previous_buffer
+
def set_previous_buffer(self, data):
self.previous_buffer = data
+
def check_refresh_buffer_list(self):
return self.buffer_list_stale and self.last_buffer_list_update + 1 < time.time()
+
def set_refresh_buffer_list(self, setting):
self.buffer_list_stale = setting
-
###### New Local Processors
+
def local_process_async_slack_api_request(request, event_router):
"""
complete
@@ -480,21 +589,25 @@ def local_process_async_slack_api_request(request, event_router):
params = {'useragent': 'wee_slack {}'.format(SCRIPT_VERSION)}
request.tried()
context = event_router.store_context(request)
- #TODO: let flashcode know about this bug - i have to 'clear' the hashtable or retry requests fail
+ # TODO: let flashcode know about this bug - i have to 'clear' the hashtable or retry requests fail
w.hook_process_hashtable('url:', params, config.slack_timeout, "", context)
w.hook_process_hashtable(weechat_request, params, config.slack_timeout, "receive_httprequest_callback", context)
###### New Callbacks
+
+@utf8_decode
def receive_httprequest_callback(data, command, return_code, out, err):
"""
complete
This is a dirty hack. There must be a better way.
"""
- #def url_processor_cb(data, command, return_code, out, err):
+ # def url_processor_cb(data, command, return_code, out, err):
EVENTROUTER.receive_httprequest_callback(data, command, return_code, out, err)
return w.WEECHAT_RC_OK
+
+@utf8_decode
def receive_ws_callback(*args):
"""
complete
@@ -505,10 +618,14 @@ def receive_ws_callback(*args):
EVENTROUTER.receive_ws_callback(args[0])
return w.WEECHAT_RC_OK
+
+@utf8_decode
def reconnect_callback(*args):
EVENTROUTER.reconnect_if_disconnected()
return w.WEECHAT_RC_OK
+
+@utf8_decode
def buffer_closing_callback(signal, sig_type, data):
"""
complete
@@ -520,6 +637,8 @@ def buffer_closing_callback(signal, sig_type, data):
eval(signal).weechat_controller.unregister_buffer(data, True, False)
return w.WEECHAT_RC_OK
+
+@utf8_decode
def buffer_input_callback(signal, buffer_ptr, data):
"""
incomplete
@@ -530,15 +649,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)
+ reaction = re.match("^(\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:
@@ -548,12 +669,29 @@ def buffer_input_callback(signal, buffer_ptr, data):
# rid of escapes.
new = new.replace(r'\/', '/')
old = old.replace(r'\/', '/')
- channel.edit_previous_message(old.decode("utf-8"), new.decode("utf-8"), flags)
+ channel.edit_nth_previous_message(msgno, old, new, flags)
else:
+ if data.startswith(('//', ' ')):
+ data = data[1:]
channel.send_message(data)
- #this is probably wrong channel.mark_read(update_remote=True, force=True)
- return w.WEECHAT_RC_ERROR
+ # this is probably wrong channel.mark_read(update_remote=True, force=True)
+ return w.WEECHAT_RC_OK
+
+
+# Workaround for supporting multiline messages. It intercepts before the input
+# callback is called, as this is called with the whole message, while it is
+# normally split on newline before being sent to buffer_input_callback
+def input_text_for_buffer_cb(data, modifier, current_buffer, string):
+ if current_buffer not in EVENTROUTER.weechat_controller.buffers:
+ return string
+ message = decode_from_utf8(string)
+ if not message.startswith("/") and "\n" in message:
+ buffer_input_callback("EVENTROUTER", current_buffer, message)
+ return ""
+ return string
+
+@utf8_decode
def buffer_switch_callback(signal, sig_type, data):
"""
incomplete
@@ -577,6 +715,8 @@ def buffer_switch_callback(signal, sig_type, data):
eventrouter.weechat_controller.set_previous_buffer(data)
return w.WEECHAT_RC_OK
+
+@utf8_decode
def buffer_list_update_callback(data, somecount):
"""
incomplete
@@ -587,7 +727,7 @@ def buffer_list_update_callback(data, somecount):
user presence via " name" <-> "+name".
"""
eventrouter = eval(data)
- #global buffer_list_update
+ # global buffer_list_update
for b in eventrouter.weechat_controller.iter_buffers():
b[1].refresh()
@@ -599,9 +739,13 @@ def buffer_list_update_callback(data, somecount):
# eventrouter.weechat_controller.set_refresh_buffer_list(False)
return w.WEECHAT_RC_OK
+
def quit_notification_callback(signal, sig_type, data):
stop_talking_to_slack()
+ return w.WEECHAT_RC_OK
+
+@utf8_decode
def typing_notification_cb(signal, sig_type, data):
msg = w.buffer_get_string(data, "input")
if len(msg) > 8 and msg[:1] != "/":
@@ -610,17 +754,21 @@ 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)
typing_timer = now
return w.WEECHAT_RC_OK
+
+@utf8_decode
def typing_update_cb(data, remaining_calls):
w.bar_item_update("slack_typing_notice")
return w.WEECHAT_RC_OK
+
+@utf8_decode
def slack_never_away_cb(data, remaining_calls):
if config.never_away:
for t in EVENTROUTER.teams.values():
@@ -630,6 +778,8 @@ def slack_never_away_cb(data, remaining_calls):
channel.team.send_to_websocket(request, expect_reply=False)
return w.WEECHAT_RC_OK
+
+@utf8_decode
def typing_bar_item_cb(data, current_buffer, args):
"""
Privides a bar item indicating who is typing in the current channel AND
@@ -663,6 +813,8 @@ def typing_bar_item_cb(data, current_buffer, args):
return typing
+
+@utf8_decode
def nick_completion_cb(data, completion_item, current_buffer, completion):
"""
Adds all @-prefixed nicks to completion list
@@ -676,9 +828,11 @@ def nick_completion_cb(data, completion_item, current_buffer, completion):
for m in current_channel.members:
u = current_channel.team.users.get(m, None)
if u:
- w.hook_completion_list_add(completion, "@" + u.slack_name, 1, w.WEECHAT_LIST_POS_SORT)
+ w.hook_completion_list_add(completion, "@" + u.name, 1, w.WEECHAT_LIST_POS_SORT)
return w.WEECHAT_RC_OK
+
+@utf8_decode
def emoji_completion_cb(data, completion_item, current_buffer, completion):
"""
Adds all :-prefixed emoji to completion list
@@ -689,10 +843,12 @@ def emoji_completion_cb(data, completion_item, current_buffer, completion):
if current_channel is None:
return w.WEECHAT_RC_OK
- for e in EMOJI['emoji']:
+ for e in current_channel.team.emoji_completions:
w.hook_completion_list_add(completion, ":" + e + ":", 0, w.WEECHAT_LIST_POS_SORT)
return w.WEECHAT_RC_OK
+
+@utf8_decode
def complete_next_cb(data, current_buffer, command):
"""Extract current word, if it is equal to a nick, prefix it with @ and
rely on nick_completion_cb adding the @-prefixed versions to the
@@ -704,7 +860,7 @@ def complete_next_cb(data, current_buffer, command):
current_buffer = w.current_buffer()
current_channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer, None)
- #channel = channels.find(current_buffer)
+ # channel = channels.find(current_buffer)
if not hasattr(current_channel, 'members') or current_channel is None or current_channel.members is None:
return w.WEECHAT_RC_OK
@@ -731,7 +887,7 @@ def complete_next_cb(data, current_buffer, command):
for m in current_channel.members:
u = current_channel.team.users.get(m, None)
- if u and u.slack_name == word:
+ if u and u.name == word:
# Here, we cheat. Insert a @ in front and rely in the @
# nicks being in the completion list
w.buffer_set(current_buffer, "input", line_input[:word_start] + "@" + line_input[word_start:])
@@ -739,10 +895,12 @@ def complete_next_cb(data, current_buffer, command):
return w.WEECHAT_RC_OK_EAT
return w.WEECHAT_RC_OK
+
def script_unloaded():
stop_talking_to_slack()
return w.WEECHAT_RC_OK
+
def stop_talking_to_slack():
"""
complete
@@ -753,15 +911,16 @@ def stop_talking_to_slack():
EVENTROUTER.shutdown()
return w.WEECHAT_RC_OK
-
##### New Classes
+
class SlackRequest(object):
"""
complete
Encapsulates a Slack api request. Valuable as an object that we can add to the queue and/or retry.
makes a SHA of the requst url and current time so we can re-tag this on the way back through.
"""
+
def __init__(self, token, request, post_data={}, **kwargs):
for key, value in kwargs.items():
setattr(self, key, value)
@@ -774,31 +933,37 @@ class SlackRequest(object):
post_data["token"] = token
self.post_data = post_data
self.params = {'useragent': 'wee_slack {}'.format(SCRIPT_VERSION)}
- self.url = 'https://{}/api/{}?{}'.format(self.domain, request, urllib.urlencode(post_data))
+ self.url = 'https://{}/api/{}?{}'.format(self.domain, request, urllib.urlencode(encode_to_utf8(post_data)))
self.response_id = sha.sha("{}{}".format(self.url, self.start_time)).hexdigest()
self.retries = kwargs.get('retries', 3)
# def __repr__(self):
# return "URL: {} Tries: {} ID: {}".format(self.url, self.tries, self.response_id)
+
def request_string(self):
return "{}".format(self.url)
+
def tried(self):
self.tries += 1
self.response_id = sha.sha("{}{}".format(self.url, time.time())).hexdigest()
+
def should_try(self):
return self.tries < self.retries
+
def retry_ready(self):
return (self.start_time + (self.tries**2)) < time.time()
+
class SlackTeam(object):
"""
incomplete
Team object under which users and channels live.. Does lots.
"""
+
def __init__(self, eventrouter, token, websocket_url, subdomain, nick, myidentifier, users, bots, channels, **kwargs):
self.ws_url = websocket_url
self.connected = False
self.connecting = False
- #self.ws = None
+ # self.ws = None
self.ws_counter = 0
self.ws_replies = {}
self.eventrouter = eventrouter
@@ -819,7 +984,6 @@ class SlackTeam(object):
self.users = users
self.bots = bots
self.team_hash = SlackTeam.generate_team_hash(self.nick, self.subdomain)
- #self.team_hash = str(sha.sha("{}{}".format(self.nick, self.subdomain)).hexdigest())
self.name = self.domain
self.channel_buffer = None
self.got_history = True
@@ -833,21 +997,36 @@ class SlackTeam(object):
self.users[self.myidentifier].force_color(w.config_string(w.config_get('weechat.color.chat_nick_self')))
# This highlight step must happen after we have set related server
self.set_highlight_words(kwargs.get('highlight_words', ""))
+ self.load_emoji_completions()
+
+ def __repr__(self):
+ return "domain={} nick={}".format(self.subdomain, self.nick)
+
def __eq__(self, compare_str):
if compare_str == self.token or compare_str == self.domain or compare_str == self.subdomain:
return True
else:
return False
+
+ def load_emoji_completions(self):
+ self.emoji_completions = list(EMOJI)
+ if self.emoji_completions:
+ s = SlackRequest(self.token, "emoji.list", {}, team_hash=self.team_hash)
+ self.eventrouter.receive(s)
+
def add_channel(self, channel):
self.channels[channel["id"]] = channel
channel.set_related_server(self)
-# def connect_request_generate(self):
-# return SlackRequest(self.token, 'rtm.start', {})
- #def close_all_buffers(self):
+
+ # def connect_request_generate(self):
+ # return SlackRequest(self.token, 'rtm.start', {})
+
+ # def close_all_buffers(self):
# for channel in self.channels:
# self.eventrouter.weechat_controller.unregister_buffer(channel.channel_buffer, update_remote=False, close_buffer=True)
# #also close this server buffer
# self.eventrouter.weechat_controller.unregister_buffer(self.channel_buffer, update_remote=False, close_buffer=True)
+
def create_buffer(self):
if not self.channel_buffer:
if config.short_buffer_names:
@@ -861,133 +1040,178 @@ 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)
+ w.buffer_set(self.channel_buffer, "localvar_set_server", self.preferred_name)
if w.config_string(w.config_get('irc.look.server_buffer')) == 'merge_with_core':
w.buffer_merge(self.channel_buffer, w.buffer_search_main())
- w.buffer_set(self.channel_buffer, "nicklist", "1")
+
def set_muted_channels(self, muted_str):
self.muted_channels = {x for x in muted_str.split(',')}
+
def set_highlight_words(self, highlight_str):
self.highlight_words = {x for x in highlight_str.split(',')}
if len(self.highlight_words) > 0:
for v in self.channels.itervalues():
v.set_highlights()
+
def formatted_name(self, **kwargs):
return self.domain
+
def buffer_prnt(self, data):
- w.prnt_date_tags(self.channel_buffer, SlackTS().major, tag("backlog"), data)
+ w.prnt_date_tags(self.channel_buffer, SlackTS().major, tag("team"), data)
+
def get_channel_map(self):
return {v.slack_name: k for k, v in self.channels.iteritems()}
+
def get_username_map(self):
- return {v.slack_name: k for k, v in self.users.iteritems()}
+ return {v.name: k for k, v in self.users.iteritems()}
+
def get_team_hash(self):
return self.team_hash
+
@staticmethod
def generate_team_hash(nick, subdomain):
return str(sha.sha("{}{}".format(nick, subdomain)).hexdigest())
+
def refresh(self):
self.rename()
+
def rename(self):
pass
- #def attach_websocket(self, ws):
+
+ # def attach_websocket(self, ws):
# self.ws = ws
+
def is_user_present(self, user_id):
user = self.users.get(user_id)
if user.presence == 'active':
return True
else:
return False
- def mark_read(self):
+
+ def mark_read(self, ts=None, update_remote=True, force=False):
pass
+
def connect(self):
if not self.connected and not self.connecting:
self.connecting = True
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)
+ # self.attach_websocket(ws)
self.set_connected()
self.connecting = False
except Exception as e:
- dbg("websocket connection error: {}".format(e))
+ dbg("websocket connection error: {}".format(decode_from_utf8(e)))
self.connecting = False
return False
else:
- #The fast reconnect failed, so start over-ish
+ # The fast reconnect failed, so start over-ish
for chan in self.channels:
self.channels[chan].got_history = False
- s = SlackRequest(self.token, 'rtm.start', {}, retries=999)
+ s = initiate_connection(self.token, retries=999)
self.eventrouter.receive(s)
self.connecting = False
- #del self.eventrouter.teams[self.get_team_hash()]
+ # del self.eventrouter.teams[self.get_team_hash()]
self.set_reconnect_url(None)
+
def set_connected(self):
self.connected = True
+
def set_disconnected(self):
+ w.unhook(self.hook)
self.connected = False
+
def set_reconnect_url(self, url):
self.ws_url = url
+
def next_ws_transaction_id(self):
if self.ws_counter > 999:
self.ws_counter = 0
self.ws_counter += 1
return self.ws_counter
+
def send_to_websocket(self, data, expect_reply=True):
data["id"] = self.next_ws_transaction_id()
message = json.dumps(data)
try:
if expect_reply:
self.ws_replies[data["id"]] = data
- self.ws.send(message)
+ self.ws.send(encode_to_utf8(message))
dbg("Sent {}...".format(message[:100]))
except:
print "WS ERROR"
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)
+
+ def subscribe_users_presence(self):
+ # FIXME: There is a limitation in the API to the size of the
+ # json we can send.
+ # We should try to be smarter to fetch the users whom we want to
+ # subscribe to.
+ users = self.users.keys()[0:750]
+ self.send_to_websocket({
+ "type": "presence_sub",
+ "ids": users,
+ }, expect_reply=False)
+
class SlackChannel(object):
"""
Represents an individual slack channel.
"""
+
def __init__(self, eventrouter, **kwargs):
- # We require these two things for a vaid object,
+ # We require these two things for a valid object,
# the rest we can just learn from slack
self.active = False
for key, value in kwargs.items():
setattr(self, key, value)
- self.members = set(kwargs.get('members', set()))
self.eventrouter = eventrouter
self.slack_name = kwargs["name"]
- self.slack_topic = kwargs.get("topic", {"value": ""})
self.slack_purpose = kwargs.get("purpose", {"value": ""})
+ self.topic = kwargs.get("topic", {}).get("value", "")
self.identifier = kwargs["id"]
self.last_read = SlackTS(kwargs.get("last_read", SlackTS()))
- #print self.last_read
self.channel_buffer = None
self.team = kwargs.get('team', None)
self.got_history = False
- self.messages = {}
+ self.messages = OrderedDict()
self.hashed_messages = {}
self.new_messages = False
self.typing = {}
self.type = 'channel'
self.set_name(self.slack_name)
- #short name relates to the localvar we change for typing indication
+ # short name relates to the localvar we change for typing indication
self.current_short_name = self.name
- self.update_nicklist()
+ self.set_members(kwargs.get('members', []))
+ 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"):
return True
else:
return False
+
def __repr__(self):
return "Name:{} Identifier:{}".format(self.name, self.identifier)
+
def set_name(self, slack_name):
self.name = "#" + slack_name
+
def refresh(self):
return self.rename()
+
def rename(self):
if self.channel_buffer:
new_name = self.formatted_name(typing=self.is_someone_typing(), style="sidebar")
@@ -996,12 +1220,28 @@ class SlackChannel(object):
w.buffer_set(self.channel_buffer, "short_name", new_name)
return True
return False
- def formatted_name(self, style="default", typing=False, **kwargs):
- if config.channel_name_typing_indicator:
- if not typing:
- prepend = "#"
+
+ def set_members(self, members):
+ self.members = set(members)
+ self.update_nicklist()
+
+ def get_members(self):
+ return self.members
+
+ def set_unread_count_display(self, count):
+ self.unread_count_display = count
+ self.new_messages = bool(self.unread_count_display)
+ for c in range(self.unread_count_display):
+ if self.type == "im":
+ w.buffer_set(self.channel_buffer, "hotlist", "2")
else:
- prepend = ">"
+ w.buffer_set(self.channel_buffer, "hotlist", "1")
+
+ def formatted_name(self, style="default", typing=False, **kwargs):
+ if typing and config.channel_name_typing_indicator:
+ prepend = ">"
+ elif self.type == "group":
+ prepend = config.group_name_prefix
else:
prepend = "#"
select = {
@@ -1012,57 +1252,59 @@ class SlackChannel(object):
"long_base": "{}.{}".format(self.team.preferred_name, self.slack_name),
}
return select[style]
- def render_topic(self, topic=None):
+
+ def render_topic(self):
if self.channel_buffer:
- if not topic:
- if self.slack_topic['value'] != "":
- encoded_topic = self.slack_topic['value'].encode('utf-8')
- else:
- encoded_topic = self.slack_purpose['value'].encode('utf-8')
+ if self.topic != "":
+ topic = self.topic
else:
- encoded_topic = topic.encode('utf-8')
- self.encoded_topic = topic.encode('utf-8')
- w.buffer_set(self.channel_buffer, "title", encoded_topic)
+ topic = self.slack_purpose['value']
+ w.buffer_set(self.channel_buffer, "title", topic)
+
+ def set_topic(self, value):
+ self.topic = value
+ self.render_topic()
+
def update_from_message_json(self, message_json):
for key, value in message_json.items():
setattr(self, key, value)
+
def open(self, update_remote=True):
if update_remote:
if "join" in SLACK_API_TRANSLATOR[self.type]:
- s = SlackRequest(self.team.token, SLACK_API_TRANSLATOR[self.type]["join"], {"name": self.name}, team_hash=self.team.team_hash, channel_identifier=self.identifier)
+ s = SlackRequest(self.team.token, SLACK_API_TRANSLATOR[self.type]["join"], {"channel": self.identifier}, team_hash=self.team.team_hash, channel_identifier=self.identifier)
self.eventrouter.receive(s)
self.create_buffer()
self.active = True
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()
+ # self.create_buffer()
+
def check_should_open(self, force=False):
- try:
- if self.is_archived:
- return
- except:
- pass
+ if hasattr(self, "is_archived") and self.is_archived:
+ return
+
if force:
self.create_buffer()
- else:
- for reason in ["is_member", "is_open", "unread_count_display"]:
- try:
- if eval("self." + reason):
- self.create_buffer()
- if config.background_load_all_history:
- self.get_history(slow_queue=True)
- except:
- pass
+ return
+
+ # Only check is_member if is_open is not set, because in some cases
+ # (e.g. group DMs), is_member should be ignored in favor of is_open.
+ is_open = self.is_open if hasattr(self, "is_open") else self.is_member
+ if is_open or self.unread_count_display:
+ self.create_buffer()
+ if config.background_load_all_history:
+ self.get_history(slow_queue=True)
+
def set_related_server(self, team):
self.team = team
+
def set_highlights(self):
- #highlight my own name and any set highlights
+ # highlight my own name and any set highlights
if self.channel_buffer:
- highlights = self.team.highlight_words.union({'@' + self.team.nick, "!here", "!channel", "!everyone"})
+ highlights = self.team.highlight_words.union({'@' + self.team.nick, self.team.myidentifier, "!here", "!channel", "!everyone"})
h_str = ",".join(highlights)
w.buffer_set(self.channel_buffer, "highlight_words", h_str)
+
def create_buffer(self):
"""
incomplete (muted doesn't work)
@@ -1077,58 +1319,56 @@ 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)
if self.channel_buffer:
- #if self.team.server_alias:
- #w.buffer_set(self.channel_buffer, "localvar_set_server", self.team.server_alias)
- #else:
+ # if self.team.server_alias:
+ # w.buffer_set(self.channel_buffer, "localvar_set_server", self.team.server_alias)
+ # else:
w.buffer_set(self.channel_buffer, "localvar_set_server", self.team.preferred_name)
- #else:
+ # 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"], {"users": self.user, "return_im": True}, team_hash=self.team.team_hash, channel_identifier=self.identifier)
+ self.eventrouter.receive(s)
+
def destroy_buffer(self, update_remote):
if self.channel_buffer is not None:
self.channel_buffer = None
- self.messages = {}
+ self.messages = OrderedDict()
self.hashed_messages = {}
self.got_history = False
- #if update_remote and not eventrouter.shutting_down:
+ # if update_remote and not eventrouter.shutting_down:
self.active = False
if update_remote and not self.eventrouter.shutting_down:
s = SlackRequest(self.team.token, SLACK_API_TRANSLATOR[self.type]["leave"], {"channel": self.identifier}, team_hash=self.team.team_hash, channel_identifier=self.identifier)
self.eventrouter.receive(s)
+
+
def buffer_prnt(self, nick, text, timestamp=str(time.time()), tagset=None, tag_nick=None, **kwargs):
- data = "{}\t{}".format(nick, text)
+ data = "{}\t{}".format(format_nick(nick), text)
ts = SlackTS(timestamp)
last_read = SlackTS(self.last_read)
- #without this, DMs won't open automatically
+ # without this, DMs won't open automatically
if not self.channel_buffer and ts > last_read:
self.open(update_remote=False)
if self.channel_buffer:
- #backlog messages - we will update the read marker as we print these
+ # backlog messages - we will update the read marker as we print these
backlog = True if ts <= last_read else False
if tagset:
tags = tag(tagset, user=tag_nick)
self.new_messages = True
- #we have to infer the tagset because we weren't told
+ # we have to infer the tagset because we weren't told
elif ts <= last_read:
tags = tag("backlog", user=tag_nick)
elif self.type in ["im", "mpdm"]:
@@ -1151,28 +1391,31 @@ class SlackChannel(object):
self.mark_read(ts, update_remote=False, force=True)
except:
dbg("Problem processing buffer_prnt")
+
def send_message(self, message, request_dict_ext={}):
- #team = self.eventrouter.teams[self.team]
+ # team = self.eventrouter.teams[self.team]
message = linkify_text(message, self.team, self)
dbg(message)
request = {"type": "message", "channel": self.identifier, "text": message, "_team": self.team.team_hash, "user": self.team.myidentifier}
request.update(request_dict_ext)
self.team.send_to_websocket(request)
self.mark_read(update_remote=False, force=True)
+
def store_message(self, message, team, from_me=False):
if not self.active:
return
if from_me:
message.message_json["user"] = team.myidentifier
self.messages[SlackTS(message.ts)] = message
- if len(self.messages.keys()) > SCROLLBACK_SIZE:
- mk = self.messages.keys()
- mk.sort()
- for k in mk[:SCROLLBACK_SIZE]:
- msg_to_delete = self.messages[k]
- if msg_to_delete.hash:
- del self.hashed_messages[msg_to_delete.hash]
- del self.messages[k]
+
+ sorted_messages = sorted(self.messages.items())
+ messages_to_delete = sorted_messages[:-SCROLLBACK_SIZE]
+ messages_to_keep = sorted_messages[-SCROLLBACK_SIZE:]
+ for message_hash in [m[1].hash for m in messages_to_delete]:
+ if message_hash in self.hashed_messages:
+ del self.hashed_messages[message_hash]
+ self.messages = OrderedDict(messages_to_keep)
+
def change_message(self, ts, text=None, suffix=None):
ts = SlackTS(ts)
if ts in self.messages:
@@ -1184,8 +1427,9 @@ class SlackChannel(object):
text = m.render(force=True)
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)
@@ -1195,18 +1439,23 @@ class SlackChannel(object):
num_replace = 0
new_message = re.sub(old, new, message["text"], num_replace)
if new_message != message["text"]:
- s = SlackRequest(self.team.token, "chat.update", {"channel": self.identifier, "ts": message['ts'], "text": new_message.encode("utf-8")}, team_hash=self.team.team_hash, channel_identifier=self.identifier)
+ 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):
- for message in reversed(self.sorted_message_keys()):
- m = self.messages[message]
+
+ def my_last_message(self, msgno):
+ for key in self.main_message_keys_reversed():
+ m = self.messages[key]
if "user" in m.message_json and "text" in m.message_json and m.message_json["user"] == self.team.myidentifier:
- 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
+
def get_history(self, slow_queue=False):
if not self.got_history:
- #we have probably reconnected. flush the buffer
+ # we have probably reconnected. flush the buffer
if self.team.connected:
w.buffer_clear(self.channel_buffer)
self.buffer_prnt('', 'getting channel history...', tagset='backlog')
@@ -1216,32 +1465,37 @@ class SlackChannel(object):
else:
self.eventrouter.receive_slow(s)
self.got_history = True
+
def send_add_reaction(self, msg_number, reaction):
self.send_change_reaction("reactions.add", msg_number, reaction)
+
def send_remove_reaction(self, msg_number, reaction):
self.send_change_reaction("reactions.remove", msg_number, reaction)
+
def send_change_reaction(self, method, msg_number, reaction):
if 0 < msg_number < len(self.messages):
- timestamp = self.sorted_message_keys()[-msg_number]
+ keys = self.main_message_keys_reversed()
+ timestamp = next(islice(keys, msg_number - 1, None))
data = {"channel": self.identifier, "timestamp": timestamp, "name": reaction}
s = SlackRequest(self.team.token, method, data)
self.eventrouter.receive(s)
- def sorted_message_keys(self):
- keys = []
- for k in self.messages:
- if type(self.messages[k]) == SlackMessage:
- keys.append(k)
- return sorted(keys)
+
+ def main_message_keys_reversed(self):
+ return (key for key in reversed(self.messages)
+ if type(self.messages[key]) == SlackMessage)
+
# Typing related
def set_typing(self, user):
if self.channel_buffer and self.is_visible():
self.typing[user] = time.time()
self.eventrouter.weechat_controller.set_refresh_buffer_list(True)
+
def unset_typing(self, user):
if self.channel_buffer and self.is_visible():
u = self.typing.get(user, None)
if u:
self.eventrouter.weechat_controller.set_refresh_buffer_list(True)
+
def is_someone_typing(self):
"""
Walks through dict of typing folks in a channel and fast
@@ -1255,6 +1509,7 @@ class SlackChannel(object):
self.typing = {}
self.eventrouter.weechat_controller.set_refresh_buffer_list(True)
return False
+
def get_typing_list(self):
"""
Returns the names of everyone in the channel who is currently typing.
@@ -1266,9 +1521,10 @@ class SlackChannel(object):
else:
del self.typing[user]
return typing
+
def mark_read(self, ts=None, update_remote=True, force=False):
if not ts:
- ts = SlackTS()
+ ts = next(self.main_message_keys_reversed(), SlackTS())
if self.new_messages or force:
if self.channel_buffer:
w.buffer_set(self.channel_buffer, "unread", "")
@@ -1277,41 +1533,45 @@ class SlackChannel(object):
s = SlackRequest(self.team.token, SLACK_API_TRANSLATOR[self.type]["mark"], {"channel": self.identifier, "ts": ts}, team_hash=self.team.team_hash, channel_identifier=self.identifier)
self.eventrouter.receive(s)
self.new_messages = False
+
def user_joined(self, user_id):
- #ugly hack - for some reason this gets turned into a list
+ # ugly hack - for some reason this gets turned into a list
self.members = set(self.members)
self.members.add(user_id)
self.update_nicklist(user_id)
+
def user_left(self, user_id):
- #pass
- #filter(lambda u: u != user_id, self.members)
self.members.discard(user_id)
self.update_nicklist(user_id)
+
def update_nicklist(self, user=None):
if not self.channel_buffer:
return
- if self.type not in ["channel", "group"]:
+ if self.type not in ["channel", "group", "mpim"]:
return
w.buffer_set(self.channel_buffer, "nicklist", "1")
# create nicklists for the current channel if they don't exist
# 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]
- nick = w.nicklist_search_nick(self.channel_buffer, "", user.slack_name)
+ if user.deleted:
+ return
+ nick = w.nicklist_search_nick(self.channel_buffer, "", user.name)
# 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:
@@ -1321,13 +1581,17 @@ 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))
+ dbg("DEBUG: {} {} {}".format(self.identifier, self.name, decode_from_utf8(e)))
else:
+ w.nicklist_remove_all(self.channel_buffer)
for fn in ["1| too", "2| many", "3| users", "4| to", "5| show"]:
w.nicklist_add_group(self.channel_buffer, '', fn, w.color('white'), 1)
+
def hash_message(self, ts):
ts = SlackTS(ts)
@@ -1337,15 +1601,15 @@ class SlackChannel(object):
if ts in self.messages and not self.messages[ts].hash:
message = self.messages[ts]
tshash = calc_hash(message)
- l = 3
- shorthash = tshash[:l]
+ hl = 3
+ shorthash = tshash[:hl]
while any(x.startswith(shorthash) for x in self.hashed_messages):
- l += 1
- shorthash = tshash[:l]
+ hl += 1
+ shorthash = tshash[:hl]
if shorthash[:-1] in self.hashed_messages:
col_msg = self.hashed_messages.pop(shorthash[:-1])
- col_new_hash = calc_hash(col_msg)[:l]
+ col_new_hash = calc_hash(col_msg)[:hl]
col_msg.hash = col_new_hash
self.hashed_messages[col_new_hash] = col_msg
self.change_message(str(col_msg.ts))
@@ -1361,6 +1625,7 @@ class SlackDMChannel(SlackChannel):
Subclass of a normal channel for person-to-person communication, which
has some important differences.
"""
+
def __init__(self, eventrouter, users, **kwargs):
dmuser = kwargs["user"]
kwargs["name"] = users[dmuser].name
@@ -1368,20 +1633,27 @@ class SlackDMChannel(SlackChannel):
self.type = 'im'
self.update_color()
self.set_name(self.slack_name)
- self.slack_topic = {"value": create_user_status_string(users[dmuser].profile)}
+ self.topic = create_user_status_string(users[dmuser].profile)
+
def set_name(self, slack_name):
self.name = slack_name
+
+ def get_members(self):
+ return {self.user}
+
def create_buffer(self):
if not self.channel_buffer:
super(SlackDMChannel, self).create_buffer()
w.buffer_set(self.channel_buffer, "localvar_set_type", 'private')
+
def update_color(self):
if config.colorize_private_chats:
- self.color_name = w.info_get('irc_nick_color_name', self.name.encode('utf-8'))
+ self.color_name = get_nick_color_name(self.name)
self.color = w.color(self.color_name)
else:
self.color = ""
self.color_name = ""
+
def formatted_name(self, style="default", typing=False, present=True, enable_color=False, **kwargs):
if config.colorize_private_chats and enable_color:
print_color = self.color
@@ -1399,18 +1671,20 @@ class SlackDMChannel(SlackChannel):
"long_base": "{}.{}".format(self.team.preferred_name, self.slack_name),
}
return print_color + select[style]
+
def open(self, update_remote=True):
self.create_buffer()
- #self.active = True
+ # 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)
if update_remote:
if "join" in SLACK_API_TRANSLATOR[self.type]:
- s = SlackRequest(self.team.token, SLACK_API_TRANSLATOR[self.type]["join"], {"user": self.user}, team_hash=self.team.team_hash, channel_identifier=self.identifier)
+ s = SlackRequest(self.team.token, SLACK_API_TRANSLATOR[self.type]["join"], {"users": self.user, "return_im": True}, team_hash=self.team.team_hash, channel_identifier=self.identifier)
self.eventrouter.receive(s)
self.create_buffer()
+
def rename(self):
if self.channel_buffer:
new_name = self.formatted_name(style="sidebar", present=self.team.is_user_present(self.user), enable_color=config.colorize_private_chats)
@@ -1419,6 +1693,7 @@ class SlackDMChannel(SlackChannel):
w.buffer_set(self.channel_buffer, "short_name", new_name)
return True
return False
+
def refresh(self):
return self.rename()
@@ -1427,45 +1702,56 @@ class SlackGroupChannel(SlackChannel):
"""
A group channel is a private discussion group.
"""
+
def __init__(self, eventrouter, **kwargs):
super(SlackGroupChannel, self).__init__(eventrouter, **kwargs)
- self.name = "#" + kwargs['name']
self.type = "group"
self.set_name(self.slack_name)
+
def set_name(self, slack_name):
- self.name = "#" + slack_name
- #def formatted_name(self, prepend="#", enable_color=True, basic=False):
+ self.name = config.group_name_prefix + slack_name
+
+ # def formatted_name(self, prepend="#", enable_color=True, basic=False):
# return prepend + self.slack_name
+
class SlackMPDMChannel(SlackChannel):
"""
An MPDM channel is a special instance of a 'group' channel.
We change the name to look less terrible in weechat.
"""
+
def __init__(self, eventrouter, **kwargs):
super(SlackMPDMChannel, self).__init__(eventrouter, **kwargs)
n = kwargs.get('name')
self.set_name(n)
- self.type = "group"
- def open(self, update_remote=False):
+ self.type = "mpim"
+
+ def open(self, update_remote=True):
self.create_buffer()
self.active = True
self.get_history()
if "info" in SLACK_API_TRANSLATOR[self.type]:
- s = SlackRequest(self.team.token, SLACK_API_TRANSLATOR[self.type]["info"], {"name": self.identifier}, team_hash=self.team.team_hash, channel_identifier=self.identifier)
+ s = SlackRequest(self.team.token, SLACK_API_TRANSLATOR[self.type]["info"], {"channel": self.identifier}, team_hash=self.team.team_hash, channel_identifier=self.identifier)
self.eventrouter.receive(s)
- #self.create_buffer()
+ if update_remote and 'join' in SLACK_API_TRANSLATOR[self.type]:
+ s = SlackRequest(self.team.token, SLACK_API_TRANSLATOR[self.type]['join'], {'users': ','.join(self.members)}, team_hash=self.team.team_hash, channel_identifier=self.identifier)
+ self.eventrouter.receive(s)
+ # self.create_buffer()
+
+ @staticmethod
+ def adjust_name(n):
+ return "|".join("-".join(n.split("-")[1:-1]).split("--"))
+
def set_name(self, n):
- self.name = "|".join("-".join(n.split("-")[1:-1]).split("--"))
+ self.name = self.adjust_name(n)
+
def formatted_name(self, style="default", typing=False, **kwargs):
- adjusted_name = "|".join("-".join(self.slack_name.split("-")[1:-1]).split("--"))
- if config.channel_name_typing_indicator:
- if not typing:
- prepend = "#"
- else:
- prepend = ">"
+ adjusted_name = self.adjust_name(self.slack_name)
+ if typing and config.channel_name_typing_indicator:
+ prepend = ">"
else:
- prepend = "#"
+ prepend = "@"
select = {
"default": adjusted_name,
"sidebar": prepend + adjusted_name,
@@ -1475,28 +1761,31 @@ class SlackMPDMChannel(SlackChannel):
}
return select[style]
-# def formatted_name(self, **kwargs):
-# return self.name
def rename(self):
pass
+
class SlackThreadChannel(object):
"""
A thread channel is a virtual channel. We don't inherit from
SlackChannel, because most of how it operates will be different.
"""
+
def __init__(self, eventrouter, parent_message):
self.eventrouter = eventrouter
self.parent_message = parent_message
self.channel_buffer = None
- #self.identifier = ""
- #self.name = "#" + kwargs['name']
+ # self.identifier = ""
+ # self.name = "#" + kwargs['name']
self.type = "thread"
self.got_history = False
self.label = None
- #self.set_name(self.slack_name)
- #def set_name(self, slack_name):
+ self.members = self.parent_message.channel.members
+ self.team = self.parent_message.team
+ # self.set_name(self.slack_name)
+ # def set_name(self, slack_name):
# self.name = "#" + slack_name
+
def formatted_name(self, style="default", **kwargs):
hash_or_ts = self.parent_message.hash or self.parent_message.ts
styles = {
@@ -1505,65 +1794,68 @@ class SlackThreadChannel(object):
"sidebar": " +{}".format(hash_or_ts),
}
return styles[style]
+
def refresh(self):
self.rename()
+
def mark_read(self, ts=None, update_remote=True, force=False):
if self.channel_buffer:
w.buffer_set(self.channel_buffer, "unread", "")
w.buffer_set(self.channel_buffer, "hotlist", "-1")
def buffer_prnt(self, nick, text, timestamp, **kwargs):
- data = "{}\t{}".format(nick, text)
+ data = "{}\t{}".format(format_nick(nick), text)
ts = SlackTS(timestamp)
if self.channel_buffer:
- #backlog messages - we will update the read marker as we print these
- #backlog = False
- #if ts <= SlackTS(self.last_read):
+ # backlog messages - we will update the read marker as we print these
+ # backlog = False
+ # if ts <= SlackTS(self.last_read):
# tags = tag("backlog")
# backlog = True
- #elif self.type in ["im", "mpdm"]:
+ # elif self.type in ["im", "mpdm"]:
# tags = tag("dm")
# self.new_messages = True
- #else:
+ # else:
tags = tag("default")
- #self.new_messages = True
+ # self.new_messages = True
w.prnt_date_tags(self.channel_buffer, ts.major, tags, data)
modify_print_time(self.channel_buffer, ts.minorstr(), ts.major)
- #if backlog:
+ # if backlog:
# self.mark_read(ts, update_remote=False, force=True)
+
def get_history(self):
self.got_history = True
for message in self.parent_message.submessages:
- #message = SlackMessage(message_json, team, channel)
+ # message = SlackMessage(message_json, team, channel)
text = message.render()
- #print text
+ # print text
suffix = ''
if 'edited' in message.message_json:
suffix = ' (edited)'
- #try:
+ # try:
# channel.unread_count += 1
- #except:
+ # except:
# channel.unread_count = 1
self.buffer_prnt(message.sender, text + suffix, message.ts)
def send_message(self, message):
- #team = self.eventrouter.teams[self.team]
- message = linkify_text(message, self.parent_message.team, self)
+ # team = self.eventrouter.teams[self.team]
+ message = linkify_text(message, self.team, self)
dbg(message)
- request = {"type": "message", "channel": self.parent_message.channel.identifier, "text": message, "_team": self.parent_message.team.team_hash, "user": self.parent_message.team.myidentifier, "thread_ts": str(self.parent_message.ts)}
- self.parent_message.team.send_to_websocket(request)
+ request = {"type": "message", "channel": self.parent_message.channel.identifier, "text": message, "_team": self.team.team_hash, "user": self.team.myidentifier, "thread_ts": str(self.parent_message.ts)}
+ self.team.send_to_websocket(request)
self.mark_read(update_remote=False, force=True)
def open(self, update_remote=True):
self.create_buffer()
self.active = True
self.get_history()
- #if "info" in SLACK_API_TRANSLATOR[self.type]:
+ # 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)
- #if update_remote:
+ # if update_remote:
# if "join" in SLACK_API_TRANSLATOR[self.type]:
# s = SlackRequest(self.team.token, SLACK_API_TRANSLATOR[self.type]["join"], {"name": self.name}, team_hash=self.team.team_hash, channel_identifier=self.identifier)
# self.eventrouter.receive(s)
@@ -1582,16 +1874,17 @@ 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.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"))
parent_time = time.localtime(SlackTS(self.parent_message.ts).major)
topic = '{} {} | {}'.format(time.strftime(time_format, parent_time), self.parent_message.sender, self.parent_message.render() )
- w.buffer_set(self.channel_buffer, "title", topic.encode('utf-8'))
+ w.buffer_set(self.channel_buffer, "title", topic)
- #self.eventrouter.weechat_controller.set_refresh_buffer_list(True)
+ # self.eventrouter.weechat_controller.set_refresh_buffer_list(True)
- #try:
+ # try:
# if self.unread_count != 0:
# for c in range(1, self.unread_count):
# if self.type == "im":
@@ -1601,50 +1894,66 @@ class SlackThreadChannel(object):
# else:
# pass
# #dbg("no unread in {}".format(self.name))
- #except:
+ # except:
# pass
- #dbg("exception no unread count")
- #if self.unread_count != 0 and not self.muted:
+ # dbg("exception no unread count")
+ # if self.unread_count != 0 and not self.muted:
# w.buffer_set(self.channel_buffer, "hotlist", "1")
+
def destroy_buffer(self, update_remote):
if self.channel_buffer is not None:
self.channel_buffer = None
self.got_history = False
- #if update_remote and not eventrouter.shutting_down:
+ # if update_remote and not eventrouter.shutting_down:
self.active = False
+
class SlackUser(object):
"""
Represends an individual slack user. Also where you set their name formatting.
"""
+
def __init__(self, **kwargs):
- # We require these two things for a vaid object,
+ # We require these two things for a valid object,
# the rest we can just learn from slack
self.identifier = kwargs["id"]
- self.slack_name = kwargs["name"]
- self.name = kwargs["name"]
+ self.profile = {} # in case it's not in kwargs
for key, value in kwargs.items():
setattr(self, key, value)
+
+ if self.profile.get("display_name"):
+ self.slack_name = self.profile["display_name"]
+ self.name = self.profile["display_name"].replace(' ', '')
+ else:
+ # No display name set. Fall back to the deprecated username field.
+ self.slack_name = kwargs["name"]
+ self.name = self.slack_name
self.update_color()
+
def __repr__(self):
return "Name:{} Identifier:{}".format(self.name, self.identifier)
+
def force_color(self, color_name):
self.color_name = color_name
self.color = w.color(self.color_name)
+
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.encode('utf-8'))
+ self.color_name = get_nick_color_name(self.name)
self.color = w.color(self.color_name)
+
def update_status(self, status_emoji, status_text):
self.profile["status_emoji"] = status_emoji
self.profile["status_text"] = status_text
+
def formatted_name(self, prepend="", enable_color=True):
if enable_color:
return self.color + prepend + self.name
else:
return prepend + self.name
+
class SlackBot(SlackUser):
"""
Basically the same as a user, but split out to identify and for future
@@ -1653,6 +1962,7 @@ class SlackBot(SlackUser):
def __init__(self, **kwargs):
super(SlackBot, self).__init__(**kwargs)
+
class SlackMessage(object):
"""
Represents a single slack message and associated context/metadata.
@@ -1676,8 +1986,8 @@ class SlackMessage(object):
self.sender, self.sender_plain = senders[0], senders[1]
self.suffix = ''
self.ts = SlackTS(message_json['ts'])
- text = self.message_json.get('text', '')
- if text.startswith('_') and text.endswith('_') and not 'subtype' in message_json:
+ text = self.message_json.get('text')
+ if text and text.startswith('_') and text.endswith('_') and 'subtype' not in message_json:
message_json['text'] = text[1:-1]
message_json['subtype'] = 'me_message'
if message_json.get('subtype') == 'me_message' and not message_json['text'].startswith(self.sender):
@@ -1685,46 +1995,49 @@ class SlackMessage(object):
def __hash__(self):
return hash(self.ts)
+
def render(self, force=False):
if len(self.submessages) > 0:
return "{} {} {}".format(render(self.message_json, self.team, self.channel, force), self.suffix, "{}[ Thread: {} Replies: {} ]".format(w.color(config.thread_suffix_color), self.hash or self.ts, len(self.submessages)))
return "{} {}".format(render(self.message_json, self.team, self.channel, force), self.suffix)
+
def change_text(self, new_text):
self.message_json["text"] = new_text
dbg(self.message_json)
+
def change_suffix(self, new_suffix):
self.suffix = new_suffix
dbg(self.message_json)
- def get_sender(self, utf8=True):
- name = u""
- name_plain = u""
- if 'bot_id' in self.message_json and self.message_json['bot_id'] is not None:
- name = u"{} :]".format(self.team.bots[self.message_json["bot_id"]].formatted_name())
- name_plain = u"{}".format(self.team.bots[self.message_json["bot_id"]].formatted_name(enable_color=False))
- elif 'user' in self.message_json:
+
+ def get_sender(self):
+ name = ""
+ name_plain = ""
+ if 'user' in self.message_json:
if self.message_json['user'] == self.team.myidentifier:
- name = self.team.users[self.team.myidentifier].name
- name_plain = self.team.users[self.team.myidentifier].name
+ u = self.team.users[self.team.myidentifier]
elif self.message_json['user'] in self.team.users:
u = self.team.users[self.message_json['user']]
- if u.is_bot:
- name = u"{} :]".format(u.formatted_name())
- else:
- name = u"{}".format(u.formatted_name())
- name_plain = u"{}".format(u.formatted_name(enable_color=False))
+ name = "{}".format(u.formatted_name())
+ name_plain = "{}".format(u.formatted_name(enable_color=False))
elif 'username' in self.message_json:
- name = u"-{}-".format(self.message_json["username"])
- name_plain = u"{}".format(self.message_json["username"])
+ u = self.message_json["username"]
+ if self.message_json.get("subtype") == "bot_message":
+ name = "{} :]".format(u)
+ name_plain = "{}".format(u)
+ else:
+ name = "-{}-".format(u)
+ name_plain = "{}".format(u)
elif 'service_name' in self.message_json:
- name = u"-{}-".format(self.message_json["service_name"])
- name_plain = u"{}".format(self.message_json["service_name"])
- else:
- name = u""
- name_plain = u""
- if utf8:
- return (name.encode('utf-8'), name_plain.encode('utf-8'))
+ name = "-{}-".format(self.message_json["service_name"])
+ name_plain = "{}".format(self.message_json["service_name"])
+ elif 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))
else:
- return (name, name_plain)
+ name = ""
+ name_plain = ""
+ return (name, name_plain)
+
def add_reaction(self, reaction, user):
m = self.message_json.get('reactions', None)
if m:
@@ -1734,9 +2047,10 @@ class SlackMessage(object):
r["users"].append(user)
found = True
if not found:
- self.message_json["reactions"].append({u"name": reaction, u"users": [user]})
+ self.message_json["reactions"].append({"name": reaction, "users": [user]})
else:
- self.message_json["reactions"] = [{u"name": reaction, u"users": [user]}]
+ self.message_json["reactions"] = [{"name": reaction, "users": [user]}]
+
def remove_reaction(self, reaction, user):
m = self.message_json.get('reactions', None)
if m:
@@ -1746,10 +2060,11 @@ class SlackMessage(object):
else:
pass
+
class SlackThreadMessage(SlackMessage):
+
def __init__(self, parent_id, *args):
super(SlackThreadMessage, self).__init__(*args)
- #super(SlackBot, self).__init__(**kwargs)
self.parent_id = parent_id
@@ -1757,18 +2072,23 @@ class WeeSlackMetadata(object):
"""
A simple container that we pickle/unpickle to hold data.
"""
+
def __init__(self, meta):
self.meta = meta
+
def jsonify(self):
return self.meta
+
class SlackTS(object):
+
def __init__(self, ts=None):
if ts:
self.major, self.minor = [int(x) for x in ts.split('.', 1)]
else:
self.major = int(time.time())
self.minor = 0
+
def __cmp__(self, other):
if isinstance(other, SlackTS):
if self.major < other.major:
@@ -1790,106 +2110,145 @@ class SlackTS(object):
return 1
elif s == other:
return 0
+
def __hash__(self):
return hash("{}.{}".format(self.major, self.minor))
+
def __repr__(self):
return str("{0}.{1:06d}".format(self.major, self.minor))
+
def split(self, *args, **kwargs):
return [self.major, self.minor]
+
def majorstr(self):
return str(self.major)
+
def minorstr(self):
return str(self.minor)
###### New handlers
+
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"])
- metadata = pickle.loads(login_data["wee_slack_request_metadata"])
+ if not login_data["ok"]:
+ w.prnt("", "ERROR: Failed connecting to Slack with token starting with {}: {}"
+ .format(metadata.token[:15], login_data["error"]))
+ return
- #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):
+ # 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))
+ users = {}
+ for item in login_data["users"]:
+ users[item["id"]] = SlackUser(**item)
- bots = {}
- for item in login_data["bots"]:
- bots[item["id"]] = SlackBot(**item)
+ 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)
+ 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)
+ for item in login_data["ims"]:
+ channels[item["id"]] = SlackDMChannel(eventrouter, users, **item)
- for item in login_data["groups"]:
- if item["name"].startswith('mpdm-'):
- channels[item["id"]] = SlackMPDMChannel(eventrouter, **item)
- else:
- channels[item["id"]] = SlackGroupChannel(eventrouter, **item)
-
- t = SlackTeam(
- eventrouter,
- metadata.token,
- login_data['url'],
- login_data["team"]["domain"],
- login_data["self"]["name"],
- login_data["self"]["id"],
- users,
- bots,
- channels,
- muted_channels=login_data["self"]["prefs"]["muted_channels"],
- highlight_words=login_data["self"]["prefs"]["highlight_words"],
- )
- eventrouter.register_team(t)
+ 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_emojilist(emoji_json, eventrouter, **kwargs):
+ if emoji_json["ok"]:
+ request_metadata = pickle.loads(emoji_json["wee_slack_request_metadata"])
+ team = eventrouter.teams[request_metadata.team_hash]
+ team.emoji_completions.extend(emoji_json["emoji"].keys())
+
+
+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]
+ channel.set_unread_count_display(channel_json['channel']['unread_count_display'])
+ channel.set_members(channel_json['channel']['members'])
+
+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_conversationsopen(conversation_json, eventrouter, object_name='channel', **kwargs):
+ request_metadata = pickle.loads(conversation_json["wee_slack_request_metadata"])
+ # Set unread count if the channel isn't new (channel_identifier exists)
+ if hasattr(request_metadata, 'channel_identifier'):
+ channel_id = request_metadata.channel_identifier
+ team = eventrouter.teams[request_metadata.team_hash]
+ conversation = team.channels[channel_id]
+ unread_count_display = conversation_json[object_name]['unread_count_display']
+ conversation.set_unread_count_display(unread_count_display)
+
+
+def handle_mpimopen(mpim_json, eventrouter, object_name='group', **kwargs):
+ handle_conversationsopen(mpim_json, eventrouter, object_name, **kwargs)
- else:
- t = eventrouter.teams.get(th)
- t.set_reconnect_url(login_data['url'])
- t.connect()
-
- #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
-
- t.buffer_prnt('Connected to Slack')
- t.buffer_prnt('{:<20} {}'.format(u"Websocket URL", login_data["url"]))
- t.buffer_prnt('{:<20} {}'.format(u"User name", login_data["self"]["name"]))
- t.buffer_prnt('{:<20} {}'.format(u"User ID", login_data["self"]["id"]))
- t.buffer_prnt('{:<20} {}'.format(u"Team name", login_data["team"]["name"]))
- t.buffer_prnt('{:<20} {}'.format(u"Team domain", login_data["team"]["domain"]))
- t.buffer_prnt('{:<20} {}'.format(u"Team id", login_data["team"]["id"]))
-
- dbg("connected to {}".format(t.domain))
-
- #self.identifier = self.domain
def handle_groupshistory(message_json, eventrouter, **kwargs):
handle_history(message_json, eventrouter, **kwargs)
+
def handle_channelshistory(message_json, eventrouter, **kwargs):
handle_history(message_json, eventrouter, **kwargs)
+
def handle_imhistory(message_json, eventrouter, **kwargs):
handle_history(message_json, eventrouter, **kwargs)
+
+def handle_mpimhistory(message_json, eventrouter, **kwargs):
+ handle_history(message_json, eventrouter, **kwargs)
+
+
def handle_history(message_json, eventrouter, **kwargs):
request_metadata = pickle.loads(message_json["wee_slack_request_metadata"])
kwargs['team'] = eventrouter.teams[request_metadata.team_hash]
@@ -1907,25 +2266,40 @@ def handle_history(message_json, eventrouter, **kwargs):
###### New/converted process_ and subprocess_ methods
+def process_hello(message_json, eventrouter, **kwargs):
+ kwargs['team'].subscribe_users_presence()
+
def process_reconnect_url(message_json, eventrouter, **kwargs):
kwargs['team'].set_reconnect_url(message_json['url'])
+
def process_manual_presence_change(message_json, eventrouter, **kwargs):
process_presence_change(message_json, eventrouter, **kwargs)
def process_presence_change(message_json, eventrouter, **kwargs):
- kwargs["user"].presence = message_json["presence"]
+ if "user" in kwargs:
+ # TODO: remove once it's stable
+ user = kwargs["user"]
+ team = kwargs["team"]
+ team.update_member_presence(user, message_json["presence"])
+ if "users" in message_json:
+ team = kwargs["team"]
+ for user_id in message_json["users"]:
+ user = team.users[user_id]
+ team.update_member_presence(user, message_json["presence"])
+
def process_pref_change(message_json, eventrouter, **kwargs):
team = kwargs["team"]
- if message_json['name'] == u'muted_channels':
+ if message_json['name'] == 'muted_channels':
team.set_muted_channels(message_json['value'])
- elif message_json['name'] == u'highlight_words':
+ elif message_json['name'] == 'highlight_words':
team.set_highlight_words(message_json['value'])
else:
dbg("Preference change not implemented: {}\n".format(message_json['name']))
+
def process_user_change(message_json, eventrouter, **kwargs):
"""
Currently only used to update status, but lots here we could do.
@@ -1937,6 +2311,7 @@ def process_user_change(message_json, eventrouter, **kwargs):
dmchannel = team.get_channel_map()[user["name"]]
team.channels[dmchannel].render_topic(topic=create_user_status_string(profile))
+
def process_user_typing(message_json, eventrouter, **kwargs):
channel = kwargs["channel"]
team = kwargs["team"]
@@ -1944,19 +2319,22 @@ def process_user_typing(message_json, eventrouter, **kwargs):
channel.set_typing(team.users.get(message_json["user"]).name)
w.bar_item_update("slack_typing_notice")
+
def process_team_join(message_json, eventrouter, **kwargs):
user = message_json['user']
team = kwargs["team"]
team.users[user["id"]] = SlackUser(**user)
+
def process_pong(message_json, eventrouter, **kwargs):
pass
+
def process_message(message_json, eventrouter, store=True, **kwargs):
channel = kwargs["channel"]
team = kwargs["team"]
- #try:
- # send these subtype messages elsewhere
+ # try:
+ # send these subtype messages elsewhere
known_subtypes = [
'thread_message',
'message_replied',
@@ -1965,8 +2343,8 @@ def process_message(message_json, eventrouter, store=True, **kwargs):
'channel_join',
'channel_leave',
'channel_topic',
- #'group_join',
- #'group_leave',
+ # 'group_join',
+ # 'group_leave',
]
if "thread_ts" in message_json and "reply_count" not in message_json:
message_json["subtype"] = "thread_message"
@@ -2005,16 +2383,15 @@ def process_message(message_json, eventrouter, store=True, **kwargs):
if store:
channel.store_message(message, team)
dbg("NORMAL REPLY {}".format(message_json))
- #except:
- # channel.buffer_prnt("WEE-SLACK-ERROR", json.dumps(message_json).encode('utf-8'), message_json["ts"], **kwargs)
+ # except:
+ # channel.buffer_prnt("WEE-SLACK-ERROR", json.dumps(message_json), message_json["ts"], **kwargs)
# traceback.print_exc()
+
def subprocess_thread_message(message_json, eventrouter, channel, team):
- #print ("THREADED: " + str(message_json))
+ # print ("THREADED: " + str(message_json))
parent_ts = message_json.get('thread_ts', None)
if parent_ts:
- #parent_ts = SlackTS(parent_ts)
- parent_ts = parent_ts
parent_message = channel.messages.get(SlackTS(parent_ts), None)
if parent_message:
message = SlackThreadMessage(parent_ts, message_json, team, channel)
@@ -2024,7 +2401,7 @@ def subprocess_thread_message(message_json, eventrouter, channel, team):
channel.change_message(parent_ts)
text = message.render()
- #channel.buffer_prnt(message.sender, text, message.ts, **kwargs)
+ # channel.buffer_prnt(message.sender, text, message.ts, **kwargs)
if parent_message.thread_channel:
parent_message.thread_channel.buffer_prnt(message.sender, text, message.ts)
@@ -2040,11 +2417,12 @@ def subprocess_thread_message(message_json, eventrouter, channel, team):
# else:
# dbg("COULDN'T find orig message {}".format(message_json['thread_ts']), main_buffer=True)
- #if threadinfo[0]:
+ # if threadinfo[0]:
# channel.messages[threadinfo[1]].become_thread()
# message_json["item"]["ts"], message_json)
- #channel.change_message(message_json["thread_ts"], None, message_json["text"])
- #channel.become_thread(message_json["item"]["ts"], message_json)
+ # channel.change_message(message_json["thread_ts"], None, message_json["text"])
+ # channel.become_thread(message_json["item"]["ts"], message_json)
+
def subprocess_channel_join(message_json, eventrouter, channel, team):
joinprefix = w.prefix("join")
@@ -2052,23 +2430,25 @@ def subprocess_channel_join(message_json, eventrouter, channel, team):
channel.buffer_prnt(joinprefix, message.render(), message_json["ts"], tagset='joinleave')
channel.user_joined(message_json['user'])
+
def subprocess_channel_leave(message_json, eventrouter, channel, team):
leaveprefix = w.prefix("quit")
message = SlackMessage(message_json, team, channel, override_sender=leaveprefix)
channel.buffer_prnt(leaveprefix, message.render(), message_json["ts"], tagset='joinleave')
channel.user_left(message_json['user'])
- #channel.update_nicklist(message_json['user'])
- #channel.update_nicklist()
+ # channel.update_nicklist(message_json['user'])
+ # channel.update_nicklist()
+
def subprocess_message_replied(message_json, eventrouter, channel, team):
pass
- #print ("REPLIED: " + str(message_json))
+
def subprocess_message_changed(message_json, eventrouter, channel, team):
m = message_json.get("message", None)
if m:
new_message = m
- #message = SlackMessage(new_message, team, channel)
+ # message = SlackMessage(new_message, team, channel)
if "attachments" in m:
message_json["attachments"] = m["attachments"]
if "text" in m:
@@ -2083,23 +2463,22 @@ def subprocess_message_changed(message_json, eventrouter, channel, team):
else:
message_json["fallback"] = m["fallback"]
- text_before = (len(new_message['text']) > 0)
- new_message["text"] += unwrap_attachments(message_json, text_before)
+ new_message["text"] += unwrap_attachments(message_json, new_message["text"])
if "edited" in new_message:
channel.change_message(new_message["ts"], new_message["text"], ' (edited)')
else:
channel.change_message(new_message["ts"], new_message["text"])
+
def subprocess_message_deleted(message_json, eventrouter, channel, team):
channel.change_message(message_json["deleted_ts"], "(deleted)", '')
+
def subprocess_channel_topic(message_json, eventrouter, channel, team):
- text = unfurl_refs(message_json["text"], ignore_alt_text=False)
- if type(text) != unicode:
- text = text.decode('utf-8', 'ignore')
- text = text.encode('utf-8')
+ 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.set_topic(unhtmlescape(message_json["topic"]))
+
def process_reply(message_json, eventrouter, **kwargs):
dbg('processing reply')
@@ -2116,10 +2495,8 @@ def process_reply(message_json, eventrouter, **kwargs):
c = original_message_json.get('channel', None)
channel = team.channels[c]
m = SlackMessage(original_message_json, team, channel)
- # m = Message(message_json, server=server)
- #dbg(m, True)
- #if "type" in message_json:
+ # if "type" in message_json:
# if message_json["type"] == "message" and "channel" in message_json.keys():
# message_json["ts"] = message_json["ts"]
# channels.find(message_json["channel"]).store_message(m, from_me=True)
@@ -2132,6 +2509,7 @@ def process_reply(message_json, eventrouter, **kwargs):
except KeyError:
dbg("Unexpected reply {}".format(message_json))
+
def process_channel_marked(message_json, eventrouter, **kwargs):
"""
complete
@@ -2142,29 +2520,39 @@ def process_channel_marked(message_json, eventrouter, **kwargs):
channel.mark_read(ts=ts, force=True, update_remote=False)
else:
dbg("tried to mark something weird {}".format(message_json))
+
+
def process_group_marked(message_json, eventrouter, **kwargs):
process_channel_marked(message_json, eventrouter, **kwargs)
+
+
def process_im_marked(message_json, eventrouter, **kwargs):
process_channel_marked(message_json, eventrouter, **kwargs)
+
+
def process_mpim_marked(message_json, eventrouter, **kwargs):
process_channel_marked(message_json, eventrouter, **kwargs)
+
def process_channel_joined(message_json, eventrouter, **kwargs):
item = message_json["channel"]
kwargs['team'].channels[item["id"]].update_from_message_json(item)
kwargs['team'].channels[item["id"]].open()
+
def process_channel_created(message_json, eventrouter, **kwargs):
item = message_json["channel"]
c = SlackChannel(eventrouter, team=kwargs["team"], **item)
kwargs['team'].channels[item["id"]] = c
kwargs['team'].buffer_prnt('Channel created: {}'.format(c.slack_name))
+
def process_channel_rename(message_json, eventrouter, **kwargs):
item = message_json["channel"]
channel = kwargs['team'].channels[item["id"]]
channel.slack_name = message_json['channel']['name']
+
def process_im_created(message_json, eventrouter, **kwargs):
team = kwargs['team']
item = message_json["channel"]
@@ -2172,17 +2560,20 @@ def process_im_created(message_json, eventrouter, **kwargs):
team.channels[item["id"]] = c
kwargs['team'].buffer_prnt('IM channel created: {}'.format(c.name))
+
def process_im_open(message_json, eventrouter, **kwargs):
channel = kwargs['channel']
item = message_json
kwargs['team'].channels[item["channel"]].check_should_open(True)
w.buffer_set(channel.channel_buffer, "hotlist", "2")
+
def process_im_close(message_json, eventrouter, **kwargs):
item = message_json
cbuf = kwargs['team'].channels[item["channel"]].channel_buffer
eventrouter.weechat_controller.unregister_buffer(cbuf, False, True)
+
def process_group_joined(message_json, eventrouter, **kwargs):
item = message_json["channel"]
if item["name"].startswith("mpdm-"):
@@ -2192,8 +2583,9 @@ def process_group_joined(message_json, eventrouter, **kwargs):
kwargs['team'].channels[item["id"]] = c
kwargs['team'].channels[item["id"]].open()
+
def process_reaction_added(message_json, eventrouter, **kwargs):
- channel = kwargs['team'].channels[message_json["item"]["channel"]]
+ channel = kwargs['team'].channels.get(message_json["item"].get("channel"))
if message_json["item"].get("type") == "message":
ts = SlackTS(message_json['item']["ts"])
@@ -2204,8 +2596,9 @@ def process_reaction_added(message_json, eventrouter, **kwargs):
else:
dbg("reaction to item type not supported: " + str(message_json))
+
def process_reaction_removed(message_json, eventrouter, **kwargs):
- channel = kwargs['team'].channels[message_json["item"]["channel"]]
+ channel = kwargs['team'].channels.get(message_json["item"].get("channel"))
if message_json["item"].get("type") == "message":
ts = SlackTS(message_json['item']["ts"])
@@ -2216,8 +2609,25 @@ def process_reaction_removed(message_json, eventrouter, **kwargs):
else:
dbg("Reaction to item type not supported: " + str(message_json))
+
+def process_emoji_changed(message_json, eventrouter, **kwargs):
+ team = kwargs['team']
+ team.load_emoji_completions()
+
###### 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.
if not force and message_json.get("_rendered_text", ""):
@@ -2231,50 +2641,48 @@ def render(message_json, team, channel, force=False):
if message_json['text'] is not None:
text = message_json["text"]
else:
- text = u""
+ text = ""
else:
- text = u""
+ text = ""
- text = unfurl_refs(text, ignore_alt_text=config.unfurl_ignore_alt_text)
+ text = unfurl_refs(text)
- text_before = (len(text) > 0)
- text += unfurl_refs(unwrap_attachments(message_json, text_before), ignore_alt_text=config.unfurl_ignore_alt_text)
+ text += unfurl_refs(unwrap_attachments(message_json, text))
text = text.lstrip()
- text = 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)
-
- if type(text) is not unicode:
- text = text.decode('UTF-8', 'replace')
- text = text.encode('utf-8')
+ 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)
# #for thread in self.threads:
text += create_reaction_string(message_json.get("reactions", ""))
-
message_json["_rendered_text"] = text
-
return text
+
def linkify_text(message, team, channel):
# The get_username_map function is a bit heavy, but this whole
# 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])
- #print targets
if targets and targets.groups()[0] == '@':
- #print targets.groups()
named = targets.groups()
if named[1] in ["group", "channel", "here"]:
message[item[0]] = "<!{}>".format(named[1])
@@ -2292,10 +2700,11 @@ def linkify_text(message, team, channel):
except:
message[item[0]] = "#{}{}".format(named[1], named[2])
- #dbg(message)
+ # dbg(message)
return " ".join(message)
-def unfurl_refs(text, ignore_alt_text=False):
+
+def unfurl_refs(text, ignore_alt_text=None, auto_link_display=None):
"""
input : <@U096Q7CQM|someuser> has joined the channel
ouput : someuser has joined the channel
@@ -2305,13 +2714,21 @@ def unfurl_refs(text, ignore_alt_text=False):
# - <#C2147483705|#otherchannel>
# - <@U2147483697|@othernick>
# Test patterns lives in ./_pytest/test_unfurl.py
- matches = re.findall(r"(<[@#]?(?:[^<]*)>)", text)
+
+ if ignore_alt_text is None:
+ ignore_alt_text = config.unfurl_ignore_alt_text
+ if auto_link_display is None:
+ auto_link_display = config.unfurl_auto_link_display
+
+ matches = re.findall(r"(<[@#]?(?:[^>]*)>)", text)
for m in matches:
# Replace them with human readable strings
- text = text.replace(m, unfurl_ref(m[1:-1], ignore_alt_text))
+ text = text.replace(
+ m, unfurl_ref(m[1:-1], ignore_alt_text, auto_link_display))
return text
-def unfurl_ref(ref, ignore_alt_text=False):
+
+def unfurl_ref(ref, ignore_alt_text, auto_link_display):
id = ref.split('|')[0]
display_text = ref
if ref.find('|') > -1:
@@ -2324,17 +2741,32 @@ def unfurl_ref(ref, ignore_alt_text=False):
display_text = ref.split('|')[1]
else:
url, desc = ref.split('|', 1)
- display_text = u"{} ({})".format(url, desc)
+ match_url = r"^\w+:(//)?{}$".format(re.escape(desc))
+ url_matches_desc = re.match(match_url, url)
+ if url_matches_desc and auto_link_display == "text":
+ display_text = desc
+ elif url_matches_desc and auto_link_display == "url":
+ display_text = url
+ else:
+ display_text = "{} ({})".format(url, desc)
else:
display_text = resolve_ref(ref)
return display_text
+
+def unhtmlescape(text):
+ return text.replace("&lt;", "<") \
+ .replace("&gt;", ">") \
+ .replace("&amp;", "&")
+
+
def unwrap_attachments(message_json, text_before):
- attachment_text = ''
+ text_before_unescaped = unhtmlescape(text_before)
+ attachment_texts = []
a = message_json.get("attachments", None)
if a:
if text_before:
- attachment_text = u'\n'
+ attachment_texts.append('')
for attachment in a:
# Attachments should be rendered roughly like:
#
@@ -2349,14 +2781,18 @@ def unwrap_attachments(message_json, text_before):
if 'pretext' in attachment:
t.append(attachment['pretext'])
title = attachment.get('title', None)
- title_link = attachment.get('title_link', None)
+ title_link = attachment.get('title_link', '')
+ if title_link in text_before_unescaped:
+ title_link = ''
if title and title_link:
t.append('%s%s (%s)' % (prepend_title_text, title, title_link,))
prepend_title_text = ''
elif title and not title_link:
- t.append(prepend_title_text + title)
+ t.append('%s%s' % (prepend_title_text, title,))
prepend_title_text = ''
- t.append(attachment.get("from_url", ""))
+ from_url = attachment.get('from_url', '')
+ if from_url not in text_before_unescaped and from_url != title_link:
+ t.append(from_url)
atext = attachment.get("text", None)
if atext:
@@ -2373,39 +2809,41 @@ def unwrap_attachments(message_json, text_before):
fallback = attachment.get("fallback", None)
if t == [] and fallback:
t.append(fallback)
- attachment_text += "\n".join([x.strip() for x in t if x])
- return attachment_text
+ attachment_texts.append("\n".join([x.strip() for x in t if x]))
+ return "\n".join(attachment_texts)
def resolve_ref(ref):
- #TODO: This hack to use eventrouter needs to go
- #this resolver should probably move to the slackteam or eventrouter itself
- #global EVENTROUTER
+ # TODO: This hack to use eventrouter needs to go
+ # this resolver should probably move to the slackteam or eventrouter itself
+ # global EVENTROUTER
if 'EVENTROUTER' in globals():
e = EVENTROUTER
if ref.startswith('@U') or ref.startswith('@W'):
for t in e.teams.keys():
if ref[1:] in e.teams[t].users:
- #try:
+ # try:
return "@{}".format(e.teams[t].users[ref[1:]].name)
- #except:
+ # except:
# dbg("NAME: {}".format(ref))
elif ref.startswith('#C'):
for t in e.teams.keys():
if ref[1:] in e.teams[t].channels:
- #try:
+ # try:
return "{}".format(e.teams[t].channels[ref[1:]].name)
- #except:
+ # except:
# dbg("CHANNEL: {}".format(ref))
# Something else, just return as-is
return ref
+
def create_user_status_string(profile):
status_emoji = profile.get("status_emoji") if profile.get("status_emoji") else "None"
status_text = profile.get("status_text") if profile.get("status_text") else "None"
return "[{}] {}".format(status_emoji, status_text)
+
def create_reaction_string(reactions):
count = 0
if not isinstance(reactions, list):
@@ -2426,6 +2864,7 @@ def create_reaction_string(reactions):
reaction_string = ''
return reaction_string
+
def modify_buffer_line(buffer, new_line, timestamp, time_id):
# get a pointer to this buffer's lines
own_lines = w.hdata_pointer(w.hdata_get('buffer'), buffer, 'own_lines')
@@ -2435,6 +2874,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
@@ -2445,13 +2886,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
@@ -2460,6 +2920,7 @@ def modify_print_time(buffer, new_id, time):
This overloads the time printed field to let us store the slack
per message unique id that comes after the "." in a slack ts
"""
+
# get a pointer to this buffer's lines
own_lines = w.hdata_pointer(w.hdata_get('buffer'), buffer, 'own_lines')
if own_lines:
@@ -2469,13 +2930,35 @@ 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
+
+def format_nick(nick):
+ nick_prefix = w.config_string(w.config_get('weechat.look.nick_prefix'))
+ nick_prefix_color_name = w.config_string(w.config_get('weechat.color.chat_nick_prefix'))
+ nick_prefix_color = w.color(nick_prefix_color_name)
+
+ nick_suffix = w.config_string(w.config_get('weechat.look.nick_suffix'))
+ nick_suffix_color_name = w.config_string(w.config_get('weechat.color.chat_nick_prefix'))
+ nick_suffix_color = w.color(nick_suffix_color_name)
+ return nick_prefix_color + nick_prefix + w.color("reset") + nick + nick_suffix_color + nick_suffix + w.color("reset")
+
+
def tag(tagset, user=None):
if user:
user.replace(" ", "_")
@@ -2483,27 +2966,28 @@ def tag(tagset, user=None):
else:
default_tag = 'nick_unknown'
tagsets = {
- #when replaying something old
- "backlog": "no_highlight,notify_none,logger_backlog_end",
- #when posting messages to a muted channel
- "muted": "no_highlight,notify_none,logger_backlog_end",
- #when my nick is in the message
- "highlightme": "notify_highlight,log1",
- #when receiving a direct message
- "dm": "notify_private,notify_message,log1,irc_privmsg",
- "dmfromme": "notify_none,log1,irc_privmsg",
- #when this is a join/leave, attach for smart filter ala:
- #if user in [x.strip() for x in w.prefix("join"), w.prefix("quit")]
- "joinleave": "irc_smart_filter,no_highlight",
- #catchall ?
- "default": "notify_message,log1",
+ # messages in the team/server buffer, e.g. "new channel created"
+ "team": "no_highlight,log3",
+ # when replaying something old
+ "backlog": "irc_privmsg,no_highlight,notify_none,logger_backlog",
+ # when posting messages to a muted channel
+ "muted": "irc_privmsg,no_highlight,notify_none,log1",
+ # when receiving a direct message
+ "dm": "irc_privmsg,notify_private,log1",
+ "dmfromme": "irc_privmsg,no_highlight,notify_none,log1",
+ # when this is a join/leave, attach for smart filter ala:
+ # if user in [x.strip() for x in w.prefix("join"), w.prefix("quit")]
+ "joinleave": "irc_smart_filter,no_highlight,log4",
+ # catchall ?
+ "default": "irc_privmsg,notify_message,log1",
}
- return default_tag + "," + tagsets[tagset]
-
+ return "{},slack_{},{}".format(default_tag, tagset, tagsets[tagset])
###### New/converted command_ commands
+
@slack_buffer_or_ignore
+@utf8_decode
def part_command_cb(data, current_buffer, args):
e = EVENTROUTER
args = args.split()
@@ -2518,102 +3002,247 @@ def part_command_cb(data, current_buffer, args):
e.weechat_controller.unregister_buffer(current_buffer, update_remote=True, close_buffer=True)
return w.WEECHAT_RC_OK_EAT
-@slack_buffer_or_ignore
-def topic_command_cb(data, current_buffer, args):
- n = len(args.split())
- if n < 2:
- channel = channels.find(current_buffer)
- if channel:
- w.prnt(current_buffer, 'Topic for {} is "{}"'.format(channel.name, channel.topic))
- return w.WEECHAT_RC_OK_EAT
- elif command_topic(data, current_buffer, args.split(None, 1)[1]):
- return w.WEECHAT_RC_OK_EAT
- else:
- return w.WEECHAT_RC_ERROR
-@slack_buffer_required
-def command_topic(data, current_buffer, args):
+def parse_topic_command(command):
+ args = command.split()[1:]
+ channel_name = None
+ topic = None
+
+ if args:
+ if args[0].startswith('#'):
+ channel_name = args[0][1:]
+ topic = args[1:]
+ else:
+ topic = args
+
+ if topic == []:
+ topic = None
+ if topic:
+ topic = ' '.join(topic)
+ if topic == '-delete':
+ topic = ''
+
+ return channel_name, topic
+
+
+@slack_buffer_or_ignore
+@utf8_decode
+def topic_command_cb(data, current_buffer, command):
"""
Change the topic of a channel
- /slack topic [<channel>] [<topic>|-delete]
+ /topic [<channel>] [<topic>|-delete]
"""
- e = EVENTROUTER
- team = e.weechat_controller.buffers[current_buffer].team
- #server = servers.find(current_domain_name())
- args = args.split(' ')
- if len(args) > 2 and args[1].startswith('#'):
- cmap = team.get_channel_map()
- channel_name = args[1][1:]
- channel = team.channels[cmap[channel_name]]
- topic = " ".join(args[2:])
+
+ channel_name, topic = parse_topic_command(command)
+
+ team = EVENTROUTER.weechat_controller.buffers[current_buffer].team
+ if channel_name:
+ channel = team.channels.get(team.get_channel_map().get(channel_name))
else:
- channel = e.weechat_controller.buffers[current_buffer]
- topic = " ".join(args[1:])
+ channel = EVENTROUTER.weechat_controller.buffers[current_buffer]
- if channel:
- if topic == "-delete":
- topic = ''
+ if not channel:
+ w.prnt(team.channel_buffer, "#{}: No such channel".format(channel_name))
+ return w.WEECHAT_RC_OK_EAT
+
+ if topic is None:
+ w.prnt(channel.channel_buffer, 'Topic for {} is "{}"'.format(channel.name, channel.topic))
+ else:
s = SlackRequest(team.token, "channels.setTopic", {"channel": channel.identifier, "topic": topic}, team_hash=team.team_hash)
EVENTROUTER.receive(s)
+ return w.WEECHAT_RC_OK_EAT
+
+@slack_buffer_or_ignore
+@utf8_decode
+def whois_command_cb(data, current_buffer, command):
+ """
+ Get real name of user
+ /whois <display_name>
+ """
+
+ args = command.split()
+ if len(args) < 2:
+ w.prnt(current_buffer, "Not enough arguments")
return w.WEECHAT_RC_OK_EAT
+ user = args[1]
+ if (user.startswith('@')):
+ user = user[1:]
+ team = EVENTROUTER.weechat_controller.buffers[current_buffer].team
+ u = team.users.get(team.get_username_map().get(user))
+ if u:
+ team.buffer_prnt("[{}]: {}".format(user, u.real_name))
+ if u.profile.get("status_text"):
+ team.buffer_prnt("[{}]: {} {}".format(user, u.profile.status_emoji, u.profile.status_text))
+ team.buffer_prnt("[{}]: Real name: {}".format(user, u.profile.get('real_name_normalized', '')))
+ team.buffer_prnt("[{}]: Title: {}".format(user, u.profile.get('title', '')))
+ team.buffer_prnt("[{}]: Email: {}".format(user, u.profile.get('email', '')))
+ team.buffer_prnt("[{}]: Phone: {}".format(user, u.profile.get('phone', '')))
else:
- return w.WEECHAT_RC_ERROR_EAT
+ team.buffer_prnt("[{}]: No such user".format(user))
+ return w.WEECHAT_RC_OK_EAT
@slack_buffer_or_ignore
+@utf8_decode
def me_command_cb(data, current_buffer, args):
message = "_{}_".format(args.split(' ', 1)[1])
buffer_input_callback("EVENTROUTER", current_buffer, message)
return w.WEECHAT_RC_OK_EAT
+
+def command_register(data, current_buffer, args):
+ CLIENT_ID = "2468770254.51917335286"
+ CLIENT_SECRET = "dcb7fe380a000cba0cca3169a5fe8d70" # Not really a secret.
+ if args == 'register':
+ message = textwrap.dedent("""
+ #### Retrieving a Slack token via OAUTH ####
+
+ 1) Paste this into a browser: https://slack.com/oauth/authorize?client_id=2468770254.51917335286&scope=client
+ 2) Select the team you wish to access from wee-slack in your browser.
+ 3) Click "Authorize" in the browser **IMPORTANT: the redirect will fail, this is expected**
+ 4) Copy the "code" portion of the URL to your clipboard
+ 5) Return to weechat and run `/slack register [code]`
+ """)
+ w.prnt("", message)
+ return
+
+ try:
+ _, oauth_code = args.split()
+ except ValueError:
+ w.prnt("",
+ "ERROR: wrong number of arguments given for register command")
+ return
+
+ uri = (
+ "https://slack.com/api/oauth.access?"
+ "client_id={}&client_secret={}&code={}"
+ ).format(CLIENT_ID, CLIENT_SECRET, oauth_code)
+ ret = urllib.urlopen(uri).read()
+ d = json.loads(ret)
+ if not d["ok"]:
+ w.prnt("",
+ "ERROR: Couldn't get Slack OAuth token: {}".format(d['error']))
+ return
+
+ if config.is_default('slack_api_token'):
+ w.config_set_plugin('slack_api_token', d['access_token'])
+ else:
+ # Add new token to existing set, joined by comma.
+ tok = config.get_string('slack_api_token')
+ w.config_set_plugin('slack_api_token',
+ ','.join([tok, d['access_token']]))
+
+ w.prnt("", "Success! Added team \"%s\"" % (d['team_name'],))
+ w.prnt("", "Please reload wee-slack with: /script reload slack")
+
+
@slack_buffer_or_ignore
+@utf8_decode
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, "talk " + 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
+
+@slack_buffer_required
+@utf8_decode
+def command_channels(data, current_buffer, args):
+ e = EVENTROUTER
+ team = e.weechat_controller.buffers[current_buffer].team
+
+ team.buffer_prnt("Channels:")
+ for channel in team.get_channel_map():
+ team.buffer_prnt(" {}".format(channel))
+ return w.WEECHAT_RC_OK_EAT
+
+
+@slack_buffer_required
+@utf8_decode
+def command_users(data, current_buffer, args):
+ e = EVENTROUTER
+ team = e.weechat_controller.buffers[current_buffer].team
+
+ team.buffer_prnt("Users:")
+ for user in team.users.values():
+ team.buffer_prnt(" {:<25}({})".format(user.name, user.presence))
+ return w.WEECHAT_RC_OK_EAT
+
+
@slack_buffer_or_ignore
+@utf8_decode
def command_talk(data, current_buffer, args):
"""
- Open a chat with the specified user
- /slack talk [user]
+ Open a chat with the specified user(s)
+ /slack talk <user>[,<user2>[,<user3>...]]
"""
+
e = EVENTROUTER
team = e.weechat_controller.buffers[current_buffer].team
channel_name = args.split(' ')[1]
- c = team.get_channel_map()
- if channel_name not in c:
- u = team.get_username_map()
- if channel_name in u:
- s = SlackRequest(team.token, "im.open", {"user": u[channel_name]}, team_hash=team.team_hash)
- EVENTROUTER.receive(s)
- dbg("found user")
- #refresh channel map here
- c = team.get_channel_map()
if channel_name.startswith('#'):
channel_name = channel_name[1:]
- if channel_name in c:
- chan = team.channels[c[channel_name]]
+
+ # Try finding the channel by name
+ chan = team.channels.get(team.get_channel_map().get(channel_name))
+
+ # If the channel doesn't exist, try finding a DM or MPDM instead
+ if not chan:
+ # Get the IDs of the users
+ u = team.get_username_map()
+ users = set()
+ for user in channel_name.split(','):
+ if user.startswith('@'):
+ user = user[1:]
+ if user in u:
+ users.add(u[user])
+
+ if users:
+ if len(users) > 1:
+ channel_type = 'mpim'
+ # Add the current user since MPDMs include them as a member
+ users.add(team.myidentifier)
+ else:
+ channel_type = 'im'
+
+ # Try finding the channel by type and members
+ for channel in team.channels.itervalues():
+ if (channel.type == channel_type and
+ channel.get_members() == users):
+ chan = channel
+ break
+
+ # If the DM or MPDM doesn't exist, create it
+ if not chan:
+ s = SlackRequest(team.token, SLACK_API_TRANSLATOR[channel_type]['join'], {'users': ','.join(users)}, team_hash=team.team_hash)
+ EVENTROUTER.receive(s)
+
+ if chan:
chan.open()
if config.switch_buffer_on_join:
w.buffer_set(chan.channel_buffer, "display", "1")
return w.WEECHAT_RC_OK_EAT
return w.WEECHAT_RC_OK_EAT
+
def command_showmuted(data, current_buffer, args):
current = w.current_buffer()
w.prnt(EVENTROUTER.weechat_controller.buffers[current].team.channel_buffer, str(EVENTROUTER.weechat_controller.buffers[current].team.muted_channels))
+
+@utf8_decode
def thread_command_callback(data, current_buffer, args):
current = w.current_buffer()
channel = EVENTROUTER.weechat_controller.buffers.get(current)
@@ -2628,19 +3257,22 @@ def thread_command_callback(data, current_buffer, args):
tc = SlackThreadChannel(EVENTROUTER, pm)
pm.thread_channel = tc
tc.open()
- #tc.create_buffer()
+ # tc.create_buffer()
+ if config.switch_buffer_on_join:
+ w.buffer_set(tc.channel_buffer, "display", "1")
return w.WEECHAT_RC_OK_EAT
elif args[0] == '/reply':
count = int(args[1])
msg = " ".join(args[2:])
- mkeys = channel.sorted_message_keys()
- mkeys.reverse()
- parent_id = str(mkeys[count - 1])
+ mkeys = channel.main_message_keys_reversed()
+ parent_id = str(next(islice(mkeys, count - 1, None)))
channel.send_message(msg, request_dict_ext={"thread_ts": parent_id})
return w.WEECHAT_RC_OK_EAT
w.prnt(current, "Invalid thread command.")
return w.WEECHAT_RC_OK_EAT
+
+@utf8_decode
def rehistory_command_callback(data, current_buffer, args):
current = w.current_buffer()
channel = EVENTROUTER.weechat_controller.buffers.get(current)
@@ -2649,7 +3281,9 @@ def rehistory_command_callback(data, current_buffer, args):
channel.get_history()
return w.WEECHAT_RC_OK_EAT
+
@slack_buffer_required
+@utf8_decode
def hide_command_callback(data, current_buffer, args):
c = EVENTROUTER.weechat_controller.buffers.get(current_buffer, None)
if c:
@@ -2658,6 +3292,8 @@ def hide_command_callback(data, current_buffer, args):
w.buffer_set(c.channel_buffer, "hidden", "1")
return w.WEECHAT_RC_OK_EAT
+
+@utf8_decode
def slack_command_cb(data, current_buffer, args):
a = args.split(' ', 1)
if len(a) > 1:
@@ -2671,6 +3307,7 @@ def slack_command_cb(data, current_buffer, args):
w.prnt("", "Command not found: " + function_name)
return w.WEECHAT_RC_OK
+
@slack_buffer_required
def command_distracting(data, current_buffer, args):
channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer, None)
@@ -2682,9 +3319,11 @@ def command_distracting(data, current_buffer, args):
config.distracting_channels.pop(config.distracting_channels.index(fullname))
save_distracting_channels()
+
def save_distracting_channels():
w.config_set_plugin('distracting_channels', ','.join(config.distracting_channels))
+
@slack_buffer_required
def command_slash(data, current_buffer, args):
"""
@@ -2696,8 +3335,8 @@ def command_slash(data, current_buffer, args):
if channel:
team = channel.team
- if args is None:
- server.buffer_prnt("Usage: /slack slash /someslashcommand [arguments...].")
+ if args == 'slash':
+ w.prnt("", "Usage: /slack slash /someslashcommand [arguments...].")
return
split_args = args.split(None, 2)
@@ -2707,6 +3346,7 @@ def command_slash(data, current_buffer, args):
s = SlackRequest(team.token, "chat.command", {"command": command, "text": text, 'channel': channel.identifier}, team_hash=team.team_hash, channel_identifier=channel.identifier)
EVENTROUTER.receive(s)
+
@slack_buffer_required
def command_mute(data, current_buffer, args):
current = w.current_buffer()
@@ -2719,9 +3359,10 @@ def command_mute(data, current_buffer, args):
s = SlackRequest(team.token, "users.prefs.set", {"name": "muted_channels", "value": ",".join(team.muted_channels)}, team_hash=team.team_hash, channel_identifier=channel_id)
EVENTROUTER.receive(s)
+
@slack_buffer_required
def command_openweb(data, current_buffer, args):
- #if done from server buffer, open slack for reals
+ # if done from server buffer, open slack for reals
channel = EVENTROUTER.weechat_controller.buffers[current_buffer]
if isinstance(channel, SlackTeam):
url = "https://{}".format(channel.team.domain)
@@ -2730,22 +3371,24 @@ def command_openweb(data, current_buffer, args):
url = "https://{}/archives/{}/p{}000000".format(channel.team.domain, channel.slack_name, now.majorstr())
w.prnt_date_tags(channel.team.channel_buffer, SlackTS().major, "openweb,logger_backlog_end,notify_none", url)
+
def command_nodistractions(data, current_buffer, args):
global hide_distractions
hide_distractions = not hide_distractions
if config.distracting_channels != ['']:
for channel in config.distracting_channels:
dbg('hiding channel {}'.format(channel))
- #try:
+ # try:
for c in EVENTROUTER.weechat_controller.buffers.itervalues():
if c == channel:
dbg('found channel {} to hide'.format(channel))
w.buffer_set(c.channel_buffer, "hidden", str(int(hide_distractions)))
- #except:
+ # except:
# dbg("Can't hide channel {} .. removing..".format(channel), main_buffer=True)
# config.distracting_channels.pop(config.distracting_channels.index(channel))
# save_distracting_channels()
+
@slack_buffer_required
def command_upload(data, current_buffer, args):
channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer)
@@ -2759,8 +3402,10 @@ def command_upload(data, current_buffer, args):
command = 'curl -F file=@{} -F channels={} -F token={} {}'.format(file_path, channel.identifier, team.token, url)
w.hook_process(command, config.slack_timeout, '', '')
+
+@utf8_decode
def away_command_cb(data, current_buffer, args):
- #TODO: reimplement all.. maybe
+ # TODO: reimplement all.. maybe
(all, message) = re.match("^/away(?:\s+(-all))?(?:\s+(.+))?", args).groups()
if message is None:
command_back(data, current_buffer, args)
@@ -2768,6 +3413,7 @@ def away_command_cb(data, current_buffer, args):
command_away(data, current_buffer, args)
return w.WEECHAT_RC_OK
+
@slack_buffer_required
def command_away(data, current_buffer, args):
"""
@@ -2775,21 +3421,44 @@ def command_away(data, current_buffer, args):
/slack away
"""
team = EVENTROUTER.weechat_controller.buffers[current_buffer].team
- s = SlackRequest(team.token, "presence.set", {"presence": "away"}, team_hash=team.team_hash)
+ s = SlackRequest(team.token, "users.setPresence", {"presence": "away"}, team_hash=team.team_hash)
EVENTROUTER.receive(s)
@slack_buffer_required
+def command_status(data, current_buffer, args):
+ """
+ Lets you set your Slack Status (not to be confused with away/here)
+ /slack status [emoji] [status_message]
+ """
+ e = EVENTROUTER
+ channel = e.weechat_controller.buffers.get(current_buffer, None)
+ if channel:
+ team = channel.team
+
+ split_args = args.split(None, 2)
+ emoji = split_args[1] if len(split_args) > 1 else ""
+ text = split_args[2] if len(split_args) > 2 else ""
+
+ profile = {"status_text":text,"status_emoji":emoji}
+
+ s = SlackRequest(team.token, "users.profile.set", {"profile": profile}, team_hash=team.team_hash)
+ EVENTROUTER.receive(s)
+
+
+@slack_buffer_required
def command_back(data, current_buffer, args):
"""
Sets your status as 'back'
/slack back
"""
team = EVENTROUTER.weechat_controller.buffers[current_buffer].team
- s = SlackRequest(team.token, "presence.set", {"presence": "active"}, team_hash=team.team_hash)
+ s = SlackRequest(team.token, "users.setPresence", {"presence": "auto"}, team_hash=team.team_hash)
EVENTROUTER.receive(s)
+
@slack_buffer_required
+@utf8_decode
def label_command_cb(data, current_buffer, args):
channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer)
if channel and channel.type == 'thread':
@@ -2798,12 +3467,29 @@ def label_command_cb(data, current_buffer, args):
channel.label = new_name
w.buffer_set(channel.channel_buffer, "short_name", new_name)
+
+@utf8_decode
+def set_unread_cb(data, current_buffer, command):
+ for channel in EVENTROUTER.weechat_controller.buffers.values():
+ channel.mark_read()
+ return w.WEECHAT_RC_OK
+
+
+@slack_buffer_or_ignore
+@utf8_decode
+def set_unread_current_buffer_cb(data, current_buffer, command):
+ channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer)
+ channel.mark_read()
+ return w.WEECHAT_RC_OK
+
+
def command_p(data, current_buffer, args):
args = args.split(' ', 1)[1]
w.prnt("", "{}".format(eval(args)))
###### NEW EXCEPTIONS
+
class ProcessNotImplemented(Exception):
"""
Raised when we try to call process_(something), but
@@ -2812,6 +3498,7 @@ class ProcessNotImplemented(Exception):
def __init__(self, function_name):
super(ProcessNotImplemented, self).__init__(function_name)
+
class InvalidType(Exception):
"""
Raised when we do type checking to ensure objects of the wrong
@@ -2822,6 +3509,7 @@ class InvalidType(Exception):
###### New but probably old and need to migrate
+
def closed_slack_debug_buffer_cb(data, buffer):
global slack_debug
slack_debug = None
@@ -2837,18 +3525,16 @@ def create_slack_debug_buffer():
slack_debug = w.buffer_new("slack-debug", "", "", "closed_slack_debug_buffer_cb", "")
w.buffer_set(slack_debug, "notify", "0")
+
def load_emoji():
try:
- global EMOJI
DIR = w.info_get("weechat_dir", "")
- #no idea why this does't work w/o checking the type?!
- dbg(type(DIR), 0)
- ef = open('{}/weemoji.json'.format(DIR), 'r')
- EMOJI = json.loads(ef.read())
- ef.close()
- except:
- dbg("Unexpected error: {}".format(sys.exc_info()), 5)
- return w.WEECHAT_RC_OK
+ with open('{}/weemoji.json'.format(DIR), 'r') as ef:
+ return json.loads(ef.read())["emoji"]
+ except Exception as e:
+ dbg("Couldn't load emoji list: {}".format(e), 5)
+ return []
+
def setup_hooks():
cmds = {k[8:]: v for k, v in globals().items() if k.startswith("command_")}
@@ -2863,8 +3549,9 @@ def setup_hooks():
w.hook_signal('buffer_closing', "buffer_closing_callback", "EVENTROUTER")
w.hook_signal('buffer_switch', "buffer_switch_callback", "EVENTROUTER")
w.hook_signal('window_switch', "buffer_switch_callback", "EVENTROUTER")
- w.hook_signal('quit', "quit_notification_cb", "")
- w.hook_signal('input_text_changed', "typing_notification_cb", "")
+ w.hook_signal('quit', "quit_notification_callback", "")
+ if config.send_typing_notice:
+ w.hook_signal('input_text_changed', "typing_notification_cb", "")
w.hook_command(
# Command name and description
@@ -2879,14 +3566,14 @@ def setup_hooks():
'|'.join(cmds.keys()),
# Function name
'slack_command_cb', '')
- #w.hook_command('me', '', 'stuff', 'stuff2', '', 'me_command_cb', '')
+ # w.hook_command('me', '', 'stuff', 'stuff2', '', 'me_command_cb', '')
w.hook_command_run('/me', 'me_command_cb', '')
w.hook_command_run('/query', 'command_talk', '')
w.hook_command_run('/join', 'command_talk', '')
w.hook_command_run('/part', 'part_command_cb', '')
w.hook_command_run('/leave', 'part_command_cb', '')
- w.hook_command_run('/topic', 'command_topic', '')
+ w.hook_command_run('/topic', 'topic_command_cb', '')
w.hook_command_run('/thread', 'thread_command_callback', '')
w.hook_command_run('/reply', 'thread_command_callback', '')
w.hook_command_run('/rehistory', 'rehistory_command_callback', '')
@@ -2894,17 +3581,18 @@ def setup_hooks():
w.hook_command_run('/msg', 'msg_command_cb', '')
w.hook_command_run('/label', 'label_command_cb', '')
w.hook_command_run("/input complete_next", "complete_next_cb", "")
+ w.hook_command_run("/input set_unread", "set_unread_cb", "")
+ w.hook_command_run("/input set_unread_current_buffer", "set_unread_current_buffer_cb", "")
w.hook_command_run('/away', 'away_command_cb', '')
+ w.hook_command_run('/whois', 'whois_command_cb', '')
w.hook_completion("nicks", "complete @-nicks for slack", "nick_completion_cb", "")
w.hook_completion("emoji", "complete :emoji: for slack", "emoji_completion_cb", "")
# Hooks to fix/implement
- #w.hook_timer(1000 * 60 * 5, 0, 0, "cache_write_cb", "")
- #w.hook_signal('buffer_opened', "buffer_opened_cb", "")
- #w.hook_signal('window_scrolled', "scrolled_cb", "")
- #w.hook_timer(3000, 0, 0, "slack_connection_persistence_cb", "")
-
+ # w.hook_signal('buffer_opened', "buffer_opened_cb", "")
+ # w.hook_signal('window_scrolled', "scrolled_cb", "")
+ # w.hook_timer(3000, 0, 0, "slack_connection_persistence_cb", "")
##### END NEW
@@ -2913,59 +3601,147 @@ def dbg(message, level=0, main_buffer=False, fout=False):
"""
send debug output to the slack-debug buffer and optionally write to a file.
"""
- #TODO: do this smarter
- #return
+ # TODO: do this smarter
+ # return
if level >= config.debug_level:
global debug_string
message = "DEBUG: {}".format(message)
- # message = message.encode('utf-8', 'replace')
if fout:
file('/tmp/debug.log', 'a+').writelines(message + '\n')
if main_buffer:
- #w.prnt("", "---------")
+ # w.prnt("", "---------")
w.prnt("", "slack: " + message)
else:
if slack_debug and (not debug_string or debug_string in message):
- #w.prnt(slack_debug, "---------")
+ # w.prnt(slack_debug, "---------")
w.prnt(slack_debug, message)
-
###### 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_messages': 'false',
- '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',
- 'cache_messages': 'true',
- '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.'),
+ 'group_name_prefix': Setting(
+ default='&',
+ desc='The prefix of buffer names for groups (private channels).'),
+ 'map_underline_to': Setting(
+ default='_',
+ desc='When sending underlined text to slack, use this formatting'
+ ' 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.'),
+ 'send_typing_notice': Setting(
+ default='true',
+ desc='Alert Slack users when you are typing a message in the input bar '
+ '(Requires reload)'),
+ 'server_aliases': Setting(
+ default='',
+ desc='A comma separated list of `subdomain:alias` pairs. The alias'
+ ' 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.'),
+ 'unfurl_auto_link_display': Setting(
+ default='both',
+ desc='When displaying ("unfurling") links to channels/users/etc,'
+ ' determine what is displayed when the text matches the url'
+ ' without the protocol. This happens when Slack automatically'
+ ' creates links, e.g. from words separated by dots or email'
+ ' addresses. Set it to "text" to only display the text written by'
+ ' the user, "url" to only display the url or "both" (the default)'
+ ' to display both.'),
+ 'unhide_buffers_with_activity': Setting(
+ default='false',
+ desc='When activity occurs on a buffer, unhide it even if it was'
+ ' 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)
@@ -2997,6 +3773,25 @@ 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))
+
+ def is_default(self, key):
+ default = self.default_settings.get(key).default
+ return w.config_get_plugin(key) == default
+
+ get_debug_level = get_int
+ get_group_name_prefix = get_string
+ get_map_underline_to = get_string
+ get_render_bold_as = get_string
+ get_render_italic_as = get_string
+ get_slack_timeout = get_int
+ get_thread_suffix_color = get_string
+ get_unfurl_auto_link_display = get_string
+
def get_distracting_channels(self, key):
return [x.strip() for x in w.config_get_plugin(key).split(',')]
@@ -3012,15 +3807,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
@@ -3042,6 +3828,7 @@ def setup_trace():
now = time.time()
f = open('{}/{}-trace.json'.format(RECORD_DIR, now), 'w')
+
def trace_calls(frame, event, arg):
global f
if event != 'call':
@@ -3062,25 +3849,30 @@ def trace_calls(frame, event, arg):
f.flush()
return
+def initiate_connection(token, retries=3):
+ return SlackRequest(token,
+ 'rtm.start',
+ {"batch_presence_aware": 1 },
+ retries=retries)
# Main
if __name__ == "__main__":
+ w = WeechatWrapper(weechat)
+
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:
global EVENTROUTER
EVENTROUTER = EventRouter()
- #setup_trace()
+ # setup_trace()
- #WEECHAT_HOME = w.info_get("weechat_dir", "")
- #CACHE_NAME = "slack.cache"
- #STOP_TALKING_TO_SLACK = False
+ # WEECHAT_HOME = w.info_get("weechat_dir", "")
# Global var section
slack_debug = None
@@ -3088,29 +3880,26 @@ if __name__ == "__main__":
config_changed_cb = config.config_changed
typing_timer = time.time()
- #domain = None
- #previous_buffer = None
- #slack_buffer = None
+ # domain = None
+ # previous_buffer = None
+ # slack_buffer = None
- #never_away = False
+ # never_away = False
hide_distractions = False
- #hotlist = w.infolist_get("hotlist", "", "")
- #main_weechat_buffer = w.info_get("irc_buffer", "{}.{}".format(domain, "DOESNOTEXIST!@#$"))
-
- #message_cache = collections.defaultdict(list)
- #if config.cache_messages:
- # cache_load()
+ # hotlist = w.infolist_get("hotlist", "", "")
+ # main_weechat_buffer = w.info_get("irc_buffer", "{}.{}".format(domain, "DOESNOTEXIST!@#$"))
w.hook_config("plugins.var.python." + SCRIPT_NAME + ".*", "config_changed_cb", "")
+ w.hook_modifier("input_text_for_buffer", "input_text_for_buffer_cb", "")
- load_emoji()
+ EMOJI.extend(load_emoji())
setup_hooks()
# attach to the weechat hooks we need
tokens = config.slack_api_token.split(',')
for t in tokens:
- s = SlackRequest(t, 'rtm.start', {})
+ s = initiate_connection(t)
EVENTROUTER.receive(s)
if config.record_events:
EVENTROUTER.record()
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"
+ ]
+}