aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRyan Huber <rhuber@gmail.com>2017-04-12 23:36:05 -0700
committerGitHub <noreply@github.com>2017-04-12 23:36:05 -0700
commit633d3ad78fe55709b5f5894a6e9668e0d606e5ec (patch)
treecbac1595e9998eff1f002d07372f272cb76c60aa
parent01beb42fec5c6dbfc0d8c53ce1de88b48b48f6db (diff)
parent3ddddcd6d84c6355c142c4f9acea190c4551e65e (diff)
downloadwee-slack-633d3ad78fe55709b5f5894a6e9668e0d606e5ec.tar.gz
Merge pull request #334 from wee-slack/wee-ng-merge
Wee ng merge
-rw-r--r--.travis.yml11
-rw-r--r--README.md30
-rw-r--r--_pytest/conftest.py121
-rw-r--r--_pytest/data/http/rtm.start.json861
-rw-r--r--_pytest/data/websocket/1485975367.79-reconnect_url.json1
-rw-r--r--_pytest/data/websocket/1485975408.19-user_typing.json1
-rw-r--r--_pytest/data/websocket/1485975412.74-user_typing.json1
-rw-r--r--_pytest/data/websocket/1485975420.36-user_typing.json1
-rw-r--r--_pytest/data/websocket/1485975421.33-message.json1
-rw-r--r--_pytest/data/websocket/1485975421.42-desktop_notification.json1
-rw-r--r--_pytest/data/websocket/1485975426.55-reaction_added.json1
-rw-r--r--_pytest/data/websocket/1485975428.76-reaction_removed.json1
-rw-r--r--_pytest/data/websocket/1485975458.35-channel_created.json1
-rw-r--r--_pytest/data/websocket/1485975458.87-channel_joined.json1
-rw-r--r--_pytest/data/websocket/1485975458.87-message.json1
-rw-r--r--_pytest/data/websocket/1485975462.62-update_thread_state.json1
-rw-r--r--_pytest/data/websocket/1485975462.62-user_typing.json1
-rw-r--r--_pytest/data/websocket/1485975476.62-channel_deleted.json1
-rw-r--r--_pytest/data/websocket/1485975476.86-group_join.json1
-rw-r--r--_pytest/data/websocket/1485975476.86-group_joined.json1
-rw-r--r--_pytest/data/websocket/1485975487.69-group_history_changed.json1
-rw-r--r--_pytest/data/websocket/1485975487.69-group_join.json1
-rw-r--r--_pytest/data/websocket/1485975493.19-group_joined.json1
-rw-r--r--_pytest/data/websocket/1485975493.23-message.json1
-rw-r--r--_pytest/data/websocket/1485975547.75-message.json1
-rw-r--r--_pytest/data/websocket/1485975547.75-update_thread_state.json1
-rw-r--r--_pytest/data/websocket/1485975547.83-reconnect_url.json1
-rw-r--r--_pytest/data/websocket/1485975606.59-team_join.json1
-rw-r--r--_pytest/data/websocket/1485975606.67-message.json1
-rw-r--r--_pytest/data/websocket/1485975606.75-im_created.json1
-rw-r--r--_pytest/data/websocket/1485975606.75-im_open.json1
-rw-r--r--_pytest/data/websocket/1485975611.29-presence_change.json1
-rw-r--r--_pytest/data/websocket/1485975640.5-user_change.json1
-rw-r--r--_pytest/data/websocket/1485975669.14-user_change.json1
-rw-r--r--_pytest/data/websocket/1485975675.81-presence_change.json1
-rw-r--r--_pytest/data/websocket/1485975690.59-mpim_joined.json1
-rw-r--r--_pytest/data/websocket/1485975690.67-group_joined.json1
-rw-r--r--_pytest/data/websocket/1485975690.67-update_thread_state.json1
-rw-r--r--_pytest/data/websocket/1485975697.58-user_typing.json1
-rw-r--r--_pytest/data/websocket/1485975698.45-message.json1
-rw-r--r--_pytest/data/websocket/1485975701.72-reaction_added.json1
-rw-r--r--_pytest/data/websocket/1485975703.22-reaction_removed.json1
-rw-r--r--_pytest/data/websocket/1485975715.79-message.json1
-rw-r--r--_pytest/data/websocket/1485975715.87-file_shared.json1
-rw-r--r--_pytest/data/websocket/1485975723.85-message.json1
-rw-r--r--_pytest/data/websocket/1485975727.84-reconnect_url.json1
-rw-r--r--_pytest/data/websocket/1485975735.91-group_archive.json1
-rw-r--r--_pytest/data/websocket/1485975735.99-message.json1
-rw-r--r--_pytest/data/websocket/1485975736.08-message.json1
-rw-r--r--_pytest/data/websocket/1485975738.1-desktop_notification.json1
-rw-r--r--_pytest/data/websocket/1485975747.17-channel_created.json1
-rw-r--r--_pytest/data/websocket/1485975757.63-channel_rename.json1
-rw-r--r--_pytest/data/websocket/1485975771.6-presence_change.json1
-rw-r--r--_pytest/data/websocket/1485975774.62-presence_change.json1
-rw-r--r--_pytest/data/websocket/1485975822.17-user_typing.json1
-rw-r--r--_pytest/data/websocket/1485975824.48-message.json1
-rw-r--r--_pytest/data/websocket/1485975836.23-message.json1
-rw-r--r--_pytest/data/websocket/1485975836.31-message.json1
-rw-r--r--_pytest/data/websocket/1485975842.1-message.json1
-rw-r--r--_pytest/data/websocket/1485975842.18-message.json1
-rw-r--r--_pytest/data/websocket/1485975850.32-mpim_open.json1
-rw-r--r--_pytest/data/websocket/1485975850.45-group_open.json1
-rw-r--r--_pytest/data/websocket/1485975858.56-reaction_added.json1
-rw-r--r--_pytest/data/websocket/1485975890.7-bot_added.json1
-rw-r--r--_pytest/data/websocket/1485975890.78-message.json1
-rw-r--r--_pytest/data/websocket/1485975896.16-message.json1
-rw-r--r--_pytest/data/websocket/1485975907.84-reconnect_url.json1
-rw-r--r--_pytest/data/websocket/1485975926.59-user_change.json1
-rw-r--r--_pytest/data/websocket/1485975930.81-user_change.json1
-rw-r--r--_pytest/data/websocket/1485975956.38-file_public.json1
-rw-r--r--_pytest/data/websocket/1485975956.47-file_shared.json1
-rw-r--r--_pytest/data/websocket/1485975956.47-message.json1
-rw-r--r--_pytest/data/websocket/1485975978.79-message.json1
-rw-r--r--_pytest/data/websocket/1485975978.91-file_public.json1
-rw-r--r--_pytest/data/websocket/1485975978.91-file_shared.json1
-rw-r--r--_pytest/data/websocket/1485975995.98-file_change.json1
-rw-r--r--_pytest/data/websocket/1485975998.13-file_change.json1
-rw-r--r--_pytest/data/websocket/1485976002.57-file_deleted.json1
-rw-r--r--_pytest/data/websocket/1485976002.66-message.json1
-rw-r--r--_pytest/data/websocket/1485976024.46-user_typing.json1
-rw-r--r--_pytest/data/websocket/1485976028.31-user_typing.json1
-rw-r--r--_pytest/data/websocket/1485976039.32-reaction_added.json1
-rw-r--r--_pytest/data/websocket/1485976040.8-reaction_removed.json1
-rw-r--r--_pytest/data/websocket/1485976087.84-reconnect_url.json1
-rw-r--r--_pytest/data/websocket/1485976107.57-channel_created.json1
-rw-r--r--_pytest/data/websocket/1485976114.72-user_typing.json1
-rw-r--r--_pytest/data/websocket/1485976117.99-user_typing.json1
-rw-r--r--_pytest/data/websocket/1485976124.9-user_typing.json1
-rw-r--r--_pytest/data/websocket/1485976124.98-message.json1
-rw-r--r--_pytest/data/websocket/1485976125.06-desktop_notification.json1
-rw-r--r--_pytest/data/websocket/1485976125.95-user_typing.json1
-rw-r--r--_pytest/data/websocket/1485976129.49-user_typing.json1
-rw-r--r--_pytest/data/websocket/1485976133.0-user_typing.json1
-rw-r--r--_pytest/data/websocket/1485976137.51-user_typing.json1
-rw-r--r--_pytest/data/websocket/1485976137.95-message.json1
-rw-r--r--_pytest/data/websocket/1485976138.73-user_typing.json1
-rw-r--r--_pytest/data/websocket/1485976141.79-user_typing.json1
-rw-r--r--_pytest/data/websocket/1485976145.8-user_typing.json1
-rw-r--r--_pytest/data/websocket/1485976149.89-user_typing.json1
-rw-r--r--_pytest/data/websocket/1485976151.6-message.json1
-rw-r--r--_pytest/data/websocket/1485976157.18-message.json1
-rw-r--r--_pytest/data/websocket/1485976157.8-user_typing.json1
-rw-r--r--_pytest/data/websocket/1485976161.29-user_typing.json1
-rw-r--r--_pytest/data/websocket/1485976161.75-message.json1
-rw-r--r--_pytest/data/websocket/1485976182.59-channel_archive.json1
-rw-r--r--_pytest/data/websocket/1485976186.62-channel_unarchive.json1
-rw-r--r--_pytest/data/websocket/1485976236.58-message.json1
-rw-r--r--_pytest/data/websocket/1485976236.67-file_comment_added.json1
-rw-r--r--_pytest/data/websocket/1485976248.57-message.json1
-rw-r--r--_pytest/data/websocket/1485976248.65-file_comment_edited.json1
-rw-r--r--_pytest/data/websocket/1485976267.81-reconnect_url.json1
-rw-r--r--_pytest/data/websocket/1486004888.32-presence_change.json1
-rw-r--r--_pytest/data/websocket/1486004888.41-reconnect_url.json1
-rw-r--r--_pytest/data/websocket/1486004935.63-presence_change.json1
-rw-r--r--_pytest/data/websocket/1486004950.43-message.json1
-rw-r--r--_pytest/data/websocket/1486004992.49-user_typing.json1
-rw-r--r--_pytest/data/websocket/1486004995.69-user_typing.json1
-rw-r--r--_pytest/data/websocket/1486004999.55-user_typing.json1
-rw-r--r--_pytest/data/websocket/1486004999.95-message.json1
-rw-r--r--_pytest/data/websocket/1486005004.03-user_typing.json1
-rw-r--r--_pytest/data/websocket/1486005007.16-user_typing.json1
-rw-r--r--_pytest/data/websocket/1486005008.45-reconnect_url.json1
-rw-r--r--_pytest/data/websocket/1486005009.0-message.json1
-rw-r--r--_pytest/data/websocket/1486005096.38-accounts_changed.json1
-rw-r--r--_pytest/data/websocket/1486005099.81-presence_change.json1
-rw-r--r--_pytest/data/websocket/1486005104.63-channel_marked.json1
-rw-r--r--_pytest/data/websocket/1486005109.58-im_marked.json1
-rw-r--r--_pytest/data/websocket/1486005119.69-im_marked.json1
-rw-r--r--_pytest/data/websocket/1486005124.51-mpim_marked.json1
-rw-r--r--_pytest/data/websocket/1486005124.59-group_marked.json1
-rw-r--r--_pytest/data/websocket/1486005124.67-group_marked.json1
-rw-r--r--_pytest/data/websocket/1486005188.48-reconnect_url.json1
-rw-r--r--_pytest/test_eventrouter.py86
-rw-r--r--_pytest/test_everything.py50
-rw-r--r--_pytest/test_linkifytext.py6
-rw-r--r--_pytest/test_presencechange.py31
-rw-r--r--_pytest/test_process_message.py68
-rw-r--r--_pytest/test_processreply.py33
-rw-r--r--_pytest/test_processteamjoin.py54
-rw-r--r--_pytest/test_sendmessage.py21
-rw-r--r--_pytest/test_slackchannel.py33
-rw-r--r--_pytest/test_slackdmchannel.py20
-rw-r--r--_pytest/test_slackgroupchannel.py20
-rw-r--r--_pytest/test_slackmpdmchannel.py20
-rw-r--r--_pytest/test_slackrequest.py11
-rw-r--r--_pytest/test_slackteam.py10
-rw-r--r--_pytest/test_slackts.py24
-rw-r--r--_pytest/test_unfurl.py25
-rw-r--r--wee_slack.py4663
-rw-r--r--weemoji.json1
150 files changed, 4117 insertions, 2210 deletions
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..6739e1c
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,11 @@
+language: python
+python:
+ - "2.7"
+install:
+ - 'pip install websocket-client pytest'
+
+script: python -m pytest
+
+notifications:
+ irc: "chat.freenode.net#wee-slack-dev"
+ email: false
diff --git a/README.md b/README.md
index 46bf652..61f66dd 100644
--- a/README.md
+++ b/README.md
@@ -4,16 +4,17 @@ wee-slack
=========
**News:**
- The 0.99.1+ has a number of backend changes to make things faster and better. You should use it. :) _(please report bugs in #wee-slack on freenode)_
+ 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, search, (and more)! Connects via the Slack API, and maintains a persistent websocket for notification of events.
+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)
Features
--------
- * **New** Slash commands (including custom ones!)
- * **New** Upload to slack capabilities!
+ * **New** [Threads](#threads) 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.
@@ -29,7 +30,6 @@ Features
* Colorized nicks in chat
* Supports bidirectional slack read notifications for all channels. (never reread the same messages on the web client or other devices).
* Typing notification, so you can see when others are typing, and they can see when you type. Appears globally for direct messages
- * Search slack history allows you to do simple searches across all previous slack conversations
* Away/back status handling
* Expands/shows metadata for things like tweets/links
* Displays edited messages (slack.com irc mode currently doesn't show these)
@@ -37,9 +37,7 @@ Features
In Development
--------------
- * fix search
* add notification of new versions of wee-slack
- * growl notification
Dependencies
@@ -193,6 +191,24 @@ Debug mode:
/slack debug
```
+#### Threads
+
+Start a new thread on the most recent message The number indicates which message in the buffer to reply to, in reverse time order:
+```
+/reply 1 here is a threaded reply to the most recent message!
+```
+
+Open an existing thread as a channel. The argument is the thread identifier, which is printed in square brackets with every threaded message in a channel:
+```
+/thread af8
+```
+
+Label a thread with a memorable name. The above command will open a channel called af8, but perhaps you want to call it "meetingnotes". To do so, select that buffer and type:
+```
+/label meetingnotes
+```
+_Note: labels do not persist once a thread buffer is closed_
+
Optional settings
-----------------
diff --git a/_pytest/conftest.py b/_pytest/conftest.py
index e9b7750..0259ac2 100644
--- a/_pytest/conftest.py
+++ b/_pytest/conftest.py
@@ -1,15 +1,48 @@
+import json
import pytest
import sys
sys.path.append(".")
-#sys.path.append(str(pytest.config.rootdir))
-from wee_slack import SlackServer
-from wee_slack import Channel
-from wee_slack import User
-from wee_slack import SearchList
+#New stuff
+from wee_slack import EventRouter
+from wee_slack import SlackRequest
import wee_slack
+class fakewebsocket(object):
+ def __init__(self):
+ self.returndata = []
+ pass
+ def add(self, data):
+ self.returndata.append(data)
+ def recv(self):
+ return json.dumps(self.returndata.pop(0))
+ def send(self, data):
+ print "websocket received: {}".format(data)
+ return
+
+@pytest.fixture
+def mock_websocket():
+ return fakewebsocket()
+
+@pytest.fixture
+def realish_eventrouter():
+ 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()
+ #e.sc is just shortcuts to these items
+ e.sc = {}
+ e.sc["team_id"] = e.teams.keys()[0]
+ e.sc["team"] = e.teams[e.sc["team_id"]]
+ e.sc["user"] = e.teams[e.sc["team_id"]].users[e.teams[e.sc["team_id"]].users.keys()[0]]
+ socket = mock_websocket
+ e.teams[e.sc["team_id"]].ws = socket
+
+ return e
+
+
class FakeWeechat():
"""
this is the thing that acts as "w." everywhere..
@@ -18,7 +51,8 @@ class FakeWeechat():
WEECHAT_RC_OK = True
def __init__(self):
- print "INITIALIZE FAKE WEECHAT"
+ pass
+ #print "INITIALIZE FAKE WEECHAT"
def prnt(*args):
output = "("
for arg in args:
@@ -33,77 +67,24 @@ class FakeWeechat():
return "1355517519"
def hdata_string(*args):
return "testuser"
-
+ def buffer_new(*args):
+ return "0x8a8a8a8b"
def __getattr__(self, name):
def method(*args):
- print "called {}".format(name)
- if args:
- print "\twith args: {}".format(args)
+ pass
+ #print "called {}".format(name)
+ #if args:
+ # print "\twith args: {}".format(args)
return method
@pytest.fixture
-def fake_weechat():
+def mock_weechat():
wee_slack.w = FakeWeechat()
- pass
-
-
-@pytest.fixture
-def slack_debug():
+ wee_slack.config = wee_slack.PluginConfig()
+ wee_slack.debug_string = None
wee_slack.slack_debug = "debug_buffer_ptr"
-
-@pytest.fixture
-def server(fake_weechat, monkeypatch):
-#def server(monkeypatch, mychannels, myusers):
- def mock_connect_to_slack(*args):
- return True
- monkeypatch.setattr(SlackServer, 'connect_to_slack', mock_connect_to_slack)
- myserver = SlackServer('xoxo-12345')
- myserver.identifier = 'test.slack.com'
- myserver.nick = 'myusername'
- return myserver
-
-@pytest.fixture
-def myservers(server):
- servers = SearchList()
- servers.append(server)
- return servers
-
-
-
-@pytest.fixture
-def channel(monkeypatch, server):
- def mock_buffer_prnt(*args):
- print "called buffer_prnt\n\twith args: {}".format(args)
- return
- def mock_do_nothing(*args):
- print args
- return True
- monkeypatch.setattr(Channel, 'create_buffer', mock_do_nothing)
- monkeypatch.setattr(Channel, 'attach_buffer', mock_do_nothing)
- monkeypatch.setattr(Channel, 'set_topic', mock_do_nothing)
- monkeypatch.setattr(Channel, 'set_topic', mock_do_nothing)
- monkeypatch.setattr(Channel, 'buffer_prnt', mock_buffer_prnt)
- mychannel = Channel(server, '#testchan', 'C2147483705', True, last_read=0, prepend_name="", members=[], topic="")
- return mychannel
-
-@pytest.fixture
-def mychannels(channel):
- channels = SearchList()
- channels.append(channel)
- return channels
-
-@pytest.fixture
-def user(monkeypatch, server):
- wee_slack.domain = None
- wee_slack.colorize_nicks = True
+ wee_slack.STOP_TALKING_TO_SLACK = False
+ wee_slack.proc = {}
pass
- myuser = User(server, "testuser", 'U2147483697', presence="away")
- myuser.color = ''
- return myuser
-@pytest.fixture
-def myusers(monkeypatch, user):
- users = SearchList()
- users.append(user)
- return users
diff --git a/_pytest/data/http/rtm.start.json b/_pytest/data/http/rtm.start.json
new file mode 100644
index 0000000..d9cc464
--- /dev/null
+++ b/_pytest/data/http/rtm.start.json
@@ -0,0 +1,861 @@
+{
+ "ok": true,
+ "self": {
+ "id": "U3ZKBBDL5",
+ "name": "bob",
+ "prefs": {
+ "highlight_words": "",
+ "user_colors": "",
+ "color_names_in_list": true,
+ "growls_enabled": true,
+ "tz": "America\/Los_Angeles",
+ "push_dm_alert": true,
+ "push_mention_alert": true,
+ "push_everything": true,
+ "push_show_preview": true,
+ "push_idle_wait": 2,
+ "push_sound": "b2.mp3",
+ "push_loud_channels": "",
+ "push_mention_channels": "",
+ "push_loud_channels_set": "",
+ "threads_everything": false,
+ "email_alerts": "instant",
+ "email_alerts_sleep_until": 0,
+ "email_misc": false,
+ "email_weekly": true,
+ "welcome_message_hidden": false,
+ "all_channels_loud": true,
+ "loud_channels": "",
+ "never_channels": "",
+ "loud_channels_set": "",
+ "search_sort": "timestamp",
+ "expand_inline_imgs": true,
+ "expand_internal_inline_imgs": true,
+ "expand_snippets": false,
+ "posts_formatting_guide": true,
+ "seen_welcome_2": true,
+ "seen_ssb_prompt": false,
+ "spaces_new_xp_banner_dismissed": false,
+ "search_only_my_channels": false,
+ "search_only_current_team": false,
+ "emoji_mode": "default",
+ "emoji_use": "",
+ "has_invited": false,
+ "has_uploaded": false,
+ "has_created_channel": false,
+ "has_searched": false,
+ "search_exclude_channels": "",
+ "messages_theme": "default",
+ "webapp_spellcheck": true,
+ "no_joined_overlays": false,
+ "no_created_overlays": false,
+ "dropbox_enabled": false,
+ "seen_domain_invite_reminder": false,
+ "seen_member_invite_reminder": false,
+ "mute_sounds": false,
+ "arrow_history": false,
+ "tab_ui_return_selects": true,
+ "obey_inline_img_limit": true,
+ "new_msg_snd": "knock_brush.mp3",
+ "require_at": false,
+ "ssb_space_window": "",
+ "mac_ssb_bounce": "",
+ "mac_ssb_bullet": true,
+ "expand_non_media_attachments": true,
+ "show_typing": true,
+ "pagekeys_handled": true,
+ "last_snippet_type": "",
+ "display_real_names_override": 0,
+ "display_preferred_names": true,
+ "time24": false,
+ "enter_is_special_in_tbt": false,
+ "graphic_emoticons": false,
+ "convert_emoticons": true,
+ "ss_emojis": true,
+ "sidebar_behavior": "",
+ "seen_onboarding_start": false,
+ "onboarding_cancelled": true,
+ "seen_onboarding_slackbot_conversation": false,
+ "seen_onboarding_channels": false,
+ "seen_onboarding_direct_messages": false,
+ "seen_onboarding_invites": false,
+ "seen_onboarding_search": false,
+ "seen_onboarding_recent_mentions": false,
+ "seen_onboarding_starred_items": false,
+ "seen_onboarding_private_groups": false,
+ "onboarding_slackbot_conversation_step": 0,
+ "dnd_enabled": true,
+ "dnd_start_hour": "22:00",
+ "dnd_end_hour": "08:00",
+ "mark_msgs_read_immediately": true,
+ "start_scroll_at_oldest": true,
+ "snippet_editor_wrap_long_lines": false,
+ "ls_disabled": false,
+ "sidebar_theme": "default",
+ "sidebar_theme_custom_values": "",
+ "f_key_search": false,
+ "k_key_omnibox": true,
+ "speak_growls": false,
+ "mac_speak_voice": "com.apple.speech.synthesis.voice.Alex",
+ "mac_speak_speed": 250,
+ "at_channel_suppressed_channels": "",
+ "push_at_channel_suppressed_channels": "",
+ "prompted_for_email_disabling": false,
+ "full_text_extracts": false,
+ "no_text_in_notifications": false,
+ "muted_channels": "",
+ "no_macelectron_banner": false,
+ "no_macssb1_banner": false,
+ "no_macssb2_banner": false,
+ "no_winssb1_banner": false,
+ "no_invites_widget_in_sidebar": false,
+ "no_omnibox_in_channels": false,
+ "k_key_omnibox_auto_hide_count": 0,
+ "prev_next_btn": false,
+ "hide_user_group_info_pane": false,
+ "mentions_exclude_at_user_groups": false,
+ "privacy_policy_seen": true,
+ "enterprise_migration_seen": true,
+ "last_tos_acknowledged": "tos_oct2016",
+ "search_exclude_bots": false,
+ "load_lato_2": false,
+ "fuller_timestamps": false,
+ "last_seen_at_channel_warning": 0,
+ "msg_preview": false,
+ "msg_preview_persistent": true,
+ "emoji_autocomplete_big": false,
+ "winssb_run_from_tray": true,
+ "winssb_window_flash_behavior": "idle",
+ "two_factor_auth_enabled": false,
+ "two_factor_type": null,
+ "two_factor_backup_type": null,
+ "hide_hex_swatch": false,
+ "client_logs_pri": "",
+ "enhanced_debugging": false,
+ "thin_channel_membership_fe": false,
+ "flannel_server_pool": "random",
+ "mentions_exclude_at_channels": true,
+ "confirm_clear_all_unreads": true,
+ "confirm_user_marked_away": true,
+ "box_enabled": false,
+ "seen_single_emoji_msg": false,
+ "confirm_sh_call_start": true,
+ "preferred_skin_tone": "",
+ "show_all_skin_tones": false,
+ "separate_private_channels": false,
+ "whats_new_read": 1485969645,
+ "hotness": false,
+ "frecency_jumper": "",
+ "frecency_ent_jumper": "",
+ "jumbomoji": true,
+ "newxp_seen_last_message": "1",
+ "attachments_with_borders": false,
+ "show_memory_instrument": false,
+ "enable_unread_view": false,
+ "seen_unread_view_coachmark": false,
+ "seen_calls_video_beta_coachmark": false,
+ "seen_calls_video_ga_coachmark": false,
+ "seen_calls_ss_window_coachmark": false,
+ "measure_css_usage": false,
+ "enable_react_emoji_picker": true,
+ "seen_replies_coachmark": false,
+ "seen_custom_status_badge": false,
+ "all_unreads_sort_order": "alphabetical",
+ "locale": "en_US",
+ "gdrive_authed": true,
+ "gdrive_enabled": true,
+ "seen_gdrive_coachmark": false,
+ "channel_sort": "{\"is_custom_sorted\":false, \"priority_display\":false, \"priority_type\":\"\", \"sorts\":[]}",
+ "overloaded_message_enabled": true,
+ "seen_highlights_coachmark": false,
+ "seen_highlights_arrows_coachmark": false,
+ "a11y_font_size": "normal",
+ "a11y_animations": true,
+ "intro_to_apps_message_seen": false
+ },
+ "created": 1485969645,
+ "manual_presence": "active"
+ },
+ "team": {
+ "id": "T3YS5EAL9",
+ "name": "weeslacktest",
+ "email_domain": "",
+ "domain": "weeslacktest",
+ "msg_edit_window_mins": -1,
+ "prefs": {
+ "invites_only_admins": false,
+ "default_channels": [
+ "C407ABS94",
+ "C3ZEQAYN7"
+ ],
+ "display_email_addresses": false,
+ "hide_referers": true,
+ "msg_edit_window_mins": -1,
+ "allow_message_deletion": true,
+ "calling_app_name": "Slack",
+ "allow_calls": true,
+ "display_real_names": false,
+ "who_can_at_everyone": "regular",
+ "who_can_at_channel": "ra",
+ "who_can_create_channels": "regular",
+ "who_can_archive_channels": "regular",
+ "who_can_create_groups": "ra",
+ "who_can_post_general": "ra",
+ "who_can_kick_channels": "admin",
+ "who_can_kick_groups": "regular",
+ "retention_type": 0,
+ "retention_duration": 0,
+ "group_retention_type": 0,
+ "group_retention_duration": 0,
+ "dm_retention_type": 0,
+ "dm_retention_duration": 0,
+ "file_retention_duration": 0,
+ "file_retention_type": 0,
+ "allow_retention_override": true,
+ "require_at_for_mention": false,
+ "default_rxns": [
+ "simple_smile",
+ "thumbsup",
+ "white_check_mark",
+ "heart",
+ "eyes"
+ ],
+ "team_handy_rxns": {
+ "restrict": false,
+ "list": [
+ {
+ "name": "slightly_smiling_face",
+ "title": "I'm happy!"
+ },
+ {
+ "name": "+1",
+ "title": "good!"
+ },
+ {
+ "name": "white_check_mark",
+ "title": "done!"
+ },
+ {
+ "name": "heart",
+ "title": "love it!"
+ },
+ {
+ "name": "eyes",
+ "title": "looking..."
+ }
+ ]
+ },
+ "channel_handy_rxns": null,
+ "compliance_export_start": 0,
+ "warn_before_at_channel": "always",
+ "disallow_public_file_urls": false,
+ "who_can_create_delete_user_groups": "admin",
+ "who_can_edit_user_groups": "admin",
+ "who_can_change_team_profile": "admin",
+ "allow_shared_channels": false,
+ "who_has_team_visibility": "ra",
+ "disable_file_uploads": "allow_all",
+ "disable_file_editing": false,
+ "disable_file_deleting": false,
+ "who_can_create_shared_channels": "admin",
+ "who_can_manage_shared_channels": {
+ "type": [
+ "admin"
+ ]
+ },
+ "who_can_post_in_shared_channels": {
+ "type": [
+ "regular",
+ "ra"
+ ]
+ },
+ "allow_shared_channel_perms_override": false,
+ "gdrive_enabled_team": true,
+ "dnd_enabled": true,
+ "dnd_start_hour": "22:00",
+ "dnd_end_hour": "08:00",
+ "auth_mode": "normal",
+ "who_can_manage_integrations": {
+ "type": [
+ "regular"
+ ]
+ },
+ "invites_limit": true
+ },
+ "icon": {
+ "image_34": "https:\/\/a.slack-edge.com\/0180\/img\/avatars-teams\/ava_0014-34.png",
+ "image_44": "https:\/\/a.slack-edge.com\/0180\/img\/avatars-teams\/ava_0014-44.png",
+ "image_68": "https:\/\/a.slack-edge.com\/66f9\/img\/avatars-teams\/ava_0014-68.png",
+ "image_88": "https:\/\/a.slack-edge.com\/66f9\/img\/avatars-teams\/ava_0014-88.png",
+ "image_102": "https:\/\/a.slack-edge.com\/66f9\/img\/avatars-teams\/ava_0014-102.png",
+ "image_132": "https:\/\/a.slack-edge.com\/66f9\/img\/avatars-teams\/ava_0014-132.png",
+ "image_230": "https:\/\/a.slack-edge.com\/bfaba\/img\/avatars-teams\/ava_0014-230.png",
+ "image_default": true
+ },
+ "over_storage_limit": false,
+ "plan": "",
+ "avatar_base_url": "https:\/\/ca.slack-edge.com\/",
+ "over_integrations_limit": true
+ },
+ "latest_event_ts": "1485976462.000000",
+ "channels": [
+ {
+ "id": "C407ABS94",
+ "name": "general",
+ "is_channel": true,
+ "created": 1485969592,
+ "creator": "U407ABLLW",
+ "is_archived": false,
+ "is_general": true,
+ "has_pins": false,
+ "is_member": true,
+ "last_read": "1485976236.000019",
+ "latest": {
+ "type": "message",
+ "subtype": "file_comment",
+ "text": "<@U407ABLLW|alice> commented on <@U407ABLLW|alice>\u2019s file <https:\/\/weeslacktest.slack.com\/files\/alice\/F3ZJQTA66\/1x1.png|1x1.png>: first comment! now i edited it.",
+ "file": {
+ "id": "F3ZJQTA66",
+ "created": 1485975955,
+ "timestamp": 1485975955,
+ "name": "1x1.png",
+ "title": "1x1.png",
+ "mimetype": "image\/png",
+ "filetype": "png",
+ "pretty_type": "PNG",
+ "user": "U407ABLLW",
+ "editable": false,
+ "size": 68,
+ "mode": "hosted",
+ "is_external": false,
+ "external_type": "",
+ "is_public": true,
+ "public_url_shared": false,
+ "display_as_bot": false,
+ "username": "",
+ "url_private": "https:\/\/files.slack.com\/files-pri\/T3YS5EAL9-F3ZJQTA66\/1x1.png",
+ "url_private_download": "https:\/\/files.slack.com\/files-pri\/T3YS5EAL9-F3ZJQTA66\/download\/1x1.png",
+ "thumb_64": "https:\/\/files.slack.com\/files-tmb\/T3YS5EAL9-F3ZJQTA66-8ab528dd77\/1x1_64.png",
+ "thumb_80": "https:\/\/files.slack.com\/files-tmb\/T3YS5EAL9-F3ZJQTA66-8ab528dd77\/1x1_80.png",
+ "thumb_360": "https:\/\/files.slack.com\/files-tmb\/T3YS5EAL9-F3ZJQTA66-8ab528dd77\/1x1_360.png",
+ "thumb_360_w": 1,
+ "thumb_360_h": 1,
+ "thumb_160": "https:\/\/files.slack.com\/files-tmb\/T3YS5EAL9-F3ZJQTA66-8ab528dd77\/1x1_160.png",
+ "image_exif_rotation": 1,
+ "original_w": 1,
+ "original_h": 1,
+ "permalink": "https:\/\/weeslacktest.slack.com\/files\/alice\/F3ZJQTA66\/1x1.png",
+ "permalink_public": "https:\/\/slack-files.com\/T3YS5EAL9-F3ZJQTA66-5d747593d2",
+ "channels": [
+ "C407ABS94"
+ ],
+ "groups": [],
+ "ims": [],
+ "comments_count": 1
+ },
+ "comment": {
+ "id": "Fc3ZMDRQLV",
+ "created": 1485976236,
+ "timestamp": 1485976236,
+ "user": "U407ABLLW",
+ "is_intro": false,
+ "comment": "first comment! now i edited it.",
+ "channel": ""
+ },
+ "is_intro": false,
+ "ts": "1485976236.000019"
+ },
+ "unread_count": 0,
+ "unread_count_display": 0,
+ "members": [
+ "U3ZKBBDL5",
+ "U407ABLLW",
+ "U4096CBHC"
+ ],
+ "topic": {
+ "value": "Company-wide announcements and work-based matters",
+ "creator": "",
+ "last_set": 0
+ },
+ "purpose": {
+ "value": "This channel is for team-wide communication and announcements. All team members are in this channel.",
+ "creator": "",
+ "last_set": 0
+ },
+ "previous_names": []
+ },
+ {
+ "id": "C3ZM2GMGU",
+ "name": "made-to-be-archived",
+ "is_channel": true,
+ "created": 1485976107,
+ "creator": "U407ABLLW",
+ "is_archived": false,
+ "is_general": false,
+ "has_pins": false,
+ "is_member": false,
+ "previous_names": []
+ },
+ {
+ "id": "C3ZEQAYN7",
+ "name": "random",
+ "is_channel": true,
+ "created": 1485969592,
+ "creator": "U407ABLLW",
+ "is_archived": false,
+ "is_general": false,
+ "has_pins": false,
+ "is_member": true,
+ "last_read": "1485969592.000002",
+ "latest": {
+ "user": "U4096CBHC",
+ "text": "<@U4096CBHC|charles> has joined the channel",
+ "type": "message",
+ "subtype": "channel_join",
+ "ts": "1485975606.000004"
+ },
+ "unread_count": 2,
+ "unread_count_display": 0,
+ "members": [
+ "U3ZKBBDL5",
+ "U407ABLLW",
+ "U4096CBHC"
+ ],
+ "topic": {
+ "value": "Non-work banter and water cooler conversation",
+ "creator": "",
+ "last_set": 0
+ },
+ "purpose": {
+ "value": "A place for non-work-related flimflam, faffing, hodge-podge or jibber-jabber you'd prefer to keep out of more focused work-related channels.",
+ "creator": "",
+ "last_set": 0
+ },
+ "previous_names": []
+ },
+ {
+ "id": "C3ZM8JTD3",
+ "name": "some-channel2-renamed",
+ "is_channel": true,
+ "created": 1485975747,
+ "creator": "U407ABLLW",
+ "is_archived": false,
+ "is_general": false,
+ "has_pins": false,
+ "is_member": false,
+ "previous_names": [
+ "some-channel2"
+ ]
+ }
+ ],
+ "groups": [
+ {
+ "id": "G3ZGMF4RZ",
+ "name": "mpdm-bob--alice--charles-1",
+ "is_group": true,
+ "created": 1485975690,
+ "creator": "U407ABLLW",
+ "is_archived": false,
+ "is_mpim": true,
+ "has_pins": false,
+ "is_open": true,
+ "last_read": "0000000000.000000",
+ "latest": {
+ "type": "message",
+ "subtype": "file_share",
+ "text": "<@U407ABLLW|alice> uploaded a file: <https:\/\/weeslacktest.slack.com\/files\/alice\/F3ZLY6K5J\/-.txt|Untitled>",
+ "file": {
+ "id": "F3ZLY6K5J",
+ "created": 1485975715,
+ "timestamp": 1485975715,
+ "name": "-.txt",
+ "title": "Untitled",
+ "mimetype": "text\/plain",
+ "filetype": "text",
+ "pretty_type": "Plain Text",
+ "user": "U407ABLLW",
+ "editable": true,
+ "size": 14,
+ "mode": "snippet",
+ "is_external": false,
+ "external_type": "",
+ "is_public": false,
+ "public_url_shared": false,
+ "display_as_bot": false,
+ "username": "",
+ "url_private": "https:\/\/files.slack.com\/files-pri\/T3YS5EAL9-F3ZLY6K5J\/-.txt",
+ "url_private_download": "https:\/\/files.slack.com\/files-pri\/T3YS5EAL9-F3ZLY6K5J\/download\/-.txt",
+ "permalink": "https:\/\/weeslacktest.slack.com\/files\/alice\/F3ZLY6K5J\/-.txt",
+ "permalink_public": "https:\/\/slack-files.com\/T3YS5EAL9-F3ZLY6K5J-39c2c4f739",
+ "edit_link": "https:\/\/weeslacktest.slack.com\/files\/alice\/F3ZLY6K5J\/-.txt\/edit",
+ "preview": "some code here",
+ "preview_highlight": "<div class=\"CodeMirror cm-s-default CodeMirrorServer\" oncopy=\"if(event.clipboardData){event.clipboardData.setData('text\/plain',window.getSelection().toString().replace(\/\\u200b\/g,''));event.preventDefault();event.stopPropagation();}\">\n<div class=\"CodeMirror-code\">\n<div><pre>some code here<\/pre><\/div>\n<\/div>\n<\/div>\n",
+ "lines": 1,
+ "lines_more": 0,
+ "preview_is_truncated": false,
+ "channels": [],
+ "groups": [
+ "G3ZGMF4RZ"
+ ],
+ "ims": [],
+ "comments_count": 0
+ },
+ "user": "U407ABLLW",
+ "upload": true,
+ "display_as_bot": false,
+ "username": "<@U407ABLLW|alice>",
+ "bot_id": null,
+ "ts": "1485975715.000005"
+ },
+ "unread_count": 1,
+ "unread_count_display": 1,
+ "members": [
+ "U407ABLLW",
+ "U3ZKBBDL5",
+ "U4096CBHC"
+ ],
+ "topic": {
+ "value": "Group messaging",
+ "creator": "U407ABLLW",
+ "last_set": 1485975690
+ },
+ "purpose": {
+ "value": "Group messaging with: @bob @alice @charles",
+ "creator": "U407ABLLW",
+ "last_set": 1485975690
+ }
+ },
+ {
+ "id": "G3ZJKP7GA",
+ "name": "some-private-channel",
+ "is_group": true,
+ "created": 1485975492,
+ "creator": "U407ABLLW",
+ "is_archived": false,
+ "is_mpim": false,
+ "has_pins": false,
+ "is_open": true,
+ "last_read": "1485975492.000002",
+ "latest": {
+ "user": "U407ABLLW",
+ "purpose": "seekret",
+ "text": "<@U407ABLLW|alice> set the channel's purpose: seekret",
+ "type": "message",
+ "subtype": "group_purpose",
+ "ts": "1485975492.000004"
+ },
+ "unread_count": 2,
+ "unread_count_display": 1,
+ "members": [
+ "U3ZKBBDL5",
+ "U407ABLLW"
+ ],
+ "topic": {
+ "value": "",
+ "creator": "",
+ "last_set": 0
+ },
+ "purpose": {
+ "value": "seekret",
+ "creator": "U407ABLLW",
+ "last_set": 1485975493
+ }
+ },
+ {
+ "id": "G409GKN9M",
+ "name": "some-channel",
+ "is_group": true,
+ "created": 1485975458,
+ "creator": "U407ABLLW",
+ "is_archived": true,
+ "is_mpim": false,
+ "has_pins": false,
+ "is_open": false,
+ "last_read": "1485975476.876631",
+ "latest": {
+ "user": "U407ABLLW",
+ "text": "<@U407ABLLW|alice> archived the private channel",
+ "type": "message",
+ "subtype": "group_archive",
+ "ts": "1485975735.000006"
+ },
+ "unread_count": 1,
+ "unread_count_display": 1,
+ "members": [
+ "U3ZKBBDL5",
+ "U407ABLLW"
+ ],
+ "topic": {
+ "value": "",
+ "creator": "U407ABLLW",
+ "last_set": 1485975476
+ },
+ "purpose": {
+ "value": "who knows?",
+ "creator": "U407ABLLW",
+ "last_set": 1485975476
+ }
+ }
+ ],
+ "ims": [
+ {
+ "id": "D3ZK1D8JY",
+ "created": 1485969645,
+ "is_im": true,
+ "is_org_shared": false,
+ "user": "USLACKBOT",
+ "has_pins": false,
+ "last_read": "1485969667.000002",
+ "latest": {
+ "type": "message",
+ "user": "USLACKBOT",
+ "text": "<@U407ABLLW|alice> archived the private channel <https:\/\/weeslacktest.slack.com\/archives\/some-channel|some-channel>",
+ "ts": "1485975735.000002"
+ },
+ "unread_count": 1,
+ "unread_count_display": 1,
+ "is_open": true
+ },
+ {
+ "id": "D3ZEQULFM",
+ "created": 1485969645,
+ "is_im": true,
+ "is_org_shared": false,
+ "user": "U3ZKBBDL5",
+ "has_pins": false,
+ "last_read": "0000000000.000000",
+ "latest": null,
+ "unread_count": 0,
+ "unread_count_display": 0,
+ "is_open": true
+ },
+ {
+ "id": "D3ZEQULHZ",
+ "created": 1485969645,
+ "is_im": true,
+ "is_org_shared": false,
+ "user": "U407ABLLW",
+ "has_pins": false,
+ "last_read": "0000000000.000000",
+ "latest": {
+ "type": "message",
+ "user": "U407ABLLW",
+ "text": "hi bob",
+ "ts": "1485975421.000002"
+ },
+ "unread_count": 1,
+ "unread_count_display": 1,
+ "is_open": true
+ },
+ {
+ "id": "D409J34CF",
+ "created": 1485975606,
+ "is_im": true,
+ "is_org_shared": false,
+ "user": "U4096CBHC",
+ "has_pins": false,
+ "last_read": "0000000000.000000",
+ "latest": null,
+ "unread_count": 0,
+ "unread_count_display": 0,
+ "is_open": true
+ }
+ ],
+ "cache_ts": 1485977062,
+ "read_only_channels": [],
+ "can_manage_shared_channels": false,
+ "subteams": {
+ "self": [],
+ "all": []
+ },
+ "dnd": {
+ "dnd_enabled": true,
+ "dnd_debug": {
+ "next_dnd_start_date": "Wed, 01 Feb 2017 22:00:00 -0800",
+ "next_dnd_end_date": "Thu, 02 Feb 2017 08:00:00 -0800",
+ "now": "Wed, 01 Feb 2017 11:24:22 -0800",
+ "type": "slack"
+ },
+ "next_dnd_start_ts": 1486015200,
+ "next_dnd_end_ts": 1486051200,
+ "snooze_enabled": false
+ },
+ "users": [
+ {
+ "id": "U407ABLLW",
+ "team_id": "T3YS5EAL9",
+ "name": "alice",
+ "deleted": false,
+ "status": null,
+ "color": "9f69e7",
+ "real_name": "First Testuser",
+ "tz": "America\/Los_Angeles",
+ "tz_label": "Pacific Standard Time",
+ "tz_offset": -28800,
+ "profile": {
+ "first_name": "First",
+ "last_name": "Testuser",
+ "avatar_hash": "gfd0ce7a168d",
+ "real_name": "First Testuser",
+ "real_name_normalized": "First Testuser",
+ "email": "redacted1@gmail.com",
+ "image_24": "https:\/\/secure.gravatar.com\/avatar\/fd0ce7a168dc2235d21c53c2c1cfee0c.jpg?s=24&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0024-24.png",
+ "image_32": "https:\/\/secure.gravatar.com\/avatar\/fd0ce7a168dc2235d21c53c2c1cfee0c.jpg?s=32&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0024-32.png",
+ "image_48": "https:\/\/secure.gravatar.com\/avatar\/fd0ce7a168dc2235d21c53c2c1cfee0c.jpg?s=48&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0024-48.png",
+ "image_72": "https:\/\/secure.gravatar.com\/avatar\/fd0ce7a168dc2235d21c53c2c1cfee0c.jpg?s=72&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0024-72.png",
+ "image_192": "https:\/\/secure.gravatar.com\/avatar\/fd0ce7a168dc2235d21c53c2c1cfee0c.jpg?s=192&d=https%3A%2F%2Fa.slack-edge.com%2F7fa9%2Fimg%2Favatars%2Fava_0024-192.png",
+ "image_512": "https:\/\/secure.gravatar.com\/avatar\/fd0ce7a168dc2235d21c53c2c1cfee0c.jpg?s=512&d=https%3A%2F%2Fa.slack-edge.com%2F7fa9%2Fimg%2Favatars%2Fava_0024-512.png",
+ "fields": null
+ },
+ "is_admin": true,
+ "is_owner": true,
+ "is_primary_owner": true,
+ "is_restricted": false,
+ "is_ultra_restricted": false,
+ "is_bot": false,
+ "presence": "away"
+ },
+ {
+ "id": "U3ZKBBDL5",
+ "team_id": "T3YS5EAL9",
+ "name": "bob",
+ "deleted": false,
+ "status": null,
+ "color": "4bbe2e",
+ "real_name": "Second Testuser",
+ "tz": "America\/Los_Angeles",
+ "tz_label": "Pacific Standard Time",
+ "tz_offset": -28800,
+ "profile": {
+ "first_name": "Second",
+ "last_name": "Testuser",
+ "avatar_hash": "g6f7a5bf7eb7",
+ "real_name": "Second Testuser",
+ "real_name_normalized": "Second Testuser",
+ "email": "redacted2@gmail.com",
+ "image_24": "https:\/\/secure.gravatar.com\/avatar\/6f7a5bf7eb782853afb1d33f28ca9ae7.jpg?s=24&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0019-24.png",
+ "image_32": "https:\/\/secure.gravatar.com\/avatar\/6f7a5bf7eb782853afb1d33f28ca9ae7.jpg?s=32&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0019-32.png",
+ "image_48": "https:\/\/secure.gravatar.com\/avatar\/6f7a5bf7eb782853afb1d33f28ca9ae7.jpg?s=48&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0019-48.png",
+ "image_72": "https:\/\/secure.gravatar.com\/avatar\/6f7a5bf7eb782853afb1d33f28ca9ae7.jpg?s=72&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0019-72.png",
+ "image_192": "https:\/\/secure.gravatar.com\/avatar\/6f7a5bf7eb782853afb1d33f28ca9ae7.jpg?s=192&d=https%3A%2F%2Fa.slack-edge.com%2F7fa9%2Fimg%2Favatars%2Fava_0019-192.png",
+ "image_512": "https:\/\/secure.gravatar.com\/avatar\/6f7a5bf7eb782853afb1d33f28ca9ae7.jpg?s=512&d=https%3A%2F%2Fa.slack-edge.com%2F7fa9%2Fimg%2Favatars%2Fava_0019-512.png",
+ "fields": null
+ },
+ "is_admin": false,
+ "is_owner": false,
+ "is_primary_owner": false,
+ "is_restricted": false,
+ "is_ultra_restricted": false,
+ "is_bot": false,
+ "has_2fa": false,
+ "presence": "active"
+ },
+ {
+ "id": "U4096CBHC",
+ "team_id": "T3YS5EAL9",
+ "name": "charles",
+ "deleted": false,
+ "status": null,
+ "color": "e7392d",
+ "real_name": "Charles Testuser",
+ "tz": "America\/Los_Angeles",
+ "tz_label": "Pacific Standard Time",
+ "tz_offset": -28800,
+ "profile": {
+ "first_name": "Charles",
+ "last_name": "Testuser",
+ "avatar_hash": "5af404f7d4b7",
+ "fields": [],
+ "image_24": "https:\/\/avatars.slack-edge.com\/2017-02-01\/135714629508_5af404f7d4b7728d10c0_24.png",
+ "image_32": "https:\/\/avatars.slack-edge.com\/2017-02-01\/135714629508_5af404f7d4b7728d10c0_24.png",
+ "image_48": "https:\/\/avatars.slack-edge.com\/2017-02-01\/135714629508_5af404f7d4b7728d10c0_24.png",
+ "image_72": "https:\/\/avatars.slack-edge.com\/2017-02-01\/135714629508_5af404f7d4b7728d10c0_24.png",
+ "image_192": "https:\/\/avatars.slack-edge.com\/2017-02-01\/135714629508_5af404f7d4b7728d10c0_24.png",
+ "image_512": "https:\/\/avatars.slack-edge.com\/2017-02-01\/135714629508_5af404f7d4b7728d10c0_24.png",
+ "image_1024": "https:\/\/avatars.slack-edge.com\/2017-02-01\/135714629508_5af404f7d4b7728d10c0_24.png",
+ "image_original": "https:\/\/avatars.slack-edge.com\/2017-02-01\/135714629508_5af404f7d4b7728d10c0_original.png",
+ "real_name": "Charles Testuser",
+ "real_name_normalized": "Charles Testuser",
+ "email": "redacted3@gmail.com"
+ },
+ "is_admin": false,
+ "is_owner": false,
+ "is_primary_owner": false,
+ "is_restricted": false,
+ "is_ultra_restricted": false,
+ "is_bot": false,
+ "presence": "away"
+ },
+ {
+ "id": "USLACKBOT",
+ "team_id": "T3YS5EAL9",
+ "name": "slackbot",
+ "deleted": false,
+ "status": null,
+ "color": "757575",
+ "real_name": "slackbot",
+ "tz": null,
+ "tz_label": "Pacific Standard Time",
+ "tz_offset": -28800,
+ "profile": {
+ "first_name": "slackbot",
+ "last_name": "",
+ "image_24": "https:\/\/a.slack-edge.com\/0180\/img\/slackbot_24.png",
+ "image_32": "https:\/\/a.slack-edge.com\/2fac\/plugins\/slackbot\/assets\/service_32.png",
+ "image_48": "https:\/\/a.slack-edge.com\/2fac\/plugins\/slackbot\/assets\/service_48.png",
+ "image_72": "https:\/\/a.slack-edge.com\/0180\/img\/slackbot_72.png",
+ "image_192": "https:\/\/a.slack-edge.com\/66f9\/img\/slackbot_192.png",
+ "image_512": "https:\/\/a.slack-edge.com\/1801\/img\/slackbot_512.png",
+ "avatar_hash": "sv1444671949",
+ "real_name": "slackbot",
+ "real_name_normalized": "slackbot",
+ "fields": null
+ },
+ "is_admin": false,
+ "is_owner": false,
+ "is_primary_owner": false,
+ "is_restricted": false,
+ "is_ultra_restricted": false,
+ "is_bot": false,
+ "presence": "active"
+ }
+ ],
+ "cache_version": "v15-koala",
+ "cache_ts_version": "v1-cat",
+ "bots": [
+ {
+ "id": "B3YTBU6L8",
+ "deleted": false,
+ "name": "incoming-webhook",
+ "app_id": "A0F7XDUAZ",
+ "icons": {
+ "image_36": "https:\/\/a.slack-edge.com\/12b5a\/plugins\/tester\/assets\/service_36.png",
+ "image_48": "https:\/\/a.slack-edge.com\/12b5a\/plugins\/tester\/assets\/service_48.png",
+ "image_72": "https:\/\/a.slack-edge.com\/12b5a\/plugins\/tester\/assets\/service_72.png"
+ }
+ },
+ {
+ "id": "B3ZESMZKM",
+ "deleted": false,
+ "name": "Slack API Tester",
+ "app_id": "A02",
+ "icons": {
+ "image_36": "https:\/\/a.slack-edge.com\/b48b\/plugins\/slack_api_news\/assets\/service_36.png",
+ "image_48": "https:\/\/a.slack-edge.com\/b48b\/plugins\/slack_api_news\/assets\/service_48.png",
+ "image_72": "https:\/\/a.slack-edge.com\/b48b\/plugins\/slack_api_news\/assets\/service_72.png"
+ }
+ },
+ {
+ "id": "B407MVCA3",
+ "deleted": false,
+ "name": "gdrive",
+ "app_id": "A0F7YS32P",
+ "icons": {
+ "image_36": "https:\/\/a.slack-edge.com\/12b5a\/plugins\/gdrive\/assets\/service_36.png",
+ "image_48": "https:\/\/a.slack-edge.com\/12b5a\/plugins\/gdrive\/assets\/service_48.png",
+ "image_72": "https:\/\/a.slack-edge.com\/12b5a\/plugins\/gdrive\/assets\/service_72.png"
+ }
+ }
+ ],
+ "url": "wss:\/\/mpmulti-9ry9.slack-msgs.com\/websocket\/U4n-6Yw8mpc9C64E74gYXGbojir25QCCcLJ9eK57KDURr0o5so6x4qZU-pKn-LX0bYt-ZmIaFcN4TxoKPry1eRtOwiCNSyGyIGxnYrUtrsvMvkFr5C2-osZVL85WqzIWRoE00sBma3U8BkKfi8oUmMoYGKFFVeJ2WPI_ygHRPOo="
+}
diff --git a/_pytest/data/websocket/1485975367.79-reconnect_url.json b/_pytest/data/websocket/1485975367.79-reconnect_url.json
new file mode 100644
index 0000000..94cacca
--- /dev/null
+++ b/_pytest/data/websocket/1485975367.79-reconnect_url.json
@@ -0,0 +1 @@
+{"url": "wss://mpmulti-gvop.slack-msgs.com/websocket/tamYoLuX4lU-WBS7cFe2RCh8kqeO86F0Mi1RKFVfk7FM-QNu1KD7HiMBStfdLkwkKHmtyTACNE6SONtoQRTbnC0q9fAoLHPl76Y7y9IhCve6VKs2KNLmRH37WutXTBsj3b9HvF79VySlPgAwVXZeH0lgfDDk_RAY9l_dJ8u-jSs=", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "reconnect_url"}
diff --git a/_pytest/data/websocket/1485975408.19-user_typing.json b/_pytest/data/websocket/1485975408.19-user_typing.json
new file mode 100644
index 0000000..422e30b
--- /dev/null
+++ b/_pytest/data/websocket/1485975408.19-user_typing.json
@@ -0,0 +1 @@
+{"wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "user_typing", "user": "U407ABLLW", "channel": "D3ZEQULHZ"}
diff --git a/_pytest/data/websocket/1485975412.74-user_typing.json b/_pytest/data/websocket/1485975412.74-user_typing.json
new file mode 100644
index 0000000..df57e72
--- /dev/null
+++ b/_pytest/data/websocket/1485975412.74-user_typing.json
@@ -0,0 +1 @@
+{"wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "user_typing", "user": "U407ABLLW", "channel": "C407ABS94"}
diff --git a/_pytest/data/websocket/1485975420.36-user_typing.json b/_pytest/data/websocket/1485975420.36-user_typing.json
new file mode 100644
index 0000000..422e30b
--- /dev/null
+++ b/_pytest/data/websocket/1485975420.36-user_typing.json
@@ -0,0 +1 @@
+{"wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "user_typing", "user": "U407ABLLW", "channel": "D3ZEQULHZ"}
diff --git a/_pytest/data/websocket/1485975421.33-message.json b/_pytest/data/websocket/1485975421.33-message.json
new file mode 100644
index 0000000..9a85771
--- /dev/null
+++ b/_pytest/data/websocket/1485975421.33-message.json
@@ -0,0 +1 @@
+{"text": "hi bob", "ts": "1485975421.000002", "user": "U407ABLLW", "team": "T3YS5EAL9", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "message", "channel": "D3ZEQULHZ"}
diff --git a/_pytest/data/websocket/1485975421.42-desktop_notification.json b/_pytest/data/websocket/1485975421.42-desktop_notification.json
new file mode 100644
index 0000000..ee6739a
--- /dev/null
+++ b/_pytest/data/websocket/1485975421.42-desktop_notification.json
@@ -0,0 +1 @@
+{"avatarImage": "https://secure.gravatar.com/avatar/fd0ce7a168dc2235d21c53c2c1cfee0c.jpg?s=192&d=https%3A%2F%2Fa.slack-edge.com%2F7fa9%2Fimg%2Favatars%2Fava_0024-192.png", "subtitle": "alice", "is_shared": false, "title": "weeslacktest", "ssbFilename": "knock_brush.mp3", "imageUri": null, "launchUri": "slack://channel?id=D3ZEQULHZ&message=1485975421000002&team=T3YS5EAL9", "event_ts": "1485975421.875655", "msg": "1485975421.000002", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "content": "hi bob", "type": "desktop_notification", "channel": "D3ZEQULHZ"}
diff --git a/_pytest/data/websocket/1485975426.55-reaction_added.json b/_pytest/data/websocket/1485975426.55-reaction_added.json
new file mode 100644
index 0000000..b51cfdb
--- /dev/null
+++ b/_pytest/data/websocket/1485975426.55-reaction_added.json
@@ -0,0 +1 @@
+{"reaction": "grinning", "event_ts": "1485975426.875724", "ts": "1485975426.000003", "item": {"type": "message", "ts": "1485975421.000002", "channel": "D3ZEQULHZ"}, "user": "U407ABLLW", "item_user": "U407ABLLW", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "reaction_added"}
diff --git a/_pytest/data/websocket/1485975428.76-reaction_removed.json b/_pytest/data/websocket/1485975428.76-reaction_removed.json
new file mode 100644
index 0000000..b3b90ac
--- /dev/null
+++ b/_pytest/data/websocket/1485975428.76-reaction_removed.json
@@ -0,0 +1 @@
+{"reaction": "grinning", "event_ts": "1485975428.875759", "ts": "1485975428.000004", "item": {"type": "message", "ts": "1485975421.000002", "channel": "D3ZEQULHZ"}, "user": "U407ABLLW", "item_user": "U407ABLLW", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "reaction_removed"}
diff --git a/_pytest/data/websocket/1485975458.35-channel_created.json b/_pytest/data/websocket/1485975458.35-channel_created.json
new file mode 100644
index 0000000..ca225d8
--- /dev/null
+++ b/_pytest/data/websocket/1485975458.35-channel_created.json
@@ -0,0 +1 @@
+{"event_ts": "1485975458.876318", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "channel_created", "channel": {"is_shared": false, "is_channel": true, "creator": "U407ABLLW", "created": 1485975458, "is_org_shared": false, "id": "C3ZJKCGTU", "name": "some-channel"}}
diff --git a/_pytest/data/websocket/1485975458.87-channel_joined.json b/_pytest/data/websocket/1485975458.87-channel_joined.json
new file mode 100644
index 0000000..8991446
--- /dev/null
+++ b/_pytest/data/websocket/1485975458.87-channel_joined.json
@@ -0,0 +1 @@
+{"event_ts": "1485975458.876336", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "channel_joined", "channel": {"previous_names": [], "is_general": false, "name": "some-channel", "is_channel": true, "created": 1485975458, "is_member": true, "unread_count_display": 0, "is_archived": false, "creator": "U407ABLLW", "topic": {"last_set": 0, "value": "", "creator": ""}, "unread_count": 0, "purpose": {"last_set": 1485975458, "value": "who knows?", "creator": "U407ABLLW"}, "members": ["U3ZKBBDL5", "U407ABLLW"], "last_read": "1485975458.000003", "id": "C3ZJKCGTU", "latest": {"text": "<@U407ABLLW|alice> set the channel purpose: who knows?", "ts": "1485975458.000003", "subtype": "channel_purpose", "purpose": "who knows?", "type": "message", "user": "U407ABLLW"}}}
diff --git a/_pytest/data/websocket/1485975458.87-message.json b/_pytest/data/websocket/1485975458.87-message.json
new file mode 100644
index 0000000..63f611c
--- /dev/null
+++ b/_pytest/data/websocket/1485975458.87-message.json
@@ -0,0 +1 @@
+{"type": "message", "user_profile": {"avatar_hash": "g6f7a5bf7eb7", "first_name": "Second", "image_72": "https://secure.gravatar.com/avatar/6f7a5bf7eb782853afb1d33f28ca9ae7.jpg?s=72&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0019-72.png", "name": "bob", "real_name": "Second Testuser"}, "text": "<@U3ZKBBDL5|bob> has joined the channel", "ts": "1485975458.000004", "subtype": "channel_join", "user": "U3ZKBBDL5", "team": "T3YS5EAL9", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "inviter": "U407ABLLW", "channel": "C3ZJKCGTU"}
diff --git a/_pytest/data/websocket/1485975462.62-update_thread_state.json b/_pytest/data/websocket/1485975462.62-update_thread_state.json
new file mode 100644
index 0000000..7a5411a
--- /dev/null
+++ b/_pytest/data/websocket/1485975462.62-update_thread_state.json
@@ -0,0 +1 @@
+{"mention_count": 0, "event_ts": "1485975458.876340", "timestamp": "1485975458.741354", "has_unreads": false, "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "update_thread_state"}
diff --git a/_pytest/data/websocket/1485975462.62-user_typing.json b/_pytest/data/websocket/1485975462.62-user_typing.json
new file mode 100644
index 0000000..ec19e94
--- /dev/null
+++ b/_pytest/data/websocket/1485975462.62-user_typing.json
@@ -0,0 +1 @@
+{"wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "user_typing", "user": "U407ABLLW", "channel": "C3ZJKCGTU"}
diff --git a/_pytest/data/websocket/1485975476.62-channel_deleted.json b/_pytest/data/websocket/1485975476.62-channel_deleted.json
new file mode 100644
index 0000000..8dd1cb8
--- /dev/null
+++ b/_pytest/data/websocket/1485975476.62-channel_deleted.json
@@ -0,0 +1 @@
+{"event_ts": "1485975476.876638", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "channel_deleted", "channel": "C3ZJKCGTU"}
diff --git a/_pytest/data/websocket/1485975476.86-group_join.json b/_pytest/data/websocket/1485975476.86-group_join.json
new file mode 100644
index 0000000..dc72eeb
--- /dev/null
+++ b/_pytest/data/websocket/1485975476.86-group_join.json
@@ -0,0 +1 @@
+{"wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "group_join", "user": "U407ABLLW", "channel": "G409GKN9M", "ts": "1485975476.000002"}
diff --git a/_pytest/data/websocket/1485975476.86-group_joined.json b/_pytest/data/websocket/1485975476.86-group_joined.json
new file mode 100644
index 0000000..bbd971c
--- /dev/null
+++ b/_pytest/data/websocket/1485975476.86-group_joined.json
@@ -0,0 +1 @@
+{"event_ts": "1485975476.876646", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "group_joined", "channel": {"name": "some-channel", "created": 1485975458, "is_mpim": false, "is_archived": false, "creator": "U407ABLLW", "is_group": true, "topic": {"last_set": 1485975476, "value": "", "creator": "U407ABLLW"}, "purpose": {"last_set": 1485975476, "value": "who knows?", "creator": "U407ABLLW"}, "members": ["U407ABLLW", "U3ZKBBDL5"], "is_starred": false, "id": "G409GKN9M"}}
diff --git a/_pytest/data/websocket/1485975487.69-group_history_changed.json b/_pytest/data/websocket/1485975487.69-group_history_changed.json
new file mode 100644
index 0000000..0196401
--- /dev/null
+++ b/_pytest/data/websocket/1485975487.69-group_history_changed.json
@@ -0,0 +1 @@
+{"event_ts": "1485975476.876650", "is_mpim": false, "ts": "1485975476.000004", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "group_history_changed", "channel": "G409GKN9M", "latest": "1485975476.876632"}
diff --git a/_pytest/data/websocket/1485975487.69-group_join.json b/_pytest/data/websocket/1485975487.69-group_join.json
new file mode 100644
index 0000000..0fdb54c
--- /dev/null
+++ b/_pytest/data/websocket/1485975487.69-group_join.json
@@ -0,0 +1 @@
+{"wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "group_join", "user": "U3ZKBBDL5", "channel": "G409GKN9M", "ts": "1485975476.000003"}
diff --git a/_pytest/data/websocket/1485975493.19-group_joined.json b/_pytest/data/websocket/1485975493.19-group_joined.json
new file mode 100644
index 0000000..76e7b55
--- /dev/null
+++ b/_pytest/data/websocket/1485975493.19-group_joined.json
@@ -0,0 +1 @@
+{"event_ts": "1485975492.876964", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "group_joined", "channel": {"name": "some-private-channel", "last_read": "1485975492.000002", "creator": "U407ABLLW", "is_mpim": false, "is_archived": false, "created": 1485975492, "is_group": true, "topic": {"last_set": 0, "value": "", "creator": ""}, "unread_count": 0, "is_open": true, "purpose": {"last_set": 0, "value": "", "creator": ""}, "members": ["U3ZKBBDL5", "U407ABLLW"], "latest": {"text": "<@U407ABLLW|alice> has joined the group", "subtype": "group_join", "type": "message", "user": "U407ABLLW", "ts": "1485975492.000002"}, "id": "G3ZJKP7GA", "unread_count_display": 0}}
diff --git a/_pytest/data/websocket/1485975493.23-message.json b/_pytest/data/websocket/1485975493.23-message.json
new file mode 100644
index 0000000..c6dad6c
--- /dev/null
+++ b/_pytest/data/websocket/1485975493.23-message.json
@@ -0,0 +1 @@
+{"type": "message", "user_profile": {"avatar_hash": "g6f7a5bf7eb7", "first_name": "Second", "image_72": "https://secure.gravatar.com/avatar/6f7a5bf7eb782853afb1d33f28ca9ae7.jpg?s=72&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0019-72.png", "name": "bob", "real_name": "Second Testuser"}, "text": "<@U3ZKBBDL5|bob> has joined the group", "ts": "1485975492.000003", "subtype": "group_join", "user": "U3ZKBBDL5", "team": "T3YS5EAL9", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "inviter": "U407ABLLW", "channel": "G3ZJKP7GA"}
diff --git a/_pytest/data/websocket/1485975547.75-message.json b/_pytest/data/websocket/1485975547.75-message.json
new file mode 100644
index 0000000..4826952
--- /dev/null
+++ b/_pytest/data/websocket/1485975547.75-message.json
@@ -0,0 +1 @@
+{"user_profile": {"avatar_hash": "gfd0ce7a168d", "first_name": "First", "image_72": "https://secure.gravatar.com/avatar/fd0ce7a168dc2235d21c53c2c1cfee0c.jpg?s=72&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0024-72.png", "name": "alice", "real_name": "First Testuser"}, "text": "<@U407ABLLW|alice> set the channel's purpose: seekret", "ts": "1485975492.000004", "subtype": "group_purpose", "user": "U407ABLLW", "team": "T3YS5EAL9", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "message", "channel": "G3ZJKP7GA", "purpose": "seekret"}
diff --git a/_pytest/data/websocket/1485975547.75-update_thread_state.json b/_pytest/data/websocket/1485975547.75-update_thread_state.json
new file mode 100644
index 0000000..4d91b20
--- /dev/null
+++ b/_pytest/data/websocket/1485975547.75-update_thread_state.json
@@ -0,0 +1 @@
+{"mention_count": 0, "event_ts": "1485975492.876967", "timestamp": "1485975493.013414", "has_unreads": false, "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "update_thread_state"}
diff --git a/_pytest/data/websocket/1485975547.83-reconnect_url.json b/_pytest/data/websocket/1485975547.83-reconnect_url.json
new file mode 100644
index 0000000..42da6c5
--- /dev/null
+++ b/_pytest/data/websocket/1485975547.83-reconnect_url.json
@@ -0,0 +1 @@
+{"url": "wss://mpmulti-t63z.slack-msgs.com/websocket/I4VgKhpT11zUwKt_Wajw_phIbuzM7xTy0V0DVLb6dyPDkqGU5-497Jn3bOGncotPX4QhfZepKVtlUkrSWCqMcbjH_vjRo91HBvzvk4nJoScjn6KJatZ6vpzokG44Ee-vNMsxXLeedcbraFAzCrLlODGRqvdqxUQDQFMVES0XHP8=", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "reconnect_url"}
diff --git a/_pytest/data/websocket/1485975606.59-team_join.json b/_pytest/data/websocket/1485975606.59-team_join.json
new file mode 100644
index 0000000..3af908e
--- /dev/null
+++ b/_pytest/data/websocket/1485975606.59-team_join.json
@@ -0,0 +1 @@
+{"event_ts": "1485975606.879253", "cache_ts": 1485975606, "type": "team_join", "user": {"status": null, "profile": {"first_name": "Chuck", "last_name": "Testuser", "fields": null, "real_name": "Chuck Testuser", "image_24": "https://secure.gravatar.com/avatar/7f87f7015f8e5081190ece053e41b11e.jpg?s=24&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0010-24.png", "real_name_normalized": "Chuck Testuser", "image_512": "https://secure.gravatar.com/avatar/7f87f7015f8e5081190ece053e41b11e.jpg?s=512&d=https%3A%2F%2Fa.slack-edge.com%2F7fa9%2Fimg%2Favatars%2Fava_0010-512.png", "image_32": "https://secure.gravatar.com/avatar/7f87f7015f8e5081190ece053e41b11e.jpg?s=32&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0010-32.png", "image_48": "https://secure.gravatar.com/avatar/7f87f7015f8e5081190ece053e41b11e.jpg?s=48&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0010-48.png", "avatar_hash": "g7f87f7015f8", "image_72": "https://secure.gravatar.com/avatar/7f87f7015f8e5081190ece053e41b11e.jpg?s=72&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0010-72.png", "image_192": "https://secure.gravatar.com/avatar/7f87f7015f8e5081190ece053e41b11e.jpg?s=192&d=https%3A%2F%2Fa.slack-edge.com%2F7fa9%2Fimg%2Favatars%2Fava_0010-192.png"}, "tz": "America/Los_Angeles", "name": "chuck", "presence": "away", "deleted": false, "is_bot": false, "tz_label": "Pacific Standard Time", "real_name": "Chuck Testuser", "color": "e7392d", "team_id": "T3YS5EAL9", "is_admin": false, "is_ultra_restricted": false, "is_restricted": false, "tz_offset": -28800, "is_primary_owner": false, "id": "U4096CBHC", "is_owner": false}, "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}}
diff --git a/_pytest/data/websocket/1485975606.67-message.json b/_pytest/data/websocket/1485975606.67-message.json
new file mode 100644
index 0000000..cfc8e5f
--- /dev/null
+++ b/_pytest/data/websocket/1485975606.67-message.json
@@ -0,0 +1 @@
+{"user_profile": {"avatar_hash": "g7f87f7015f8", "first_name": "Chuck", "image_72": "https://secure.gravatar.com/avatar/7f87f7015f8e5081190ece053e41b11e.jpg?s=72&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0010-72.png", "name": "chuck", "real_name": "Chuck Testuser"}, "text": "<@U4096CBHC|chuck> has joined the channel", "ts": "1485975606.000003", "subtype": "channel_join", "user": "U4096CBHC", "team": "T3YS5EAL9", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "message", "channel": "C407ABS94"}
diff --git a/_pytest/data/websocket/1485975606.75-im_created.json b/_pytest/data/websocket/1485975606.75-im_created.json
new file mode 100644
index 0000000..fa360d8
--- /dev/null
+++ b/_pytest/data/websocket/1485975606.75-im_created.json
@@ -0,0 +1 @@
+{"event_ts": "1485975606.879278", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "im_created", "user": "U4096CBHC", "channel": {"last_read": "0000000000.000000", "created": 1485975606, "is_org_shared": false, "unread_count": 0, "is_open": false, "user": "U4096CBHC", "unread_count_display": 0, "is_im": true, "id": "D409J34CF", "latest": null}}
diff --git a/_pytest/data/websocket/1485975606.75-im_open.json b/_pytest/data/websocket/1485975606.75-im_open.json
new file mode 100644
index 0000000..f2a9696
--- /dev/null
+++ b/_pytest/data/websocket/1485975606.75-im_open.json
@@ -0,0 +1 @@
+{"event_ts": "1485975606.879280", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "im_open", "user": "U4096CBHC", "channel": "D409J34CF"}
diff --git a/_pytest/data/websocket/1485975611.29-presence_change.json b/_pytest/data/websocket/1485975611.29-presence_change.json
new file mode 100644
index 0000000..ec41e5f
--- /dev/null
+++ b/_pytest/data/websocket/1485975611.29-presence_change.json
@@ -0,0 +1 @@
+{"wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "presence_change", "user": "U4096CBHC", "presence": "active"}
diff --git a/_pytest/data/websocket/1485975640.5-user_change.json b/_pytest/data/websocket/1485975640.5-user_change.json
new file mode 100644
index 0000000..9c2b114
--- /dev/null
+++ b/_pytest/data/websocket/1485975640.5-user_change.json
@@ -0,0 +1 @@
+{"event_ts": "1485975640.879906", "cache_ts": 1485975640, "type": "user_change", "user": {"status": null, "profile": {"first_name": "Charles", "last_name": "Testuser", "fields": [], "real_name": "Charles Testuser", "image_24": "https://secure.gravatar.com/avatar/7f87f7015f8e5081190ece053e41b11e.jpg?s=24&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0010-24.png", "real_name_normalized": "Charles Testuser", "image_512": "https://secure.gravatar.com/avatar/7f87f7015f8e5081190ece053e41b11e.jpg?s=512&d=https%3A%2F%2Fa.slack-edge.com%2F7fa9%2Fimg%2Favatars%2Fava_0010-512.png", "image_32": "https://secure.gravatar.com/avatar/7f87f7015f8e5081190ece053e41b11e.jpg?s=32&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0010-32.png", "image_48": "https://secure.gravatar.com/avatar/7f87f7015f8e5081190ece053e41b11e.jpg?s=48&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0010-48.png", "avatar_hash": "g7f87f7015f8", "image_72": "https://secure.gravatar.com/avatar/7f87f7015f8e5081190ece053e41b11e.jpg?s=72&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0010-72.png", "image_192": "https://secure.gravatar.com/avatar/7f87f7015f8e5081190ece053e41b11e.jpg?s=192&d=https%3A%2F%2Fa.slack-edge.com%2F7fa9%2Fimg%2Favatars%2Fava_0010-192.png"}, "tz": "America/Los_Angeles", "name": "chuck", "deleted": false, "is_bot": false, "tz_label": "Pacific Standard Time", "real_name": "Charles Testuser", "color": "e7392d", "team_id": "T3YS5EAL9", "is_admin": false, "is_ultra_restricted": false, "is_restricted": false, "tz_offset": -28800, "is_primary_owner": false, "id": "U4096CBHC", "is_owner": false}, "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}}
diff --git a/_pytest/data/websocket/1485975669.14-user_change.json b/_pytest/data/websocket/1485975669.14-user_change.json
new file mode 100644
index 0000000..0561553
--- /dev/null
+++ b/_pytest/data/websocket/1485975669.14-user_change.json
@@ -0,0 +1 @@
+{"event_ts": "1485975668.880329", "cache_ts": 1485975669, "type": "user_change", "user": {"status": null, "profile": {"first_name": "Charles", "last_name": "Testuser", "fields": [], "real_name": "Charles Testuser", "image_24": "https://secure.gravatar.com/avatar/7f87f7015f8e5081190ece053e41b11e.jpg?s=24&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0010-24.png", "real_name_normalized": "Charles Testuser", "image_512": "https://secure.gravatar.com/avatar/7f87f7015f8e5081190ece053e41b11e.jpg?s=512&d=https%3A%2F%2Fa.slack-edge.com%2F7fa9%2Fimg%2Favatars%2Fava_0010-512.png", "image_32": "https://secure.gravatar.com/avatar/7f87f7015f8e5081190ece053e41b11e.jpg?s=32&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0010-32.png", "image_48": "https://secure.gravatar.com/avatar/7f87f7015f8e5081190ece053e41b11e.jpg?s=48&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0010-48.png", "avatar_hash": "g7f87f7015f8", "image_72": "https://secure.gravatar.com/avatar/7f87f7015f8e5081190ece053e41b11e.jpg?s=72&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0010-72.png", "image_192": "https://secure.gravatar.com/avatar/7f87f7015f8e5081190ece053e41b11e.jpg?s=192&d=https%3A%2F%2Fa.slack-edge.com%2F7fa9%2Fimg%2Favatars%2Fava_0010-192.png"}, "tz": "America/Los_Angeles", "name": "charles", "deleted": false, "is_bot": false, "tz_label": "Pacific Standard Time", "real_name": "Charles Testuser", "color": "e7392d", "team_id": "T3YS5EAL9", "is_admin": false, "is_ultra_restricted": false, "is_restricted": false, "tz_offset": -28800, "is_primary_owner": false, "id": "U4096CBHC", "is_owner": false}, "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}}
diff --git a/_pytest/data/websocket/1485975675.81-presence_change.json b/_pytest/data/websocket/1485975675.81-presence_change.json
new file mode 100644
index 0000000..ec41e5f
--- /dev/null
+++ b/_pytest/data/websocket/1485975675.81-presence_change.json
@@ -0,0 +1 @@
+{"wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "presence_change", "user": "U4096CBHC", "presence": "active"}
diff --git a/_pytest/data/websocket/1485975690.59-mpim_joined.json b/_pytest/data/websocket/1485975690.59-mpim_joined.json
new file mode 100644
index 0000000..d377778
--- /dev/null
+++ b/_pytest/data/websocket/1485975690.59-mpim_joined.json
@@ -0,0 +1 @@
+{"event_ts": "1485975690.880722", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "mpim_joined", "channel": {"name": "mpdm-bob--alice--charles-1", "last_read": "0000000000.000000", "creator": "U407ABLLW", "is_mpim": true, "is_archived": false, "created": 1485975690, "is_group": true, "topic": {"last_set": 1485975690, "value": "Group messaging", "creator": "U407ABLLW"}, "unread_count": 0, "is_open": false, "purpose": {"last_set": 1485975690, "value": "Group messaging with: @bob @alice @charles", "creator": "U407ABLLW"}, "members": ["U407ABLLW", "U3ZKBBDL5", "U4096CBHC"], "latest": null, "id": "G3ZGMF4RZ", "unread_count_display": 0}}
diff --git a/_pytest/data/websocket/1485975690.67-group_joined.json b/_pytest/data/websocket/1485975690.67-group_joined.json
new file mode 100644
index 0000000..b1c27dd
--- /dev/null
+++ b/_pytest/data/websocket/1485975690.67-group_joined.json
@@ -0,0 +1 @@
+{"event_ts": "1485975690.880723", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "group_joined", "channel": {"name": "mpdm-bob--alice--charles-1", "last_read": "0000000000.000000", "creator": "U407ABLLW", "is_mpim": true, "is_archived": false, "created": 1485975690, "is_group": true, "topic": {"last_set": 1485975690, "value": "Group messaging", "creator": "U407ABLLW"}, "unread_count": 0, "is_open": false, "purpose": {"last_set": 1485975690, "value": "Group messaging with: @bob @alice @charles", "creator": "U407ABLLW"}, "members": ["U407ABLLW", "U3ZKBBDL5", "U4096CBHC"], "latest": null, "id": "G3ZGMF4RZ", "unread_count_display": 0}}
diff --git a/_pytest/data/websocket/1485975690.67-update_thread_state.json b/_pytest/data/websocket/1485975690.67-update_thread_state.json
new file mode 100644
index 0000000..c745df6
--- /dev/null
+++ b/_pytest/data/websocket/1485975690.67-update_thread_state.json
@@ -0,0 +1 @@
+{"mention_count": 0, "event_ts": "1485975690.880724", "timestamp": "1485975690.537094", "has_unreads": false, "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "update_thread_state"}
diff --git a/_pytest/data/websocket/1485975697.58-user_typing.json b/_pytest/data/websocket/1485975697.58-user_typing.json
new file mode 100644
index 0000000..b8239d4
--- /dev/null
+++ b/_pytest/data/websocket/1485975697.58-user_typing.json
@@ -0,0 +1 @@
+{"wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "user_typing", "user": "U407ABLLW", "channel": "G3ZGMF4RZ"}
diff --git a/_pytest/data/websocket/1485975698.45-message.json b/_pytest/data/websocket/1485975698.45-message.json
new file mode 100644
index 0000000..2118ec7
--- /dev/null
+++ b/_pytest/data/websocket/1485975698.45-message.json
@@ -0,0 +1 @@
+{"text": "surely", "ts": "1485975698.000002", "user": "U407ABLLW", "team": "T3YS5EAL9", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "message", "channel": "G3ZGMF4RZ"}
diff --git a/_pytest/data/websocket/1485975701.72-reaction_added.json b/_pytest/data/websocket/1485975701.72-reaction_added.json
new file mode 100644
index 0000000..b4d2282
--- /dev/null
+++ b/_pytest/data/websocket/1485975701.72-reaction_added.json
@@ -0,0 +1 @@
+{"reaction": "unamused", "event_ts": "1485975701.880957", "ts": "1485975701.000003", "item": {"type": "message", "ts": "1485975698.000002", "channel": "G3ZGMF4RZ"}, "user": "U407ABLLW", "item_user": "U407ABLLW", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "reaction_added"}
diff --git a/_pytest/data/websocket/1485975703.22-reaction_removed.json b/_pytest/data/websocket/1485975703.22-reaction_removed.json
new file mode 100644
index 0000000..3deafb2
--- /dev/null
+++ b/_pytest/data/websocket/1485975703.22-reaction_removed.json
@@ -0,0 +1 @@
+{"reaction": "unamused", "event_ts": "1485975703.880979", "ts": "1485975703.000004", "item": {"type": "message", "ts": "1485975698.000002", "channel": "G3ZGMF4RZ"}, "user": "U407ABLLW", "item_user": "U407ABLLW", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "reaction_removed"}
diff --git a/_pytest/data/websocket/1485975715.79-message.json b/_pytest/data/websocket/1485975715.79-message.json
new file mode 100644
index 0000000..5d3c51c
--- /dev/null
+++ b/_pytest/data/websocket/1485975715.79-message.json
@@ -0,0 +1 @@
+{"username": "<@U407ABLLW|alice>", "source_team": "T3YS5EAL9", "user_profile": {"avatar_hash": "gfd0ce7a168d", "first_name": "First", "image_72": "https://secure.gravatar.com/avatar/fd0ce7a168dc2235d21c53c2c1cfee0c.jpg?s=72&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0024-72.png", "name": "alice", "real_name": "First Testuser"}, "text": "<@U407ABLLW|alice> uploaded a file: <https://weeslacktest.slack.com/files/alice/F3ZLY6K5J/-.txt|Untitled>", "team": "T3YS5EAL9", "upload": true, "ts": "1485975715.000005", "display_as_bot": false, "user": "U407ABLLW", "file": {"filetype": "text", "lines_more": 0, "channels": [], "display_as_bot": false, "id": "F3ZLY6K5J", "size": 14, "title": "Untitled", "url_private": "https://files.slack.com/files-pri/T3YS5EAL9-F3ZLY6K5J/-.txt", "ims": [], "preview": "some code here", "external_type": "", "edit_link": "https://weeslacktest.slack.com/files/alice/F3ZLY6K5J/-.txt/edit", "username": "", "timestamp": 1485975715, "public_url_shared": false, "editable": true, "preview_highlight": "<div class=\"CodeMirror cm-s-default CodeMirrorServer\" oncopy=\"if(event.clipboardData){event.clipboardData.setData('text/plain',window.getSelection().toString().replace(/\\u200b/g,''));event.preventDefault();event.stopPropagation();}\">\n<div class=\"CodeMirror-code\">\n<div><pre>some code here</pre></div>\n</div>\n</div>\n", "url_private_download": "https://files.slack.com/files-pri/T3YS5EAL9-F3ZLY6K5J/download/-.txt", "user": "U407ABLLW", "groups": [], "is_public": false, "pretty_type": "Plain Text", "is_external": false, "mimetype": "text/plain", "permalink_public": "https://slack-files.com/T3YS5EAL9-F3ZLY6K5J-39c2c4f739", "permalink": "https://weeslacktest.slack.com/files/alice/F3ZLY6K5J/-.txt", "name": "-.txt", "created": 1485975715, "lines": 1, "comments_count": 0, "mode": "snippet", "preview_is_truncated": false}, "subtype": "file_share", "user_team": "T3YS5EAL9", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "message", "channel": "G3ZGMF4RZ", "bot_id": null}
diff --git a/_pytest/data/websocket/1485975715.87-file_shared.json b/_pytest/data/websocket/1485975715.87-file_shared.json
new file mode 100644
index 0000000..526f72c
--- /dev/null
+++ b/_pytest/data/websocket/1485975715.87-file_shared.json
@@ -0,0 +1 @@
+{"user_id": "U407ABLLW", "event_ts": "1485975715.881302", "file_id": "F3ZLY6K5J", "file": {"id": "F3ZLY6K5J"}, "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "file_shared"}
diff --git a/_pytest/data/websocket/1485975723.85-message.json b/_pytest/data/websocket/1485975723.85-message.json
new file mode 100644
index 0000000..d06d025
--- /dev/null
+++ b/_pytest/data/websocket/1485975723.85-message.json
@@ -0,0 +1 @@
+{"event_ts": "1485975723.881563", "ts": "1485975723.000006", "subtype": "message_deleted", "hidden": true, "previous_message": {"text": "surely", "type": "message", "user": "U407ABLLW", "ts": "1485975698.000002"}, "type": "message", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "channel": "G3ZGMF4RZ", "deleted_ts": "1485975698.000002"}
diff --git a/_pytest/data/websocket/1485975727.84-reconnect_url.json b/_pytest/data/websocket/1485975727.84-reconnect_url.json
new file mode 100644
index 0000000..3630d02
--- /dev/null
+++ b/_pytest/data/websocket/1485975727.84-reconnect_url.json
@@ -0,0 +1 @@
+{"url": "wss://mpmulti-824w.slack-msgs.com/websocket/Pl7CWLLi3w1S0gHESj79_JySeV0ho6vZRl1usylFoDVBWTj332mKkaLBR6OdrDqRDlYPnwwnh9TqlqOMVajfyHMH_Q02oVU14YJS_ao5_nEJBBd58vDB3XkKfGdPRZNgP2Id05xTpEy9izt9EF_BdDaDaTU9nGc5c2ggnuS9gos=", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "reconnect_url"}
diff --git a/_pytest/data/websocket/1485975735.91-group_archive.json b/_pytest/data/websocket/1485975735.91-group_archive.json
new file mode 100644
index 0000000..61cfdc7
--- /dev/null
+++ b/_pytest/data/websocket/1485975735.91-group_archive.json
@@ -0,0 +1 @@
+{"event_ts": "1485975735.881780", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "group_archive", "ts": "1485975735.000005", "channel": "G409GKN9M"}
diff --git a/_pytest/data/websocket/1485975735.99-message.json b/_pytest/data/websocket/1485975735.99-message.json
new file mode 100644
index 0000000..10f541f
--- /dev/null
+++ b/_pytest/data/websocket/1485975735.99-message.json
@@ -0,0 +1 @@
+{"user_profile": {"avatar_hash": "gfd0ce7a168d", "first_name": "First", "image_72": "https://secure.gravatar.com/avatar/fd0ce7a168dc2235d21c53c2c1cfee0c.jpg?s=72&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0024-72.png", "name": "alice", "real_name": "First Testuser"}, "text": "<@U407ABLLW|alice> archived the private channel", "ts": "1485975735.000006", "subtype": "group_archive", "user": "U407ABLLW", "team": "T3YS5EAL9", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "message", "channel": "G409GKN9M"}
diff --git a/_pytest/data/websocket/1485975736.08-message.json b/_pytest/data/websocket/1485975736.08-message.json
new file mode 100644
index 0000000..147dbb4
--- /dev/null
+++ b/_pytest/data/websocket/1485975736.08-message.json
@@ -0,0 +1 @@
+{"text": "<@U407ABLLW|alice> archived the private channel <https://weeslacktest.slack.com/archives/some-channel|some-channel>", "ts": "1485975735.000002", "user": "USLACKBOT", "team": "T3YS5EAL9", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "message", "channel": "D3ZK1D8JY"}
diff --git a/_pytest/data/websocket/1485975738.1-desktop_notification.json b/_pytest/data/websocket/1485975738.1-desktop_notification.json
new file mode 100644
index 0000000..da451bb
--- /dev/null
+++ b/_pytest/data/websocket/1485975738.1-desktop_notification.json
@@ -0,0 +1 @@
+{"avatarImage": "https://a.slack-edge.com/66f9/img/slackbot_192.png", "subtitle": "slackbot", "is_shared": false, "title": "weeslacktest", "ssbFilename": "knock_brush.mp3", "imageUri": null, "launchUri": "slack://channel?id=D3ZK1D8JY&message=1485975735000002&team=T3YS5EAL9", "event_ts": "1485975737.881829", "msg": "1485975735.000002", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "content": "@alice archived the private channel some-channel", "type": "desktop_notification", "channel": "D3ZK1D8JY"}
diff --git a/_pytest/data/websocket/1485975747.17-channel_created.json b/_pytest/data/websocket/1485975747.17-channel_created.json
new file mode 100644
index 0000000..6bcafdb
--- /dev/null
+++ b/_pytest/data/websocket/1485975747.17-channel_created.json
@@ -0,0 +1 @@
+{"event_ts": "1485975746.882009", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "channel_created", "channel": {"is_shared": false, "is_channel": true, "creator": "U407ABLLW", "created": 1485975747, "is_org_shared": false, "id": "C3ZM8JTD3", "name": "some-channel2"}}
diff --git a/_pytest/data/websocket/1485975757.63-channel_rename.json b/_pytest/data/websocket/1485975757.63-channel_rename.json
new file mode 100644
index 0000000..a921fed
--- /dev/null
+++ b/_pytest/data/websocket/1485975757.63-channel_rename.json
@@ -0,0 +1 @@
+{"event_ts": "1485975757.882178", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "channel_rename", "channel": {"name": "some-channel2-renamed", "id": "C3ZM8JTD3", "is_channel": true, "created": "1485975747"}}
diff --git a/_pytest/data/websocket/1485975771.6-presence_change.json b/_pytest/data/websocket/1485975771.6-presence_change.json
new file mode 100644
index 0000000..e70421c
--- /dev/null
+++ b/_pytest/data/websocket/1485975771.6-presence_change.json
@@ -0,0 +1 @@
+{"wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "presence_change", "user": "U407ABLLW", "presence": "away"}
diff --git a/_pytest/data/websocket/1485975774.62-presence_change.json b/_pytest/data/websocket/1485975774.62-presence_change.json
new file mode 100644
index 0000000..9728fae
--- /dev/null
+++ b/_pytest/data/websocket/1485975774.62-presence_change.json
@@ -0,0 +1 @@
+{"wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "presence_change", "user": "U407ABLLW", "presence": "active"}
diff --git a/_pytest/data/websocket/1485975822.17-user_typing.json b/_pytest/data/websocket/1485975822.17-user_typing.json
new file mode 100644
index 0000000..df57e72
--- /dev/null
+++ b/_pytest/data/websocket/1485975822.17-user_typing.json
@@ -0,0 +1 @@
+{"wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "user_typing", "user": "U407ABLLW", "channel": "C407ABS94"}
diff --git a/_pytest/data/websocket/1485975824.48-message.json b/_pytest/data/websocket/1485975824.48-message.json
new file mode 100644
index 0000000..9fae542
--- /dev/null
+++ b/_pytest/data/websocket/1485975824.48-message.json
@@ -0,0 +1 @@
+{"text": "generally, yep!", "ts": "1485975824.000004", "user": "U407ABLLW", "team": "T3YS5EAL9", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "message", "channel": "C407ABS94"}
diff --git a/_pytest/data/websocket/1485975836.23-message.json b/_pytest/data/websocket/1485975836.23-message.json
new file mode 100644
index 0000000..6715bed
--- /dev/null
+++ b/_pytest/data/websocket/1485975836.23-message.json
@@ -0,0 +1 @@
+{"thread_ts": "1485975824.000004", "text": "have you met my friend threads?", "ts": "1485975835.000005", "user": "U407ABLLW", "team": "T3YS5EAL9", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "message", "channel": "C407ABS94"}
diff --git a/_pytest/data/websocket/1485975836.31-message.json b/_pytest/data/websocket/1485975836.31-message.json
new file mode 100644
index 0000000..3823c61
--- /dev/null
+++ b/_pytest/data/websocket/1485975836.31-message.json
@@ -0,0 +1 @@
+{"hidden": true, "event_ts": "1485975835.883772", "ts": "1485975835.000006", "subtype": "message_replied", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "message", "message": {"thread_ts": "1485975824.000004", "text": "generally, yep!", "ts": "1485975824.000004", "reply_count": 1, "user": "U407ABLLW", "replies": [{"user": "U407ABLLW", "ts": "1485975835.000005"}], "type": "message"}, "channel": "C407ABS94"}
diff --git a/_pytest/data/websocket/1485975842.1-message.json b/_pytest/data/websocket/1485975842.1-message.json
new file mode 100644
index 0000000..b94a913
--- /dev/null
+++ b/_pytest/data/websocket/1485975842.1-message.json
@@ -0,0 +1 @@
+{"thread_ts": "1485975824.000004", "text": "react if yes", "ts": "1485975841.000007", "user": "U407ABLLW", "team": "T3YS5EAL9", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "message", "channel": "C407ABS94"}
diff --git a/_pytest/data/websocket/1485975842.18-message.json b/_pytest/data/websocket/1485975842.18-message.json
new file mode 100644
index 0000000..7bc2b38
--- /dev/null
+++ b/_pytest/data/websocket/1485975842.18-message.json
@@ -0,0 +1 @@
+{"hidden": true, "event_ts": "1485975841.883922", "ts": "1485975841.000008", "subtype": "message_replied", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "message", "message": {"thread_ts": "1485975824.000004", "text": "generally, yep!", "ts": "1485975824.000004", "reply_count": 2, "user": "U407ABLLW", "replies": [{"user": "U407ABLLW", "ts": "1485975835.000005"}, {"user": "U407ABLLW", "ts": "1485975841.000007"}], "type": "message"}, "channel": "C407ABS94"}
diff --git a/_pytest/data/websocket/1485975850.32-mpim_open.json b/_pytest/data/websocket/1485975850.32-mpim_open.json
new file mode 100644
index 0000000..ebce13e
--- /dev/null
+++ b/_pytest/data/websocket/1485975850.32-mpim_open.json
@@ -0,0 +1 @@
+{"event_ts": "1485975849.884091", "is_mpim": true, "user": "U3ZKBBDL5", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "mpim_open", "channel": "G3ZGMF4RZ"}
diff --git a/_pytest/data/websocket/1485975850.45-group_open.json b/_pytest/data/websocket/1485975850.45-group_open.json
new file mode 100644
index 0000000..beb27d3
--- /dev/null
+++ b/_pytest/data/websocket/1485975850.45-group_open.json
@@ -0,0 +1 @@
+{"event_ts": "1485975849.884092", "is_mpim": true, "user": "U3ZKBBDL5", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "group_open", "channel": "G3ZGMF4RZ"}
diff --git a/_pytest/data/websocket/1485975858.56-reaction_added.json b/_pytest/data/websocket/1485975858.56-reaction_added.json
new file mode 100644
index 0000000..978d30f
--- /dev/null
+++ b/_pytest/data/websocket/1485975858.56-reaction_added.json
@@ -0,0 +1 @@
+{"reaction": "stuck_out_tongue", "event_ts": "1485975858.884268", "item": {"type": "message", "ts": "1485975841.000007", "channel": "C407ABS94"}, "user": "U4096CBHC", "item_user": "U407ABLLW", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "reaction_added"}
diff --git a/_pytest/data/websocket/1485975890.7-bot_added.json b/_pytest/data/websocket/1485975890.7-bot_added.json
new file mode 100644
index 0000000..91254cf
--- /dev/null
+++ b/_pytest/data/websocket/1485975890.7-bot_added.json
@@ -0,0 +1 @@
+{"event_ts": "1485975890.885679", "type": "bot_added", "cache_ts": 1485975890, "bot": {"deleted": false, "id": "B3YTBU6L8", "app_id": "A0F7XDUAZ", "name": "incoming-webhook", "icons": {"image_36": "https://a.slack-edge.com/12b5a/plugins/tester/assets/service_36.png", "image_48": "https://a.slack-edge.com/12b5a/plugins/tester/assets/service_48.png", "image_72": "https://a.slack-edge.com/12b5a/plugins/tester/assets/service_72.png"}}, "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}}
diff --git a/_pytest/data/websocket/1485975890.78-message.json b/_pytest/data/websocket/1485975890.78-message.json
new file mode 100644
index 0000000..785faa6
--- /dev/null
+++ b/_pytest/data/websocket/1485975890.78-message.json
@@ -0,0 +1 @@
+{"text": "added an integration to this channel: <https://weeslacktest.slack.com/services/B3YTBU6L8|incoming-webhook>", "ts": "1485975890.000009", "subtype": "bot_add", "user": "U4096CBHC", "team": "T3YS5EAL9", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "message", "channel": "C407ABS94", "bot_id": "B3YTBU6L8"}
diff --git a/_pytest/data/websocket/1485975896.16-message.json b/_pytest/data/websocket/1485975896.16-message.json
new file mode 100644
index 0000000..3a9db89
--- /dev/null
+++ b/_pytest/data/websocket/1485975896.16-message.json
@@ -0,0 +1 @@
+{"text": "disabled an integration in this channel: <https://weeslacktest.slack.com/services/B3YTBU6L8|incoming-webhook>", "ts": "1485975896.000010", "subtype": "bot_disable", "user": "U4096CBHC", "team": "T3YS5EAL9", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "message", "channel": "C407ABS94", "bot_id": "B3YTBU6L8"}
diff --git a/_pytest/data/websocket/1485975907.84-reconnect_url.json b/_pytest/data/websocket/1485975907.84-reconnect_url.json
new file mode 100644
index 0000000..a2f99c1
--- /dev/null
+++ b/_pytest/data/websocket/1485975907.84-reconnect_url.json
@@ -0,0 +1 @@
+{"url": "wss://mpmulti-1r8c.slack-msgs.com/websocket/8atJCMnYZ10YaqNai-zwEaNMfkwh85XwRpc0MYVVOzAojnPcrx8SboF-NBWeep6Hy7arytqEffr_Fh7mPrDagwEGwbOeX-OH3OLlubVjpC2cCLWwm2jN3rEZcq8A4j0tpPP56GZ84jbLn4BKvDhKkkKhRXQhuYtC7kskaXXLTbk=", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "reconnect_url"}
diff --git a/_pytest/data/websocket/1485975926.59-user_change.json b/_pytest/data/websocket/1485975926.59-user_change.json
new file mode 100644
index 0000000..179a67d
--- /dev/null
+++ b/_pytest/data/websocket/1485975926.59-user_change.json
@@ -0,0 +1 @@
+{"event_ts": "1485975926.886492", "cache_ts": 1485975926, "type": "user_change", "user": {"status": null, "profile": {"first_name": "Charles", "last_name": "Testuser", "fields": [], "real_name": "Charles Testuser", "image_24": "https://avatars.slack-edge.com/2017-02-01/135714629508_5af404f7d4b7728d10c0_24.png", "image_original": "https://avatars.slack-edge.com/2017-02-01/135714629508_5af404f7d4b7728d10c0_original.png", "real_name_normalized": "Charles Testuser", "image_512": "https://avatars.slack-edge.com/2017-02-01/135714629508_5af404f7d4b7728d10c0_24.png", "image_32": "https://avatars.slack-edge.com/2017-02-01/135714629508_5af404f7d4b7728d10c0_24.png", "image_48": "https://avatars.slack-edge.com/2017-02-01/135714629508_5af404f7d4b7728d10c0_24.png", "avatar_hash": "5af404f7d4b7", "image_72": "https://avatars.slack-edge.com/2017-02-01/135714629508_5af404f7d4b7728d10c0_24.png", "image_1024": "https://avatars.slack-edge.com/2017-02-01/135714629508_5af404f7d4b7728d10c0_24.png", "image_192": "https://avatars.slack-edge.com/2017-02-01/135714629508_5af404f7d4b7728d10c0_24.png"}, "tz": "America/Los_Angeles", "name": "charles", "deleted": false, "is_bot": false, "tz_label": "Pacific Standard Time", "real_name": "Charles Testuser", "color": "e7392d", "team_id": "T3YS5EAL9", "is_admin": false, "is_ultra_restricted": false, "is_restricted": false, "tz_offset": -28800, "is_primary_owner": false, "id": "U4096CBHC", "is_owner": false}, "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}}
diff --git a/_pytest/data/websocket/1485975930.81-user_change.json b/_pytest/data/websocket/1485975930.81-user_change.json
new file mode 100644
index 0000000..afbbb1b
--- /dev/null
+++ b/_pytest/data/websocket/1485975930.81-user_change.json
@@ -0,0 +1 @@
+{"event_ts": "1485975930.886576", "cache_ts": 1485975930, "type": "user_change", "user": {"status": null, "profile": {"first_name": "Charles", "last_name": "Testuser", "fields": [], "real_name": "Charles Testuser", "image_24": "https://avatars.slack-edge.com/2017-02-01/135714629508_5af404f7d4b7728d10c0_24.png", "image_original": "https://avatars.slack-edge.com/2017-02-01/135714629508_5af404f7d4b7728d10c0_original.png", "real_name_normalized": "Charles Testuser", "image_512": "https://avatars.slack-edge.com/2017-02-01/135714629508_5af404f7d4b7728d10c0_24.png", "image_32": "https://avatars.slack-edge.com/2017-02-01/135714629508_5af404f7d4b7728d10c0_24.png", "image_48": "https://avatars.slack-edge.com/2017-02-01/135714629508_5af404f7d4b7728d10c0_24.png", "avatar_hash": "5af404f7d4b7", "image_72": "https://avatars.slack-edge.com/2017-02-01/135714629508_5af404f7d4b7728d10c0_24.png", "image_1024": "https://avatars.slack-edge.com/2017-02-01/135714629508_5af404f7d4b7728d10c0_24.png", "image_192": "https://avatars.slack-edge.com/2017-02-01/135714629508_5af404f7d4b7728d10c0_24.png"}, "tz": "America/Los_Angeles", "name": "charles", "deleted": false, "is_bot": false, "tz_label": "Pacific Standard Time", "real_name": "Charles Testuser", "color": "e7392d", "team_id": "T3YS5EAL9", "is_admin": false, "is_ultra_restricted": false, "is_restricted": false, "tz_offset": -28800, "is_primary_owner": false, "id": "U4096CBHC", "is_owner": false}, "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}}
diff --git a/_pytest/data/websocket/1485975956.38-file_public.json b/_pytest/data/websocket/1485975956.38-file_public.json
new file mode 100644
index 0000000..288b528
--- /dev/null
+++ b/_pytest/data/websocket/1485975956.38-file_public.json
@@ -0,0 +1 @@
+{"user_id": "U407ABLLW", "event_ts": "1485975956.887078", "file_id": "F3ZJQTA66", "file": {"id": "F3ZJQTA66"}, "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "file_public"}
diff --git a/_pytest/data/websocket/1485975956.47-file_shared.json b/_pytest/data/websocket/1485975956.47-file_shared.json
new file mode 100644
index 0000000..1064ac7
--- /dev/null
+++ b/_pytest/data/websocket/1485975956.47-file_shared.json
@@ -0,0 +1 @@
+{"user_id": "U407ABLLW", "event_ts": "1485975956.887081", "file_id": "F3ZJQTA66", "file": {"id": "F3ZJQTA66"}, "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "file_shared"}
diff --git a/_pytest/data/websocket/1485975956.47-message.json b/_pytest/data/websocket/1485975956.47-message.json
new file mode 100644
index 0000000..04e67a8
--- /dev/null
+++ b/_pytest/data/websocket/1485975956.47-message.json
@@ -0,0 +1 @@
+{"username": "<@U407ABLLW|alice>", "source_team": "T3YS5EAL9", "user_profile": {"avatar_hash": "gfd0ce7a168d", "first_name": "First", "image_72": "https://secure.gravatar.com/avatar/fd0ce7a168dc2235d21c53c2c1cfee0c.jpg?s=72&d=https%3A%2F%2Fa.slack-edge.com%2F66f9%2Fimg%2Favatars%2Fava_0024-72.png", "name": "alice", "real_name": "First Testuser"}, "text": "<@U407ABLLW|alice> uploaded a file: <https://weeslacktest.slack.com/files/alice/F3ZJQTA66/1x1.png|1x1.png>", "team": "T3YS5EAL9", "upload": true, "ts": "1485975956.000011", "display_as_bot": false, "user": "U407ABLLW", "file": {"image_exif_rotation": 1, "filetype": "png", "channels": ["C407ABS94"], "display_as_bot": false, "id": "F3ZJQTA66", "size": 68, "original_h": 1, "thumb_360_w": 1, "title": "1x1.png", "url_private": "https://files.slack.com/files-pri/T3YS5EAL9-F3ZJQTA66/1x1.png", "thumb_360": "https://files.slack.com/files-tmb/T3YS5EAL9-F3ZJQTA66-8ab528dd77/1x1_360.png", "thumb_64": "https://files.slack.com/files-tmb/T3YS5EAL9-F3ZJQTA66-8ab528dd77/1x1_64.png", "ims": [], "thumb_80": "https://files.slack.com/files-tmb/T3YS5EAL9-F3ZJQTA66-8ab528dd77/1x1_80.png", "thumb_360_h": 1, "external_type": "", "username": "", "timestamp": 1485975955, "public_url_shared": false, "editable": false, "thumb_160": "https://files.slack.com/files-tmb/T3YS5EAL9-F3ZJQTA66-8ab528dd77/1x1_160.png", "url_private_download": "https://files.slack.com/files-pri/T3YS5EAL9-F3ZJQTA66/download/1x1.png", "user": "U407ABLLW", "groups": [], "is_public": true, "pretty_type": "PNG", "is_external": false, "mimetype": "image/png", "permalink_public": "https://slack-files.com/T3YS5EAL9-F3ZJQTA66-5d747593d2", "permalink": "https://weeslacktest.slack.com/files/alice/F3ZJQTA66/1x1.png", "name": "1x1.png", "created": 1485975955, "original_w": 1, "comments_count": 0, "mode": "hosted"}, "subtype": "file_share", "user_team": "T3YS5EAL9", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "message", "channel": "C407ABS94", "bot_id": null}
diff --git a/_pytest/data/websocket/1485975978.79-message.json b/_pytest/data/websocket/1485975978.79-message.json
new file mode 100644
index 0000000..f7c1846
--- /dev/null
+++ b/_pytest/data/websocket/1485975978.79-message.json
@@ -0,0 +1 @@
+{"username": "<@U407ABLLW|alice>", "subtype": "file_share", "text": "<@U407ABLLW|alice> shared a file: <https://weeslacktest.slack.com/files/alice/F3YTCL8TA/some_post_here|some post here>", "upload": false, "ts": "1485975978.000012", "display_as_bot": false, "user": "U407ABLLW", "file": {"filetype": "space", "channels": ["C407ABS94"], "display_as_bot": false, "id": "F3YTCL8TA", "size": 73, "title": "some post here", "url_private": "https://files.slack.com/files-pri/T3YS5EAL9-F3YTCL8TA/some_post_here", "ims": [], "state": "locked", "editor": "U407ABLLW", "preview": null, "external_type": "", "username": "", "updated": 1485975959, "timestamp": 1485975967, "public_url_shared": false, "editable": true, "url_private_download": "https://files.slack.com/files-pri/T3YS5EAL9-F3YTCL8TA/download/some_post_here", "user": "U407ABLLW", "groups": [], "is_public": false, "last_editor": "U407ABLLW", "pretty_type": "Post", "is_external": false, "mimetype": "text/plain", "permalink_public": "https://slack-files.com/T3YS5EAL9-F3YTCL8TA-9d9391a713", "permalink": "https://weeslacktest.slack.com/files/alice/F3YTCL8TA/some_post_here", "name": "some_post_here", "created": 1485975959, "comments_count": 0, "mode": "space"}, "team": "T3YS5EAL9", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "message", "channel": "C407ABS94", "bot_id": null}
diff --git a/_pytest/data/websocket/1485975978.91-file_public.json b/_pytest/data/websocket/1485975978.91-file_public.json
new file mode 100644
index 0000000..820f802
--- /dev/null
+++ b/_pytest/data/websocket/1485975978.91-file_public.json
@@ -0,0 +1 @@
+{"user_id": "U407ABLLW", "event_ts": "1485975978.887563", "file_id": "F3YTCL8TA", "file": {"id": "F3YTCL8TA"}, "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "file_public"}
diff --git a/_pytest/data/websocket/1485975978.91-file_shared.json b/_pytest/data/websocket/1485975978.91-file_shared.json
new file mode 100644
index 0000000..ede98ba
--- /dev/null
+++ b/_pytest/data/websocket/1485975978.91-file_shared.json
@@ -0,0 +1 @@
+{"user_id": "U407ABLLW", "event_ts": "1485975978.887560", "file_id": "F3YTCL8TA", "file": {"id": "F3YTCL8TA"}, "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "file_shared"}
diff --git a/_pytest/data/websocket/1485975995.98-file_change.json b/_pytest/data/websocket/1485975995.98-file_change.json
new file mode 100644
index 0000000..9744692
--- /dev/null
+++ b/_pytest/data/websocket/1485975995.98-file_change.json
@@ -0,0 +1 @@
+{"user_id": "U407ABLLW", "event_ts": "1485975995.887846", "file_id": "F3YTCL8TA", "file": {"id": "F3YTCL8TA"}, "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "file_change"}
diff --git a/_pytest/data/websocket/1485975998.13-file_change.json b/_pytest/data/websocket/1485975998.13-file_change.json
new file mode 100644
index 0000000..8933dd7
--- /dev/null
+++ b/_pytest/data/websocket/1485975998.13-file_change.json
@@ -0,0 +1 @@
+{"user_id": "U407ABLLW", "event_ts": "1485975998.887906", "file_id": "F3YTCL8TA", "file": {"id": "F3YTCL8TA"}, "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "file_change"}
diff --git a/_pytest/data/websocket/1485976002.57-file_deleted.json b/_pytest/data/websocket/1485976002.57-file_deleted.json
new file mode 100644
index 0000000..34719d0
--- /dev/null
+++ b/_pytest/data/websocket/1485976002.57-file_deleted.json
@@ -0,0 +1 @@
+{"event_ts": "1485976002.888005", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "file_deleted", "file_id": "F3YTCL8TA"}
diff --git a/_pytest/data/websocket/1485976002.66-message.json b/_pytest/data/websocket/1485976002.66-message.json
new file mode 100644
index 0000000..cb7bc08
--- /dev/null
+++ b/_pytest/data/websocket/1485976002.66-message.json
@@ -0,0 +1 @@
+{"event_ts": "1485976002.888006", "ts": "1485976002.000013", "subtype": "message_deleted", "hidden": true, "previous_message": {"username": "<@U407ABLLW|alice>", "subtype": "file_share", "text": "<@U407ABLLW|alice> shared a file: <https://weeslacktest.slack.com/files/alice/F3YTCL8TA/some_post_here|some post here>", "upload": false, "ts": "1485975978.000012", "display_as_bot": false, "user": "U407ABLLW", "file": {"filetype": "space", "channels": [], "display_as_bot": false, "id": "F3YTCL8TA", "size": 73, "title": "some post here", "url_private": "https://files.slack.com/files-pri/T3YS5EAL9-F3YTCL8TA/some_post_here", "ims": [], "state": "locked", "editor": "U407ABLLW", "preview": null, "external_type": "", "username": "", "updated": 1485975959, "timestamp": 1485975967, "public_url_shared": false, "editable": true, "url_private_download": "https://files.slack.com/files-pri/T3YS5EAL9-F3YTCL8TA/download/some_post_here", "user": "U407ABLLW", "groups": [], "is_public": true, "last_editor": "U407ABLLW", "pretty_type": "Post", "is_external": false, "mimetype": "text/plain", "permalink_public": "https://slack-files.com/T3YS5EAL9-F3YTCL8TA-9d9391a713", "permalink": "https://weeslacktest.slack.com/files/alice/F3YTCL8TA/some_post_here", "name": "some_post_here", "created": 1485975959, "comments_count": 0, "mode": "space"}, "type": "message", "bot_id": null}, "type": "message", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "channel": "C407ABS94", "deleted_ts": "1485975978.000012"}
diff --git a/_pytest/data/websocket/1485976024.46-user_typing.json b/_pytest/data/websocket/1485976024.46-user_typing.json
new file mode 100644
index 0000000..b8239d4
--- /dev/null
+++ b/_pytest/data/websocket/1485976024.46-user_typing.json
@@ -0,0 +1 @@
+{"wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "user_typing", "user": "U407ABLLW", "channel": "G3ZGMF4RZ"}
diff --git a/_pytest/data/websocket/1485976028.31-user_typing.json b/_pytest/data/websocket/1485976028.31-user_typing.json
new file mode 100644
index 0000000..8307f14
--- /dev/null
+++ b/_pytest/data/websocket/1485976028.31-user_typing.json
@@ -0,0 +1 @@
+{"wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "user_typing", "user": "U407ABLLW", "channel": "G3ZJKP7GA"}
diff --git a/_pytest/data/websocket/1485976039.32-reaction_added.json b/_pytest/data/websocket/1485976039.32-reaction_added.json
new file mode 100644
index 0000000..864ea68
--- /dev/null
+++ b/_pytest/data/websocket/1485976039.32-reaction_added.json
@@ -0,0 +1 @@
+{"reaction": "unamused", "event_ts": "1485976039.888795", "item": {"type": "message", "ts": "1485975841.000007", "channel": "C407ABS94"}, "user": "U407ABLLW", "item_user": "U407ABLLW", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "reaction_added"}
diff --git a/_pytest/data/websocket/1485976040.8-reaction_removed.json b/_pytest/data/websocket/1485976040.8-reaction_removed.json
new file mode 100644
index 0000000..3553a10
--- /dev/null
+++ b/_pytest/data/websocket/1485976040.8-reaction_removed.json
@@ -0,0 +1 @@
+{"reaction": "unamused", "event_ts": "1485976040.888829", "item": {"type": "message", "ts": "1485975841.000007", "channel": "C407ABS94"}, "user": "U407ABLLW", "item_user": "U407ABLLW", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "reaction_removed"}
diff --git a/_pytest/data/websocket/1485976087.84-reconnect_url.json b/_pytest/data/websocket/1485976087.84-reconnect_url.json
new file mode 100644
index 0000000..40308ce
--- /dev/null
+++ b/_pytest/data/websocket/1485976087.84-reconnect_url.json
@@ -0,0 +1 @@
+{"url": "wss://mpmulti-clcz.slack-msgs.com/websocket/ZwLc8-LUIYLyvEwRO5WKs99SLtxBN9Cyu1W5qmqkre0AtWFl_H8xTE7HbF0E9LUvgI4bNkIRG8WgPxIzfr5gLJsU6vg8By36_mUUZYfH0TV65y_h2vV7NQe6s3A6WJdKRoJAe_EI2AN5L-VeL9rK1Ygc0nw3ngmuL78G6cm48xw=", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "reconnect_url"}
diff --git a/_pytest/data/websocket/1485976107.57-channel_created.json b/_pytest/data/websocket/1485976107.57-channel_created.json
new file mode 100644
index 0000000..b061743
--- /dev/null
+++ b/_pytest/data/websocket/1485976107.57-channel_created.json
@@ -0,0 +1 @@
+{"event_ts": "1485976107.890695", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "channel_created", "channel": {"is_shared": false, "is_channel": true, "creator": "U407ABLLW", "created": 1485976107, "is_org_shared": false, "id": "C3ZM2GMGU", "name": "made-to-be-archived"}}
diff --git a/_pytest/data/websocket/1485976114.72-user_typing.json b/_pytest/data/websocket/1485976114.72-user_typing.json
new file mode 100644
index 0000000..df57e72
--- /dev/null
+++ b/_pytest/data/websocket/1485976114.72-user_typing.json
@@ -0,0 +1 @@
+{"wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "user_typing", "user": "U407ABLLW", "channel": "C407ABS94"}
diff --git a/_pytest/data/websocket/1485976117.99-user_typing.json b/_pytest/data/websocket/1485976117.99-user_typing.json
new file mode 100644
index 0000000..df57e72
--- /dev/null
+++ b/_pytest/data/websocket/1485976117.99-user_typing.json
@@ -0,0 +1 @@
+{"wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "user_typing", "user": "U407ABLLW", "channel": "C407ABS94"}
diff --git a/_pytest/data/websocket/1485976124.9-user_typing.json b/_pytest/data/websocket/1485976124.9-user_typing.json
new file mode 100644
index 0000000..df57e72
--- /dev/null
+++ b/_pytest/data/websocket/1485976124.9-user_typing.json
@@ -0,0 +1 @@
+{"wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "user_typing", "user": "U407ABLLW", "channel": "C407ABS94"}
diff --git a/_pytest/data/websocket/1485976124.98-message.json b/_pytest/data/websocket/1485976124.98-message.json
new file mode 100644
index 0000000..26ce4e5
--- /dev/null
+++ b/_pytest/data/websocket/1485976124.98-message.json
@@ -0,0 +1 @@
+{"text": "referencing someong by <@U3ZKBBDL5> name", "ts": "1485976124.000014", "user": "U407ABLLW", "team": "T3YS5EAL9", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "message", "channel": "C407ABS94"}
diff --git a/_pytest/data/websocket/1485976125.06-desktop_notification.json b/_pytest/data/websocket/1485976125.06-desktop_notification.json
new file mode 100644
index 0000000..ddd6dff
--- /dev/null
+++ b/_pytest/data/websocket/1485976125.06-desktop_notification.json
@@ -0,0 +1 @@
+{"avatarImage": "https://secure.gravatar.com/avatar/fd0ce7a168dc2235d21c53c2c1cfee0c.jpg?s=192&d=https%3A%2F%2Fa.slack-edge.com%2F7fa9%2Fimg%2Favatars%2Fava_0024-192.png", "subtitle": "#general", "is_shared": false, "title": "weeslacktest", "ssbFilename": "knock_brush.mp3", "imageUri": null, "launchUri": "slack://channel?id=C407ABS94&message=1485976124000014&team=T3YS5EAL9", "event_ts": "1485976124.891186", "msg": "1485976124.000014", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "content": "alice: referencing someong by @bob name", "type": "desktop_notification", "channel": "C407ABS94"}
diff --git a/_pytest/data/websocket/1485976125.95-user_typing.json b/_pytest/data/websocket/1485976125.95-user_typing.json
new file mode 100644
index 0000000..df57e72
--- /dev/null
+++ b/_pytest/data/websocket/1485976125.95-user_typing.json
@@ -0,0 +1 @@
+{"wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "user_typing", "user": "U407ABLLW", "channel": "C407ABS94"}
diff --git a/_pytest/data/websocket/1485976129.49-user_typing.json b/_pytest/data/websocket/1485976129.49-user_typing.json
new file mode 100644
index 0000000..df57e72
--- /dev/null
+++ b/_pytest/data/websocket/1485976129.49-user_typing.json
@@ -0,0 +1 @@
+{"wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "user_typing", "user": "U407ABLLW", "channel": "C407ABS94"}
diff --git a/_pytest/data/websocket/1485976133.0-user_typing.json b/_pytest/data/websocket/1485976133.0-user_typing.json
new file mode 100644
index 0000000..df57e72
--- /dev/null
+++ b/_pytest/data/websocket/1485976133.0-user_typing.json
@@ -0,0 +1 @@
+{"wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "user_typing", "user": "U407ABLLW", "channel": "C407ABS94"}
diff --git a/_pytest/data/websocket/1485976137.51-user_typing.json b/_pytest/data/websocket/1485976137.51-user_typing.json
new file mode 100644
index 0000000..df57e72
--- /dev/null
+++ b/_pytest/data/websocket/1485976137.51-user_typing.json
@@ -0,0 +1 @@
+{"wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "user_typing", "user": "U407ABLLW", "channel": "C407ABS94"}
diff --git a/_pytest/data/websocket/1485976137.95-message.json b/_pytest/data/websocket/1485976137.95-message.json
new file mode 100644
index 0000000..f4579e5
--- /dev/null
+++ b/_pytest/data/websocket/1485976137.95-message.json
@@ -0,0 +1 @@
+{"text": "referencing someone else by <@U407ABLLW> name", "ts": "1485976137.000015", "user": "U407ABLLW", "team": "T3YS5EAL9", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "message", "channel": "C407ABS94"}
diff --git a/_pytest/data/websocket/1485976138.73-user_typing.json b/_pytest/data/websocket/1485976138.73-user_typing.json
new file mode 100644
index 0000000..df57e72
--- /dev/null
+++ b/_pytest/data/websocket/1485976138.73-user_typing.json
@@ -0,0 +1 @@
+{"wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "user_typing", "user": "U407ABLLW", "channel": "C407ABS94"}
diff --git a/_pytest/data/websocket/1485976141.79-user_typing.json b/_pytest/data/websocket/1485976141.79-user_typing.json
new file mode 100644
index 0000000..df57e72
--- /dev/null
+++ b/_pytest/data/websocket/1485976141.79-user_typing.json
@@ -0,0 +1 @@
+{"wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "user_typing", "user": "U407ABLLW", "channel": "C407ABS94"}
diff --git a/_pytest/data/websocket/1485976145.8-user_typing.json b/_pytest/data/websocket/1485976145.8-user_typing.json
new file mode 100644
index 0000000..df57e72
--- /dev/null
+++ b/_pytest/data/websocket/1485976145.8-user_typing.json
@@ -0,0 +1 @@
+{"wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "user_typing", "user": "U407ABLLW", "channel": "C407ABS94"}
diff --git a/_pytest/data/websocket/1485976149.89-user_typing.json b/_pytest/data/websocket/1485976149.89-user_typing.json
new file mode 100644
index 0000000..df57e72
--- /dev/null
+++ b/_pytest/data/websocket/1485976149.89-user_typing.json
@@ -0,0 +1 @@
+{"wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "user_typing", "user": "U407ABLLW", "channel": "C407ABS94"}
diff --git a/_pytest/data/websocket/1485976151.6-message.json b/_pytest/data/websocket/1485976151.6-message.json
new file mode 100644
index 0000000..7efa65d
--- /dev/null
+++ b/_pytest/data/websocket/1485976151.6-message.json
@@ -0,0 +1 @@
+{"text": "referencing a #channel-that-doesnt-exist", "ts": "1485976151.000016", "user": "U407ABLLW", "team": "T3YS5EAL9", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "message", "channel": "C407ABS94"}
diff --git a/_pytest/data/websocket/1485976157.18-message.json b/_pytest/data/websocket/1485976157.18-message.json
new file mode 100644
index 0000000..62244b4
--- /dev/null
+++ b/_pytest/data/websocket/1485976157.18-message.json
@@ -0,0 +1 @@
+{"hidden": true, "event_ts": "1485976156.891735", "ts": "1485976156.000017", "subtype": "message_changed", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "message", "message": {"text": "referencing a <#C407ABS94|general>", "edited": {"user": "U407ABLLW", "ts": "1485976157.000000"}, "type": "message", "user": "U407ABLLW", "ts": "1485976151.000016"}, "channel": "C407ABS94", "previous_message": {"text": "referencing a #channel-that-doesnt-exist", "type": "message", "user": "U407ABLLW", "ts": "1485976151.000016"}}
diff --git a/_pytest/data/websocket/1485976157.8-user_typing.json b/_pytest/data/websocket/1485976157.8-user_typing.json
new file mode 100644
index 0000000..df57e72
--- /dev/null
+++ b/_pytest/data/websocket/1485976157.8-user_typing.json
@@ -0,0 +1 @@
+{"wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "user_typing", "user": "U407ABLLW", "channel": "C407ABS94"}
diff --git a/_pytest/data/websocket/1485976161.29-user_typing.json b/_pytest/data/websocket/1485976161.29-user_typing.json
new file mode 100644
index 0000000..df57e72
--- /dev/null
+++ b/_pytest/data/websocket/1485976161.29-user_typing.json
@@ -0,0 +1 @@
+{"wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "user_typing", "user": "U407ABLLW", "channel": "C407ABS94"}
diff --git a/_pytest/data/websocket/1485976161.75-message.json b/_pytest/data/websocket/1485976161.75-message.json
new file mode 100644
index 0000000..f0b899b
--- /dev/null
+++ b/_pytest/data/websocket/1485976161.75-message.json
@@ -0,0 +1 @@
+{"text": "referencing <#C3ZM8JTD3|some-channel2-renamed>", "ts": "1485976161.000018", "user": "U407ABLLW", "team": "T3YS5EAL9", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "message", "channel": "C407ABS94"}
diff --git a/_pytest/data/websocket/1485976182.59-channel_archive.json b/_pytest/data/websocket/1485976182.59-channel_archive.json
new file mode 100644
index 0000000..4ebdb1f
--- /dev/null
+++ b/_pytest/data/websocket/1485976182.59-channel_archive.json
@@ -0,0 +1 @@
+{"event_ts": "1485976182.892242", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "channel_archive", "user": "U407ABLLW", "channel": "C3ZM2GMGU"}
diff --git a/_pytest/data/websocket/1485976186.62-channel_unarchive.json b/_pytest/data/websocket/1485976186.62-channel_unarchive.json
new file mode 100644
index 0000000..c95897d
--- /dev/null
+++ b/_pytest/data/websocket/1485976186.62-channel_unarchive.json
@@ -0,0 +1 @@
+{"event_ts": "1485976186.892309", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "channel_unarchive", "user": "U407ABLLW", "channel": "C3ZM2GMGU"}
diff --git a/_pytest/data/websocket/1485976236.58-message.json b/_pytest/data/websocket/1485976236.58-message.json
new file mode 100644
index 0000000..f09b294
--- /dev/null
+++ b/_pytest/data/websocket/1485976236.58-message.json
@@ -0,0 +1 @@
+{"comment": {"comment": "first comment!", "created": 1485976236, "timestamp": 1485976236, "is_intro": false, "user": "U407ABLLW", "id": "Fc3ZMDRQLV", "channel": ""}, "text": "<@U407ABLLW|alice> commented on <@U407ABLLW|alice>\u2019s file <https://weeslacktest.slack.com/files/alice/F3ZJQTA66/1x1.png|1x1.png>: first comment!", "ts": "1485976236.000019", "subtype": "file_comment", "is_intro": false, "file": {"image_exif_rotation": 1, "filetype": "png", "channels": ["C407ABS94"], "display_as_bot": false, "id": "F3ZJQTA66", "size": 68, "original_h": 1, "thumb_360_w": 1, "title": "1x1.png", "url_private": "https://files.slack.com/files-pri/T3YS5EAL9-F3ZJQTA66/1x1.png", "thumb_360": "https://files.slack.com/files-tmb/T3YS5EAL9-F3ZJQTA66-8ab528dd77/1x1_360.png", "thumb_64": "https://files.slack.com/files-tmb/T3YS5EAL9-F3ZJQTA66-8ab528dd77/1x1_64.png", "ims": [], "thumb_80": "https://files.slack.com/files-tmb/T3YS5EAL9-F3ZJQTA66-8ab528dd77/1x1_80.png", "thumb_360_h": 1, "external_type": "", "username": "", "timestamp": 1485975955, "public_url_shared": false, "editable": false, "thumb_160": "https://files.slack.com/files-tmb/T3YS5EAL9-F3ZJQTA66-8ab528dd77/1x1_160.png", "url_private_download": "https://files.slack.com/files-pri/T3YS5EAL9-F3ZJQTA66/download/1x1.png", "user": "U407ABLLW", "groups": [], "is_public": true, "pretty_type": "PNG", "is_external": false, "mimetype": "image/png", "permalink_public": "https://slack-files.com/T3YS5EAL9-F3ZJQTA66-5d747593d2", "permalink": "https://weeslacktest.slack.com/files/alice/F3ZJQTA66/1x1.png", "name": "1x1.png", "created": 1485975955, "original_w": 1, "comments_count": 1, "mode": "hosted"}, "team": "T3YS5EAL9", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "message", "channel": "C407ABS94"}
diff --git a/_pytest/data/websocket/1485976236.67-file_comment_added.json b/_pytest/data/websocket/1485976236.67-file_comment_added.json
new file mode 100644
index 0000000..82c1f0c
--- /dev/null
+++ b/_pytest/data/websocket/1485976236.67-file_comment_added.json
@@ -0,0 +1 @@
+{"comment": {"comment": "first comment!", "created": 1485976236, "timestamp": 1485976236, "is_intro": false, "user": "U407ABLLW", "id": "Fc3ZMDRQLV", "channel": ""}, "user_id": "U407ABLLW", "event_ts": "1485976236.893560", "file_id": "F3ZJQTA66", "file": {"id": "F3ZJQTA66"}, "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "file_comment_added"}
diff --git a/_pytest/data/websocket/1485976248.57-message.json b/_pytest/data/websocket/1485976248.57-message.json
new file mode 100644
index 0000000..d3c8ce6
--- /dev/null
+++ b/_pytest/data/websocket/1485976248.57-message.json
@@ -0,0 +1 @@
+{"hidden": true, "event_ts": "1485976248.893799", "ts": "1485976248.000020", "subtype": "message_changed", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "message", "message": {"comment": {"comment": "first comment! now i edited it.", "created": 1485976236, "timestamp": 1485976236, "is_intro": false, "user": "U407ABLLW", "id": "Fc3ZMDRQLV", "channel": ""}, "text": "<@U407ABLLW|alice> commented on <@U407ABLLW|alice>\u2019s file <https://weeslacktest.slack.com/files/alice/F3ZJQTA66/1x1.png|1x1.png>: first comment! now i edited it.", "ts": "1485976236.000019", "subtype": "file_comment", "is_intro": false, "file": {"image_exif_rotation": 1, "filetype": "png", "channels": ["C407ABS94"], "display_as_bot": false, "id": "F3ZJQTA66", "size": 68, "original_h": 1, "thumb_360_w": 1, "title": "1x1.png", "url_private": "https://files.slack.com/files-pri/T3YS5EAL9-F3ZJQTA66/1x1.png", "thumb_360": "https://files.slack.com/files-tmb/T3YS5EAL9-F3ZJQTA66-8ab528dd77/1x1_360.png", "thumb_64": "https://files.slack.com/files-tmb/T3YS5EAL9-F3ZJQTA66-8ab528dd77/1x1_64.png", "ims": [], "thumb_80": "https://files.slack.com/files-tmb/T3YS5EAL9-F3ZJQTA66-8ab528dd77/1x1_80.png", "thumb_360_h": 1, "external_type": "", "username": "", "timestamp": 1485975955, "public_url_shared": false, "editable": false, "thumb_160": "https://files.slack.com/files-tmb/T3YS5EAL9-F3ZJQTA66-8ab528dd77/1x1_160.png", "url_private_download": "https://files.slack.com/files-pri/T3YS5EAL9-F3ZJQTA66/download/1x1.png", "user": "U407ABLLW", "groups": [], "is_public": true, "pretty_type": "PNG", "is_external": false, "mimetype": "image/png", "permalink_public": "https://slack-files.com/T3YS5EAL9-F3ZJQTA66-5d747593d2", "permalink": "https://weeslacktest.slack.com/files/alice/F3ZJQTA66/1x1.png", "name": "1x1.png", "created": 1485975955, "original_w": 1, "comments_count": 1, "mode": "hosted"}, "type": "message"}, "channel": "C407ABS94", "previous_message": {"comment": {"comment": "first comment! now i edited it.", "created": 1485976236, "timestamp": 1485976236, "is_intro": false, "user": "U407ABLLW", "id": "Fc3ZMDRQLV", "channel": ""}, "text": "<@U407ABLLW|alice> commented on <@U407ABLLW|alice>\u2019s file <https://weeslacktest.slack.com/files/alice/F3ZJQTA66/1x1.png|1x1.png>: first comment! now i edited it.", "ts": "1485976236.000019", "subtype": "file_comment", "is_intro": false, "file": {"image_exif_rotation": 1, "filetype": "png", "channels": ["C407ABS94"], "display_as_bot": false, "id": "F3ZJQTA66", "size": 68, "original_h": 1, "thumb_360_w": 1, "title": "1x1.png", "url_private": "https://files.slack.com/files-pri/T3YS5EAL9-F3ZJQTA66/1x1.png", "thumb_360": "https://files.slack.com/files-tmb/T3YS5EAL9-F3ZJQTA66-8ab528dd77/1x1_360.png", "thumb_64": "https://files.slack.com/files-tmb/T3YS5EAL9-F3ZJQTA66-8ab528dd77/1x1_64.png", "ims": [], "thumb_80": "https://files.slack.com/files-tmb/T3YS5EAL9-F3ZJQTA66-8ab528dd77/1x1_80.png", "thumb_360_h": 1, "external_type": "", "username": "", "timestamp": 1485975955, "public_url_shared": false, "editable": false, "thumb_160": "https://files.slack.com/files-tmb/T3YS5EAL9-F3ZJQTA66-8ab528dd77/1x1_160.png", "url_private_download": "https://files.slack.com/files-pri/T3YS5EAL9-F3ZJQTA66/download/1x1.png", "user": "U407ABLLW", "groups": [], "is_public": true, "pretty_type": "PNG", "is_external": false, "mimetype": "image/png", "permalink_public": "https://slack-files.com/T3YS5EAL9-F3ZJQTA66-5d747593d2", "permalink": "https://weeslacktest.slack.com/files/alice/F3ZJQTA66/1x1.png", "name": "1x1.png", "created": 1485975955, "original_w": 1, "comments_count": 1, "mode": "hosted"}, "type": "message"}}
diff --git a/_pytest/data/websocket/1485976248.65-file_comment_edited.json b/_pytest/data/websocket/1485976248.65-file_comment_edited.json
new file mode 100644
index 0000000..3e7c528
--- /dev/null
+++ b/_pytest/data/websocket/1485976248.65-file_comment_edited.json
@@ -0,0 +1 @@
+{"comment": {"comment": "first comment! now i edited it.", "created": 1485976236, "timestamp": 1485976236, "is_intro": false, "user": "U407ABLLW", "id": "Fc3ZMDRQLV", "channel": ""}, "user_id": "U407ABLLW", "event_ts": "1485976248.893801", "file_id": "F3ZJQTA66", "file": {"id": "F3ZJQTA66"}, "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "file_comment_edited"}
diff --git a/_pytest/data/websocket/1485976267.81-reconnect_url.json b/_pytest/data/websocket/1485976267.81-reconnect_url.json
new file mode 100644
index 0000000..55533ab
--- /dev/null
+++ b/_pytest/data/websocket/1485976267.81-reconnect_url.json
@@ -0,0 +1 @@
+{"url": "wss://mpmulti-m00z.slack-msgs.com/websocket/fZI1FV2ZuMYXeQLMKi1SrH2j69xL6m_6hC38CVCr0ugPFpMS1IBxlCvHJYbSrH-fMhSuLmbkEd1te_ND5Q-EyVVX-w06mn5NLZM9GaX_mWb9A3w79sThYzEgnnQ8onoeFM2CWgFNxWM_3XS4HJaWXeee-_sNh_booNbby8jm9mg=", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "reconnect_url"}
diff --git a/_pytest/data/websocket/1486004888.32-presence_change.json b/_pytest/data/websocket/1486004888.32-presence_change.json
new file mode 100644
index 0000000..c52679d
--- /dev/null
+++ b/_pytest/data/websocket/1486004888.32-presence_change.json
@@ -0,0 +1 @@
+{"wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "presence_change", "user": "U3ZKBBDL5", "presence": "active"} \ No newline at end of file
diff --git a/_pytest/data/websocket/1486004888.41-reconnect_url.json b/_pytest/data/websocket/1486004888.41-reconnect_url.json
new file mode 100644
index 0000000..ae7d1f1
--- /dev/null
+++ b/_pytest/data/websocket/1486004888.41-reconnect_url.json
@@ -0,0 +1 @@
+{"url": "wss://mpmulti-3z94.slack-msgs.com/websocket/v0jzriT0ZxrHn6tVO4Dpi96hr54r8ZOaGklQfQN2G_D1kO6mEMs7KXXfUTax40_5zkAA6XTfZpSsjo3TGVD0hcFrhhSwhTsbFvs6W2m5pHH8hfaLJuKFkfPZySnprXsHCXDNKMWyTKMFTZ_CAUKjyhJWzY9Z708pAGB8oy6YMcE=", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "reconnect_url"} \ No newline at end of file
diff --git a/_pytest/data/websocket/1486004935.63-presence_change.json b/_pytest/data/websocket/1486004935.63-presence_change.json
new file mode 100644
index 0000000..eca19bc
--- /dev/null
+++ b/_pytest/data/websocket/1486004935.63-presence_change.json
@@ -0,0 +1 @@
+{"wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "presence_change", "user": "U407ABLLW", "presence": "active"} \ No newline at end of file
diff --git a/_pytest/data/websocket/1486004950.43-message.json b/_pytest/data/websocket/1486004950.43-message.json
new file mode 100644
index 0000000..ce13b3f
--- /dev/null
+++ b/_pytest/data/websocket/1486004950.43-message.json
@@ -0,0 +1 @@
+{"text": "is here", "ts": "1486004950.000002", "subtype": "me_message", "user": "U407ABLLW", "team": "T3YS5EAL9", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "message", "channel": "C407ABS94"} \ No newline at end of file
diff --git a/_pytest/data/websocket/1486004992.49-user_typing.json b/_pytest/data/websocket/1486004992.49-user_typing.json
new file mode 100644
index 0000000..859d905
--- /dev/null
+++ b/_pytest/data/websocket/1486004992.49-user_typing.json
@@ -0,0 +1 @@
+{"wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "user_typing", "user": "U407ABLLW", "channel": "C407ABS94"} \ No newline at end of file
diff --git a/_pytest/data/websocket/1486004995.69-user_typing.json b/_pytest/data/websocket/1486004995.69-user_typing.json
new file mode 100644
index 0000000..859d905
--- /dev/null
+++ b/_pytest/data/websocket/1486004995.69-user_typing.json
@@ -0,0 +1 @@
+{"wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "user_typing", "user": "U407ABLLW", "channel": "C407ABS94"} \ No newline at end of file
diff --git a/_pytest/data/websocket/1486004999.55-user_typing.json b/_pytest/data/websocket/1486004999.55-user_typing.json
new file mode 100644
index 0000000..859d905
--- /dev/null
+++ b/_pytest/data/websocket/1486004999.55-user_typing.json
@@ -0,0 +1 @@
+{"wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "user_typing", "user": "U407ABLLW", "channel": "C407ABS94"} \ No newline at end of file
diff --git a/_pytest/data/websocket/1486004999.95-message.json b/_pytest/data/websocket/1486004999.95-message.json
new file mode 100644
index 0000000..b906b1b
--- /dev/null
+++ b/_pytest/data/websocket/1486004999.95-message.json
@@ -0,0 +1 @@
+{"text": "this _is_ a *formatted* message", "ts": "1486004999.000003", "user": "U407ABLLW", "team": "T3YS5EAL9", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "message", "channel": "C407ABS94"} \ No newline at end of file
diff --git a/_pytest/data/websocket/1486005004.03-user_typing.json b/_pytest/data/websocket/1486005004.03-user_typing.json
new file mode 100644
index 0000000..859d905
--- /dev/null
+++ b/_pytest/data/websocket/1486005004.03-user_typing.json
@@ -0,0 +1 @@
+{"wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "user_typing", "user": "U407ABLLW", "channel": "C407ABS94"} \ No newline at end of file
diff --git a/_pytest/data/websocket/1486005007.16-user_typing.json b/_pytest/data/websocket/1486005007.16-user_typing.json
new file mode 100644
index 0000000..859d905
--- /dev/null
+++ b/_pytest/data/websocket/1486005007.16-user_typing.json
@@ -0,0 +1 @@
+{"wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "user_typing", "user": "U407ABLLW", "channel": "C407ABS94"} \ No newline at end of file
diff --git a/_pytest/data/websocket/1486005008.45-reconnect_url.json b/_pytest/data/websocket/1486005008.45-reconnect_url.json
new file mode 100644
index 0000000..bafddd6
--- /dev/null
+++ b/_pytest/data/websocket/1486005008.45-reconnect_url.json
@@ -0,0 +1 @@
+{"url": "wss://mpmulti-14cn.slack-msgs.com/websocket/28Rgy5cmEP5N50Jq5Jk0YSCKRdC_PTNklbuT07jR1-BP0vpyvFzhUFpTqbl7pxbMkSCE1rv8OMQMKsWyHgDnoJ9KANYeHweBmRAsL41zPn-dJnJmTpnm670fgK1Nr-tVsfXpo7ql0cbstLR_KWDHr8zt1B2TbqIUeoikNN6oBiY=", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "reconnect_url"} \ No newline at end of file
diff --git a/_pytest/data/websocket/1486005009.0-message.json b/_pytest/data/websocket/1486005009.0-message.json
new file mode 100644
index 0000000..10c96e9
--- /dev/null
+++ b/_pytest/data/websocket/1486005009.0-message.json
@@ -0,0 +1 @@
+{"text": "this is ~strikethrough~", "ts": "1486005008.000004", "user": "U407ABLLW", "team": "T3YS5EAL9", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "message", "channel": "C407ABS94"} \ No newline at end of file
diff --git a/_pytest/data/websocket/1486005096.38-accounts_changed.json b/_pytest/data/websocket/1486005096.38-accounts_changed.json
new file mode 100644
index 0000000..1d0086c
--- /dev/null
+++ b/_pytest/data/websocket/1486005096.38-accounts_changed.json
@@ -0,0 +1 @@
+{"event_ts": "1486005096.312395", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "accounts_changed"} \ No newline at end of file
diff --git a/_pytest/data/websocket/1486005099.81-presence_change.json b/_pytest/data/websocket/1486005099.81-presence_change.json
new file mode 100644
index 0000000..c52679d
--- /dev/null
+++ b/_pytest/data/websocket/1486005099.81-presence_change.json
@@ -0,0 +1 @@
+{"wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "presence_change", "user": "U3ZKBBDL5", "presence": "active"} \ No newline at end of file
diff --git a/_pytest/data/websocket/1486005104.63-channel_marked.json b/_pytest/data/websocket/1486005104.63-channel_marked.json
new file mode 100644
index 0000000..956e886
--- /dev/null
+++ b/_pytest/data/websocket/1486005104.63-channel_marked.json
@@ -0,0 +1 @@
+{"mention_count": 0, "event_ts": "1486005104.313131", "ts": "1486005008.000004", "unread_count": 0, "num_mentions": 0, "mention_count_display": 0, "unread_count_display": 0, "num_mentions_display": 0, "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "channel_marked", "channel": "C407ABS94"} \ No newline at end of file
diff --git a/_pytest/data/websocket/1486005109.58-im_marked.json b/_pytest/data/websocket/1486005109.58-im_marked.json
new file mode 100644
index 0000000..b74c742
--- /dev/null
+++ b/_pytest/data/websocket/1486005109.58-im_marked.json
@@ -0,0 +1 @@
+{"event_ts": "1486005109.313169", "ts": "1485975735.000002", "dm_count": 0, "mention_count_display": 0, "unread_count_display": 0, "num_mentions_display": 0, "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "im_marked", "channel": "D3ZK1D8JY"} \ No newline at end of file
diff --git a/_pytest/data/websocket/1486005119.69-im_marked.json b/_pytest/data/websocket/1486005119.69-im_marked.json
new file mode 100644
index 0000000..802f4c9
--- /dev/null
+++ b/_pytest/data/websocket/1486005119.69-im_marked.json
@@ -0,0 +1 @@
+{"event_ts": "1486005119.313254", "ts": "1485975421.000002", "dm_count": 0, "mention_count_display": 0, "unread_count_display": 0, "num_mentions_display": 0, "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "im_marked", "channel": "D3ZEQULHZ"} \ No newline at end of file
diff --git a/_pytest/data/websocket/1486005124.51-mpim_marked.json b/_pytest/data/websocket/1486005124.51-mpim_marked.json
new file mode 100644
index 0000000..3357683
--- /dev/null
+++ b/_pytest/data/websocket/1486005124.51-mpim_marked.json
@@ -0,0 +1 @@
+{"mention_count": 0, "event_ts": "1486005124.313290", "is_mpim": true, "ts": "1485975715.000005", "unread_count": 0, "num_mentions": 0, "mention_count_display": 0, "unread_count_display": 0, "num_mentions_display": 0, "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "mpim_marked", "channel": "G3ZGMF4RZ"} \ No newline at end of file
diff --git a/_pytest/data/websocket/1486005124.59-group_marked.json b/_pytest/data/websocket/1486005124.59-group_marked.json
new file mode 100644
index 0000000..8efe97a
--- /dev/null
+++ b/_pytest/data/websocket/1486005124.59-group_marked.json
@@ -0,0 +1 @@
+{"mention_count": 0, "event_ts": "1486005124.313291", "is_mpim": true, "ts": "1485975715.000005", "unread_count": 0, "num_mentions": 0, "mention_count_display": 0, "unread_count_display": 0, "num_mentions_display": 0, "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "group_marked", "channel": "G3ZGMF4RZ"} \ No newline at end of file
diff --git a/_pytest/data/websocket/1486005124.67-group_marked.json b/_pytest/data/websocket/1486005124.67-group_marked.json
new file mode 100644
index 0000000..7753500
--- /dev/null
+++ b/_pytest/data/websocket/1486005124.67-group_marked.json
@@ -0,0 +1 @@
+{"mention_count": 0, "event_ts": "1486005124.313292", "is_mpim": false, "ts": "1485975492.000004", "unread_count": 0, "num_mentions": 0, "mention_count_display": 0, "unread_count_display": 0, "num_mentions_display": 0, "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "group_marked", "channel": "G3ZJKP7GA"} \ No newline at end of file
diff --git a/_pytest/data/websocket/1486005188.48-reconnect_url.json b/_pytest/data/websocket/1486005188.48-reconnect_url.json
new file mode 100644
index 0000000..1bdbd43
--- /dev/null
+++ b/_pytest/data/websocket/1486005188.48-reconnect_url.json
@@ -0,0 +1 @@
+{"url": "wss://mpmulti-lz08.slack-msgs.com/websocket/dHqqUxSrrgTCJI7af1mYz7ECm0-rxu4GOIPmnYwK_K9uWOwBXvF54joGrhpZcUjrJ913Vx01Wh6Ta4u5-bC4-Zn9H9wXklCc6mDDf8iJJmXKC2nWZYaP8jOTzPRNM97iISoe5LNSXl_JB-9J5DSSwEdBh7eQ0SEFeko7NxXP6_o=", "wee_slack_metadata": {"team": "d80c2b6c3127dbb1991917394ed219e8212a2606"}, "type": "reconnect_url"} \ No newline at end of file
diff --git a/_pytest/test_eventrouter.py b/_pytest/test_eventrouter.py
new file mode 100644
index 0000000..b235512
--- /dev/null
+++ b/_pytest/test_eventrouter.py
@@ -0,0 +1,86 @@
+import pytest
+from wee_slack import EventRouter, ProcessNotImplemented, SlackRequest
+
+def test_EventRouter(mock_weechat):
+ # Sending valid json adds to the queue.
+ e = EventRouter()
+ e.receive_json('{}')
+ assert len(e.queue) == 1
+
+ # Handling an event removes from the queue.
+ e = EventRouter()
+ # Create a function to test we are called
+ e.proc['testfunc'] = lambda x, y: x
+ e.receive_json('{"type": "testfunc"}')
+ e.handle_next()
+ assert len(e.queue) == 0
+
+ # Handling a local event removes from the queue.
+ e = EventRouter()
+ # Create a function to test we are called
+ e.proc['local_testfunc'] = lambda x, y: x
+ e.receive_json('{"type": "local_testfunc"}')
+ e.handle_next()
+ assert len(e.queue) == 0
+
+ # Handling an event without an associated processor
+ # raises an exception.
+ e = EventRouter()
+ # Create a function to test we are called
+ e.receive_json('{"type": "testfunc"}')
+ with pytest.raises(ProcessNotImplemented):
+ e.handle_next()
+
+def test_EventRouterReceivedata(mock_weechat):
+
+ e = EventRouter()
+ context = e.store_context(SlackRequest('xoxoxoxox', "rtm.startold", {"meh": "blah"}))
+ print context
+ e.receive_httprequest_callback(context, 1, -1, ' {"JSON": "MEH", ', 4)
+ #print len(e.reply_buffer)
+ context = e.store_context(SlackRequest('xoxoxoxox', "rtm.startold", {"meh": "blah"}))
+ print context
+ e.receive_httprequest_callback(context, 1, -1, ' "JSON2": "MEH", ', 4)
+ #print len(e.reply_buffer)
+ context = e.store_context(SlackRequest('xoxoxoxox', "rtm.startold", {"meh": "blah"}))
+ print context
+ e.receive_httprequest_callback(context, 1, 0, ' "JSON3": "MEH"}', 4)
+ #print len(e.reply_buffer)
+ try:
+ e.handle_next()
+ e.handle_next()
+ e.handle_next()
+ e.handle_next()
+ except:
+ pass
+
+ print e.context
+ #assert False
+
+ 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, -1, rtmstartdata[:5000], 4)
+ e.receive_httprequest_callback(context, 1, 0, rtmstartdata[5000:], 4)
+ e.handle_next()
+
+ #print len(e.reply_buffer)
+
+ #print e.teams
+
+ for t in e.teams:
+ #print vars(e.teams[t])
+ for c in e.teams[t].channels:
+ pass
+ #print c
+ for u in e.teams[t].users:
+ pass
+ #print vars(u)
+
+
+# e = EventRouter()
+# # Create a function to test we are called
+# e.receive_json('{"type": "message"}')
+# e.handle_next()
+# assert False
+
+ #assert False
diff --git a/_pytest/test_everything.py b/_pytest/test_everything.py
new file mode 100644
index 0000000..a121541
--- /dev/null
+++ b/_pytest/test_everything.py
@@ -0,0 +1,50 @@
+import glob
+import json
+
+#from wee_slack import render
+from wee_slack import ProcessNotImplemented
+
+def test_process_message(monkeypatch, realish_eventrouter, mock_websocket):
+
+ eventrouter = realish_eventrouter
+
+ t = eventrouter.teams.keys()[0]
+ #u = eventrouter.teams[t].users.keys()[0]
+
+ #user = eventrouter.teams[t].users[u]
+ #print user
+
+ socket = mock_websocket
+ eventrouter.teams[t].ws = socket
+
+ datafiles = glob.glob("_pytest/data/websocket/*.json")
+
+ print datafiles
+ #assert False
+
+ notimplemented = set()
+
+ for fname in datafiles:
+ try:
+ print "####################"
+ data = json.loads(open(fname, 'r').read())
+ socket.add(data)
+ print data
+ eventrouter.receive_ws_callback(t)
+ eventrouter.handle_next()
+ except ProcessNotImplemented as e:
+ notimplemented.add(str(e))
+ #this handles some message data not existing - need to fix
+ except KeyError:
+ pass
+
+ if len(notimplemented) > 0:
+ print "####################"
+ print sorted(notimplemented)
+ print "####################"
+
+ print len(eventrouter.queue)
+ #assert False
+
+
+
diff --git a/_pytest/test_linkifytext.py b/_pytest/test_linkifytext.py
new file mode 100644
index 0000000..f9da3f9
--- /dev/null
+++ b/_pytest/test_linkifytext.py
@@ -0,0 +1,6 @@
+from wee_slack import linkify_text
+
+#def test_linkifytext():
+# linkify_text('@ryan')
+
+# assert False
diff --git a/_pytest/test_presencechange.py b/_pytest/test_presencechange.py
new file mode 100644
index 0000000..b4202fa
--- /dev/null
+++ b/_pytest/test_presencechange.py
@@ -0,0 +1,31 @@
+
+def test_PresenceChange(monkeypatch, realish_eventrouter, mock_websocket):
+
+ e = realish_eventrouter
+
+ t = e.teams.keys()[0]
+ u = e.teams[t].users.keys()[0]
+
+ user = e.teams[t].users[u]
+
+ socket = mock_websocket
+ e.teams[t].ws = socket
+
+ socket.add({
+ "type": "presence_change",
+ "user": user.identifier,
+ "presence": "active",
+ })
+ socket.add({
+ "type": "presence_change",
+ "user": user.identifier,
+ "presence": "away",
+ })
+
+ e.receive_ws_callback(t)
+ e.handle_next()
+ assert e.teams[t].users[u].presence == "active"
+
+ e.receive_ws_callback(t)
+ e.handle_next()
+ assert e.teams[t].users[u].presence == "away"
diff --git a/_pytest/test_process_message.py b/_pytest/test_process_message.py
index 0ca56cd..e2447f7 100644
--- a/_pytest/test_process_message.py
+++ b/_pytest/test_process_message.py
@@ -1,38 +1,48 @@
-
-import wee_slack
-import pytest
import json
-from collections import defaultdict
+from wee_slack import render
+
+def test_process_message(monkeypatch, realish_eventrouter, mock_websocket):
-def test_process_message(slack_debug, monkeypatch, myservers, mychannels, myusers):
- called = defaultdict(int)
- wee_slack.servers = myservers
- wee_slack.channels = mychannels
- wee_slack.users = myusers
- wee_slack.message_cache = {}
- wee_slack.servers[0].users = myusers
- wee_slack.unfurl_ignore_alt_text = False
+ e = realish_eventrouter
- def mock_buffer_prnt(*args):
- called['buffer_prnt'] += 1
- monkeypatch.setattr(wee_slack.Channel, 'buffer_prnt', mock_buffer_prnt)
+ t = e.teams.keys()[0]
+ u = e.teams[t].users.keys()[0]
-# def mock_buffer_prnt_changed(*args):
-# called['buffer_prnt_changed'] += 1
-# print args
-# monkeypatch.setattr(wee_slack.Channel, 'buffer_prnt_changed', mock_buffer_prnt_changed)
+ user = e.teams[t].users[u]
+ #print user
+ socket = mock_websocket
+ e.teams[t].ws = socket
messages = []
- messages.append( json.loads(open('_pytest/data/message-normal.json', 'r').read()) )
- messages.append( json.loads(open('_pytest/data/message-normal2.json', 'r').read()) )
- messages.append( json.loads(open('_pytest/data/message-changed.json', 'r').read()) )
- messages.append( json.loads(open('_pytest/data/message-deleted.json', 'r').read()) )
+ messages.append(json.loads(open('_pytest/data/websocket/1485975421.33-message.json', 'r').read()))
+
+ # test message and then change
+ messages.append(json.loads(open('_pytest/data/websocket/1485976157.18-message.json', 'r').read()))
+ messages.append(json.loads(open('_pytest/data/websocket/1485976151.6-message.json', 'r').read()))
+
+ # test message then deletion
+ messages.append(json.loads(open('_pytest/data/websocket/1485975698.45-message.json', 'r').read()))
+ messages.append(json.loads(open('_pytest/data/websocket/1485975723.85-message.json', 'r').read()))
+
for m in messages:
- wee_slack.process_message(m)
- print "---"
- print called
- print "---"
-# assert called['buffer_prnt'] == 2
-# assert called['buffer_prnt_changed'] == 1
+ m["user"] = user.id
+ socket.add(m)
+
+ e.receive_ws_callback(t)
+ e.handle_next()
+
+ e.receive_ws_callback(t)
+ e.handle_next()
+
+ e.receive_ws_callback(t)
+ e.handle_next()
+
+ e.receive_ws_callback(t)
+ e.handle_next()
+
+
+ #assert e.teams[t].channels['C407ABS94'].messages.keys()[0] == '1485976151.00016'
+ #assert False
+
diff --git a/_pytest/test_processreply.py b/_pytest/test_processreply.py
new file mode 100644
index 0000000..a725f23
--- /dev/null
+++ b/_pytest/test_processreply.py
@@ -0,0 +1,33 @@
+#from wee_slack import process_reply
+
+def test_process_reply(monkeypatch, realish_eventrouter, mock_websocket):
+
+ e = realish_eventrouter
+
+ t = e.teams.keys()[0]
+ #u = e.teams[t].users.keys()[0]
+
+ #user = e.teams[t].users[u]
+ #print user
+
+ socket = mock_websocket
+ e.teams[t].ws = socket
+
+ c = e.teams[t].channels.keys()[0]
+ channel = e.teams[t].channels[c]
+ channel.send_message('asdf')
+
+ socket = mock_websocket
+ socket.add({"reply_to": 1, "_team": t, "ts": "12341234.111"})
+
+ print e.teams[t].ws_replies
+
+ e.receive_ws_callback(t)
+ e.handle_next()
+
+ #reply = {"reply_to": 1, "_team": t, "ts": "12341234.111"}
+ #print reply
+ #process_reply(reply, e)
+ #print e.teams[t].ws_replies
+ #assert False
+ pass
diff --git a/_pytest/test_processteamjoin.py b/_pytest/test_processteamjoin.py
new file mode 100644
index 0000000..00a8b4c
--- /dev/null
+++ b/_pytest/test_processteamjoin.py
@@ -0,0 +1,54 @@
+import glob
+import json
+
+from wee_slack import ProcessNotImplemented
+
+def test_process_reply(monkeypatch, mock_websocket, realish_eventrouter):
+
+ eventrouter = realish_eventrouter
+
+ t = eventrouter.teams.keys()[0]
+ #u = eventrouter.teams[t].users.keys()[0]
+
+ #user = eventrouter.teams[t].users[u]
+ #print user
+
+ #delete charles so we can add him
+ del eventrouter.teams[t].users['U4096CBHC']
+
+ assert len(eventrouter.teams[t].users) == 3
+
+ socket = mock_websocket
+ eventrouter.teams[t].ws = socket
+
+ datafiles = glob.glob("_pytest/data/websocket/1485975606.59-team_join.json")
+
+ print datafiles
+ #assert False
+
+ notimplemented = set()
+
+ for fname in datafiles:
+ try:
+ print "####################"
+ data = json.loads(open(fname, 'r').read())
+ socket.add(data)
+ print data
+ eventrouter.receive_ws_callback(t)
+ eventrouter.handle_next()
+ except ProcessNotImplemented as e:
+ notimplemented.add(str(e))
+ #this handles some message data not existing - need to fix
+ except KeyError:
+ pass
+
+ if len(notimplemented) > 0:
+ print "####################"
+ print sorted(notimplemented)
+ print "####################"
+
+ #print len(eventrouter.queue)
+ assert len(eventrouter.teams[t].users) == 4
+
+
+
diff --git a/_pytest/test_sendmessage.py b/_pytest/test_sendmessage.py
new file mode 100644
index 0000000..a87942d
--- /dev/null
+++ b/_pytest/test_sendmessage.py
@@ -0,0 +1,21 @@
+
+def test_send_message(monkeypatch, realish_eventrouter, mock_websocket):
+ e = realish_eventrouter
+
+ t = e.teams.keys()[0]
+ #u = e.teams[t].users.keys()[0]
+
+ #user = e.teams[t].users[u]
+ #print user
+
+ socket = mock_websocket
+ e.teams[t].ws = socket
+
+ c = e.teams[t].channels.keys()[0]
+
+ channel = e.teams[t].channels[c]
+ channel.send_message('asdf')
+
+ print c
+
+ #assert False
diff --git a/_pytest/test_slackchannel.py b/_pytest/test_slackchannel.py
new file mode 100644
index 0000000..7d93afc
--- /dev/null
+++ b/_pytest/test_slackchannel.py
@@ -0,0 +1,33 @@
+from mock import Mock
+#from wee_slack import SlackChannel
+
+def test_SlackChannel(realish_eventrouter):
+ e = realish_eventrouter
+
+ print e.sc["team"].channels
+ #c = SlackChannel(e, **json.loads(chan))
+ c = e.sc["team"].channels['C3ZEQAYN7']
+
+ print c.formatted_name()
+ c.is_someone_typing = Mock(return_value=True)
+ c.channel_buffer = Mock(return_value=True)
+ print c.create_buffer()
+ print c.rename()
+ print c.current_short_name
+ print c.formatted_name()
+ print c.rename()
+ print c.formatted_name()
+
+ print "-------"
+ print c == "random"
+ print "-------"
+ print c == "#random"
+ print "-------"
+ print c == "weeslacktest.slack.com.#random"
+ print "-------"
+ print c == "weeslacktest.slack.com.random"
+ print "-------"
+ print c == "dandom"
+
+ print e.weechat_controller.buffers
+ #assert False
diff --git a/_pytest/test_slackdmchannel.py b/_pytest/test_slackdmchannel.py
new file mode 100644
index 0000000..203c4ae
--- /dev/null
+++ b/_pytest/test_slackdmchannel.py
@@ -0,0 +1,20 @@
+from mock import Mock
+#from wee_slack import SlackChannel
+
+def test_SlackDMChannel(realish_eventrouter):
+ e = realish_eventrouter
+
+ print e.sc["team"].channels
+ #c = SlackChannel(e, **json.loads(chan))
+ c = e.sc["team"].channels['D3ZEQULHZ']
+
+ print c.formatted_name()
+ c.is_someone_typing = Mock(return_value=True)
+ c.channel_buffer = Mock(return_value=True)
+ print c.create_buffer()
+ print c.rename()
+ print c.current_short_name
+ print c.formatted_name()
+ print c.rename()
+ print c.formatted_name()
+# assert False
diff --git a/_pytest/test_slackgroupchannel.py b/_pytest/test_slackgroupchannel.py
new file mode 100644
index 0000000..59793f2
--- /dev/null
+++ b/_pytest/test_slackgroupchannel.py
@@ -0,0 +1,20 @@
+from mock import Mock
+#from wee_slack import SlackChannel
+
+def test_SlackGroupChannel(realish_eventrouter):
+ e = realish_eventrouter
+
+ print e.sc["team"].channels
+ #c = SlackChannel(e, **json.loads(chan))
+ c = e.sc["team"].channels['G3ZJKP7GA']
+
+ print c.formatted_name()
+ c.is_someone_typing = Mock(return_value=True)
+ c.channel_buffer = Mock(return_value=True)
+ print c.create_buffer()
+ print c.rename()
+ print c.current_short_name
+ print c.formatted_name()
+ print c.rename()
+ print c.formatted_name()
+# assert False
diff --git a/_pytest/test_slackmpdmchannel.py b/_pytest/test_slackmpdmchannel.py
new file mode 100644
index 0000000..a2c6c51
--- /dev/null
+++ b/_pytest/test_slackmpdmchannel.py
@@ -0,0 +1,20 @@
+from mock import Mock
+#from wee_slack import SlackChannel
+
+def test_SlackMPDMChannel(realish_eventrouter):
+ e = realish_eventrouter
+
+ print e.sc["team"].channels
+ #c = SlackChannel(e, **json.loads(chan))
+ c = e.sc["team"].channels['G3ZGMF4RZ']
+
+ print c.formatted_name()
+ c.is_someone_typing = Mock(return_value=True)
+ c.channel_buffer = Mock(return_value=True)
+ print c.create_buffer()
+ print c.rename()
+ print c.current_short_name
+ print c.formatted_name()
+ print c.rename()
+ print c.formatted_name()
+# assert False
diff --git a/_pytest/test_slackrequest.py b/_pytest/test_slackrequest.py
new file mode 100644
index 0000000..081330e
--- /dev/null
+++ b/_pytest/test_slackrequest.py
@@ -0,0 +1,11 @@
+from wee_slack import SlackRequest, EventRouter
+
+def test_SlackRequest():
+ s = SlackRequest('xoxoxoxox', "blah.get", {"meh": "blah"})
+ print s
+
+ e = EventRouter()
+ e.receive(s)
+ e.handle_next()
+ #assert False
+
diff --git a/_pytest/test_slackteam.py b/_pytest/test_slackteam.py
new file mode 100644
index 0000000..fa6ac6a
--- /dev/null
+++ b/_pytest/test_slackteam.py
@@ -0,0 +1,10 @@
+from wee_slack import SlackTeam, EventRouter
+
+def test_SlackTeam():
+ e = EventRouter()
+ #s = SlackTeam('xoxo')
+ #e.register_team(s)
+ #print e.queue
+ #print e.handle_next()
+ #assert False
+
diff --git a/_pytest/test_slackts.py b/_pytest/test_slackts.py
new file mode 100644
index 0000000..87f43a7
--- /dev/null
+++ b/_pytest/test_slackts.py
@@ -0,0 +1,24 @@
+from wee_slack import SlackTS
+
+
+def test_slackts():
+ base = SlackTS("1485976156.000017")
+
+ b = SlackTS("1485976156.000016")
+ c = SlackTS("1485976156.000018")
+
+ d = SlackTS("1485976155.000017")
+ e = SlackTS("1485976157.000017")
+
+ assert base > b
+ assert base < c
+
+ assert base > d
+ assert base < e
+
+ c = SlackTS()
+ assert c > base
+
+ assert base == "1485976156.000017"
+ assert base > "1485976156.000016"
+ assert base < "1485976156.000018"
diff --git a/_pytest/test_unfurl.py b/_pytest/test_unfurl.py
index 2168aed..b631888 100644
--- a/_pytest/test_unfurl.py
+++ b/_pytest/test_unfurl.py
@@ -20,7 +20,7 @@ slack = wee_slack
},
{
'input': "foo <#C2147483705> foo",
- 'output': "foo #testchan foo",
+ 'output': "foo #test-chan foo",
},
{
'input': "url: <https://example.com|example> suffix",
@@ -34,13 +34,20 @@ slack = wee_slack
'input': "<@U2147483697|@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",
+ },
))
-def test_unfurl_refs(myservers, mychannels, myusers, case):
- slack.servers = myservers
- slack.channels = mychannels
- slack.users = myusers
- slack.message_cache = {}
- slack.servers[0].users = myusers
- print mychannels[0].identifier
+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']
- assert slack.unfurl_refs(case['input'], ignore_alt_text=case.get('ignore_alt_text', False)) == case['output']
diff --git a/wee_slack.py b/wee_slack.py
index ed96f03..e8b736c 100644
--- a/wee_slack.py
+++ b/wee_slack.py
@@ -1,20 +1,21 @@
-# -*- coding: utf-8 -*-
+#-*- coding: utf-8 -*-
#
from functools import wraps
import time
import json
-import os
import pickle
import sha
+import os
import re
import urllib
-import HTMLParser
import sys
import traceback
-import collections
+#import collections
import ssl
+import random
+import string
from websocket import create_connection, WebSocketConnectionClosedException
@@ -24,9 +25,9 @@ try:
except:
pass
-SCRIPT_NAME = "slack_extension"
+SCRIPT_NAME = "slack"
SCRIPT_AUTHOR = "Ryan Huber <rhuber@gmail.com>"
-SCRIPT_VERSION = "0.99.9"
+SCRIPT_VERSION = "1.99"
SCRIPT_LICENSE = "MIT"
SCRIPT_DESC = "Extends weechat for typing notification/search/etc on slack.com"
@@ -35,6 +36,8 @@ SCROLLBACK_SIZE = 500
CACHE_VERSION = "4"
+RECORD_DIR = "/tmp/weeslack-debug"
+
SLACK_API_TRANSLATOR = {
"channel": {
"history": "channels.history",
@@ -54,10 +57,42 @@ SLACK_API_TRANSLATOR = {
"join": "channels.join",
"leave": "groups.leave",
"mark": "groups.mark",
+ },
+ "thread": {
+ "history": None,
+ "join": None,
+ "leave": None,
+ "mark": None,
}
+
}
+###### 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
+ """
+ @wraps(f)
+ def wrapper(data, current_buffer, *args, **kwargs):
+ if current_buffer not in EVENTROUTER.weechat_controller.buffers:
+ return w.WEECHAT_RC_OK
+ 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
+ """
+ @wraps(f)
+ def wrapper(data, current_buffer, *args, **kwargs):
+ if current_buffer not in EVENTROUTER.weechat_controller.buffers:
+ return w.WEECHAT_RC_ERROR
+ return f(data, current_buffer, *args, **kwargs)
+ return wrapper
+
+
NICK_GROUP_HERE = "0|Here"
NICK_GROUP_AWAY = "1|Away"
@@ -67,334 +102,436 @@ 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}
+##### BEGIN NEW
-def dbg(message, fout=False, main_buffer=False):
- """
- send debug output to the slack-debug buffer and optionally write to a file.
- """
- 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("", "slack: " + message)
- else:
- if slack_debug is not None:
- w.prnt(slack_debug, message)
+IGNORED_EVENTS = [
+ "hello",
+ #"pref_change",
+ #"reconnect_url",
+]
+###### New central Event router
-class SearchList(list):
- """
- A normal python list with some syntactic sugar for searchability
- """
- def __init__(self):
- self.hashtable = {}
- super(SearchList, self).__init__(self)
-
- def find(self, name):
- if name in self.hashtable:
- return self.hashtable[name]
- # this is a fallback to __eq__ if the item isn't in the hashtable already
- if name in self:
- self.update_hashtable()
- return self[self.index(name)]
-
- def append(self, item, aliases=[]):
- super(SearchList, self).append(item)
- self.update_hashtable(item)
-
- def update_hashtable(self, item=None):
- if item is not None:
- try:
- for alias in item.get_aliases():
- if alias is not None:
- self.hashtable[alias] = item
- except AttributeError:
- pass
- else:
- for child in self:
- try:
- for alias in child.get_aliases():
- if alias is not None:
- self.hashtable[alias] = child
- except AttributeError:
- pass
-
- def find_by_class(self, class_name):
- items = []
- for child in self:
- if child.__class__ == class_name:
- items.append(child)
- return items
-
- def find_by_class_deep(self, class_name, attribute):
- items = []
- for child in self:
- if child.__class__ == self.__class__:
- items += child.find_by_class_deep(class_name, attribute)
- else:
- items += (eval('child.' + attribute).find_by_class(class_name))
- return items
-
+class EventRouter(object):
-class SlackServer(object):
- """
- Root object used to represent connection and state of the connection to a slack group.
- """
- def __init__(self, token):
- self.nick = None
- self.name = None
- self.team = None
- self.domain = None
- self.server_buffer_name = None
- self.login_data = None
- self.buffer = None
- self.token = token
- self.ws = None
- self.ws_hook = None
- self.users = SearchList()
- self.bots = SearchList()
- self.channels = SearchList()
- self.connecting = False
- self.connected = False
- self.connection_attempt_time = 0
- self.communication_counter = 0
- self.message_buffer = {}
- self.ping_hook = None
- self.alias = None
- self.got_history = False
-
- self.identifier = None
- self.connect_to_slack()
+ def __init__(self):
+ """
+ complete
+ Eventrouter is the central hub we use to route:
+ 1) incoming websocket data
+ 2) outgoing http requests and incoming replies
+ 3) local requests
+ It has a recorder that, when enabled, logs most events
+ to the location specified in RECORD_DIR.
+ """
+ self.queue = []
+ self.slow_queue = []
+ self.slow_queue_timer = 0
+ self.teams = {}
+ self.context = {}
+ self.weechat_controller = WeechatController(self)
+ self.previous_buffer = ""
+ self.reply_buffer = {}
+ self.cmds = {k[8:]: v for k, v in globals().items() if k.startswith("command_")}
+ self.proc = {k[8:]: v for k, v in globals().items() if k.startswith("process_")}
+ self.handlers = {k[7:]: v for k, v in globals().items() if k.startswith("handle_")}
+ self.local_proc = {k[14:]: v for k, v in globals().items() if k.startswith("local_process_")}
+ self.shutting_down = False
+ self.recording = False
+ self.recording_path = "/tmp"
+
+ def record(self):
+ """
+ complete
+ Toggles the event recorder and creates a directory for data if enabled.
+ """
+ self.recording = not self.recording
+ if self.recording:
+ if not os.path.exists(RECORD_DIR):
+ os.makedirs(RECORD_DIR)
- def __eq__(self, compare_str):
- if compare_str == self.identifier or compare_str == self.token or compare_str == self.buffer:
- return True
+ def record_event(self, message_json, file_name_field, subdir=None):
+ """
+ complete
+ Called each time you want to record an event.
+ message_json is a json in dict form
+ file_name_field is the json key whose value you want to be part of the file name
+ """
+ now = time.time()
+ if subdir:
+ directory = "{}/{}".format(RECORD_DIR, subdir)
else:
- return False
-
- def __str__(self):
- return "{}".format(self.identifier)
-
- def __repr__(self):
- return "{}".format(self.identifier)
-
- def add_user(self, user):
- self.users.append(user, user.get_aliases())
- users.append(user, user.get_aliases())
+ directory = RECORD_DIR
+ if not os.path.exists(directory):
+ os.makedirs(directory)
+ mtype = message_json.get(file_name_field, 'unknown')
+ f = open('{}/{}-{}.json'.format(directory, now, mtype), 'w')
+ f.write("{}".format(json.dumps(message_json)))
+ f.close()
+
+ def store_context(self, data):
+ """
+ A place to store data and vars needed by callback returns. We need this because
+ weechat's "callback_data" has a limited size and weechat will crash if you exceed
+ this size.
+ """
+ identifier = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(40))
+ self.context[identifier] = data
+ dbg("stored context {} {} ".format(identifier, data.url))
+ return identifier
- def add_bot(self, bot):
- self.bots.append(bot)
+ def retrieve_context(self, identifier):
+ """
+ A place to retrieve data and vars needed by callback returns. We need this because
+ weechat's "callback_data" has a limited size and weechat will crash if you exceed
+ this size.
+ """
+ data = self.context.get(identifier, None)
+ if data:
+ #dbg("retrieved eontext {} ".format(identifier))
+ return data
- def add_channel(self, channel):
- self.channels.append(channel, channel.get_aliases())
- channels.append(channel, channel.get_aliases())
+ def delete_context(self, identifier):
+ """
+ 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))
+ del self.context[identifier]
- def get_aliases(self):
- aliases = filter(None, [self.identifier, self.token, self.buffer, self.alias])
- return aliases
+ def shutdown(self):
+ """
+ complete
+ This toggles shutdown mode. Shutdown mode tells us not to
+ talk to Slack anymore. Without this, typing /quit will trigger
+ a race with the buffer close callback and may result in you
+ leaving every slack channel.
+ """
+ self.shutting_down = not self.shutting_down
- def find(self, name, attribute):
- attribute = eval("self." + attribute)
- return attribute.find(name)
+ def register_team(self, team):
+ """
+ complete
+ Adds a team to the list of known teams for this EventRouter.
+ """
+ if isinstance(team, SlackTeam):
+ self.teams[team.get_team_hash()] = team
+ else:
+ raise InvalidType(type(team))
- def get_communication_id(self):
- if self.communication_counter > 999:
- self.communication_counter = 0
- self.communication_counter += 1
- return self.communication_counter
+ def reconnect_if_disconnected(self):
+ for team_id, team in self.teams.iteritems():
+ if not team.connected:
+ team.connect()
+ dbg("reconnecting {}".format(team))
- def send_to_websocket(self, data, expect_reply=True):
- data["id"] = self.get_communication_id()
- message = json.dumps(data)
+ def receive_ws_callback(self, team_hash):
+ """
+ incomplete (reconnect)
+ This is called by the global method of the same name.
+ It is triggered when we have incoming data on a websocket,
+ which needs to be read. Once it is read, we will ensure
+ the data is valid JSON, add metadata, and place it back
+ on the queue for processing as JSON.
+ """
try:
- if expect_reply:
- self.message_buffer[data["id"]] = data
- self.ws.send(message)
- dbg("Sent {}...".format(message[:100]))
- except:
- dbg("Unexpected error: {}\nSent: {}".format(sys.exc_info()[0], data))
- self.connected = False
-
- def ping(self):
- request = {"type": "ping"}
- self.send_to_websocket(request)
+ # Read the data from the websocket associated with this team.
+ data = self.teams[team_hash].ws.recv()
+ message_json = json.loads(data)
+ metadata = WeeSlackMetadata({
+ "team": team_hash,
+ }).jsonify()
+ message_json["wee_slack_metadata"] = metadata
+ if self.recording:
+ self.record_event(message_json, 'type', 'websocket')
+ self.receive_json(json.dumps(message_json))
+ except WebSocketConnectionClosedException:
+ #TODO: handle reconnect here
+ self.teams[team_hash].set_disconnected()
+ return w.WEECHAT_RC_OK
+ except Exception:
+ dbg("socket issue: {}\n".format(traceback.format_exc()))
+ return w.WEECHAT_RC_OK
- def should_connect(self):
+ def receive_httprequest_callback(self, data, command, return_code, out, err):
"""
- If we haven't tried to connect OR we tried and never heard back and it
- has been 125 seconds consider the attempt dead and try again
+ complete
+ Receives the result of an http request we previously handed
+ off to weechat (weechat bundles libcurl). Weechat can fragment
+ replies, so it buffers them until the reply is complete.
+ It is then populated with metadata here so we can identify
+ where the request originated and route properly.
"""
- if self.connection_attempt_time == 0 or self.connection_attempt_time + 125 < int(time.time()):
- return True
+ request_metadata = self.retrieve_context(data)
+ try:
+ dbg("RECEIVED CALLBACK with request of {} id of {} and code {} of length {}".format(request_metadata.request, request_metadata.response_id, return_code, len(out)))
+ except:
+ dbg(request_metadata)
+ 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
+ try:
+ j = json.loads(self.reply_buffer[request_metadata.response_id])
+ except:
+ pass
+ #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)
+ self.reply_buffer.pop(request_metadata.response_id)
+ if self.recording:
+ self.record_event(j, 'wee_slack_process_method', 'http')
+ self.receive_json(json.dumps(j))
+ self.delete_context(data)
+ except:
+ dbg("HTTP REQUEST CALLBACK FAILED", True)
+ pass
+ # We got an empty reply and this is weird so just ditch it and retry
+ else:
+ dbg("length was zero, probably a bug..")
+ self.delete_context(data)
+ self.receive(request_metadata)
+ elif return_code != -1:
+ self.reply_buffer.pop(request_metadata.response_id, None)
+ self.delete_context(data)
else:
- return False
+ 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
- def connect_to_slack(self):
- t = time.time()
- # Double check that we haven't exceeded a long wait to connect and try again.
- if self.connecting and self.should_connect():
- self.connecting = False
- if not self.connecting:
- async_slack_api_request("slack.com", self.token, "rtm.start", {"ts": t})
- self.connection_attempt_time = int(time.time())
- self.connecting = True
+ def receive_json(self, data):
+ """
+ complete
+ Receives a raw JSON string from and unmarshals it
+ as dict, then places it back on the queue for processing.
+ """
+ dbg("RECEIVED JSON of len {}".format(len(data)))
+ message_json = json.loads(data)
+ self.queue.append(message_json)
+ def receive(self, dataobj):
+ """
+ complete
+ Receives a raw object and places it on the queue for
+ processing. Object must be known to handle_next or
+ be JSON.
+ """
+ dbg("RECEIVED FROM QUEUE")
+ self.queue.append(dataobj)
+ def receive_slow(self, dataobj):
+ """
+ complete
+ Receives a raw object and places it on the slow queue for
+ processing. Object must be known to handle_next or
+ be JSON.
+ """
+ dbg("RECEIVED FROM QUEUE")
+ self.slow_queue.append(dataobj)
+ def handle_next(self):
+ """
+ complete
+ Main handler of the EventRouter. This is called repeatedly
+ via callback to drain events from the queue. It also attaches
+ 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]:
+ dbg("from slow queue", 0)
+ self.queue.append(self.slow_queue.pop())
+ #self.slow_queue = []
+ self.slow_queue_timer = time.time()
+ if len(self.queue) > 0:
+ j = self.queue.pop(0)
+ # Reply is a special case of a json reply from websocket.
+ kwargs = {}
+ if isinstance(j, SlackRequest):
+ if j.should_try():
+ if j.retry_ready():
+ local_process_async_slack_api_request(j, self)
+ else:
+ self.slow_queue.append(j)
+ else:
+ dbg("Max retries for Slackrequest")
- def connected_to_slack(self, login_data):
- if login_data["ok"]:
- self.team = login_data["team"]["domain"]
- self.domain = login_data["team"]["domain"] + ".slack.com"
- dbg("connected to {}".format(self.domain))
- self.identifier = self.domain
-
- alias = w.config_get_plugin("server_alias.{}".format(login_data["team"]["domain"]))
- if alias:
- self.server_buffer_name = alias
- self.alias = alias
else:
- self.server_buffer_name = self.domain
-
- self.nick = login_data["self"]["name"]
- self.create_local_buffer()
- if self.create_slack_websocket(login_data):
- if self.ping_hook:
- w.unhook(self.ping_hook)
- self.communication_counter = 0
- self.ping_hook = w.hook_timer(1000 * 5, 0, 0, "slack_ping_cb", self.domain)
- if len(self.users) == 0 or len(self.channels) == 0:
- self.create_slack_mappings(login_data)
-
- self.connected = True
- self.connecting = False
+ if "reply_to" in j:
+ dbg("SET FROM REPLY")
+ function_name = "reply"
+ elif "type" in j:
+ dbg("SET FROM type")
+ function_name = j["type"]
+ elif "wee_slack_process_method" in j:
+ dbg("SET FROM META")
+ function_name = j["wee_slack_process_method"]
+ else:
+ dbg("SET FROM NADA")
+ function_name = "unknown"
+
+ # Here we are passing the actual objects. No more lookups.
+ meta = j.get("wee_slack_metadata", None)
+ if meta:
+ try:
+ if isinstance(meta, str):
+ dbg("string of metadata")
+ team = meta.get("team", None)
+ if team:
+ kwargs["team"] = self.teams[team]
+ if "user" in j:
+ kwargs["user"] = self.teams[team].users[j["user"]]
+ if "channel" in j:
+ kwargs["channel"] = self.teams[team].channels[j["channel"]]
+ except:
+ dbg("metadata failure")
+
+ if function_name not in IGNORED_EVENTS:
+ dbg("running {}".format(function_name))
+ if function_name.startswith("local_") and function_name in self.local_proc:
+ self.local_proc[function_name](j, self, **kwargs)
+ elif function_name in self.proc:
+ self.proc[function_name](j, self, **kwargs)
+ elif function_name in self.handlers:
+ self.handlers[function_name](j, self, **kwargs)
+ else:
+ raise ProcessNotImplemented(function_name)
- self.print_connection_info(login_data)
- if len(self.message_buffer) > 0:
- for message_id in self.message_buffer.keys():
- if self.message_buffer[message_id]["type"] != 'ping':
- resend = self.message_buffer.pop(message_id)
- dbg("Resent failed message.")
- self.send_to_websocket(resend)
- # sleep to prevent being disconnected by websocket server
- time.sleep(1)
- else:
- self.message_buffer.pop(message_id)
- for chan in self.channels:
- # Set channel history back to false because we will miss messages that came
- # while we were disconnected otherwise.
- chan.got_history = False
- if chan.channel_buffer and chan.muted:
- w.buffer_set(chan.channel_buffer, "hotlist", "-1")
- return True
+def handle_next(*args):
+ """
+ complete
+ This is just a place to call the event router globally.
+ This is a dirty hack. There must be a better way.
+ """
+ try:
+ EVENTROUTER.handle_next()
+ except:
+ if config.debug_mode:
+ traceback.print_exc()
else:
- token_start = self.token[:10]
- error = """
-!! slack.com login error: {}
- The problematic token starts with {}
- Please check your API token with
- "/set plugins.var.python.slack_extension.slack_api_token (token)"
-
-""".format(login_data["error"], token_start)
- w.prnt("", error)
- self.connected = False
-
- def print_connection_info(self, login_data):
- self.buffer_prnt('Connected to Slack', backlog=True)
- self.buffer_prnt('{:<20} {}'.format(u"Websocket URL", login_data["url"]), backlog=True)
- self.buffer_prnt('{:<20} {}'.format(u"User name", login_data["self"]["name"]), backlog=True)
- self.buffer_prnt('{:<20} {}'.format(u"User ID", login_data["self"]["id"]), backlog=True)
- self.buffer_prnt('{:<20} {}'.format(u"Team name", login_data["team"]["name"]), backlog=True)
- self.buffer_prnt('{:<20} {}'.format(u"Team domain", login_data["team"]["domain"]), backlog=True)
- self.buffer_prnt('{:<20} {}'.format(u"Team id", login_data["team"]["id"]), backlog=True)
-
- def create_local_buffer(self):
- if not w.buffer_search("", self.server_buffer_name):
- self.buffer = w.buffer_new(self.server_buffer_name, "buffer_input_cb", "", "", "")
- if w.config_string(w.config_get('irc.look.server_buffer')) == 'merge_with_core':
- w.buffer_merge(self.buffer, w.buffer_search_main())
- w.buffer_set(self.buffer, "nicklist", "1")
-
- def create_slack_websocket(self, data):
- web_socket_url = data['url']
- try:
- self.ws = create_connection(web_socket_url, sslopt=sslopt_ca_certs)
- self.ws_hook = w.hook_fd(self.ws.sock._sock.fileno(), 1, 0, 0, "slack_websocket_cb", self.identifier)
- self.ws.sock.setblocking(0)
- return True
- except Exception as e:
- print("websocket connection error: {}".format(e))
- return False
-
- def create_slack_mappings(self, data):
-
- for item in data["users"]:
- self.add_user(User(self, item["name"], item["id"], item["presence"], item["deleted"], is_bot=item.get('is_bot', False)))
-
- for item in data["bots"]:
- self.add_bot(Bot(self, item["name"], item["id"], item["deleted"]))
-
- for item in data["channels"]:
- item["is_open"] = item["is_member"]
- item["prepend_name"] = "#"
- if not item["is_archived"]:
- self.add_channel(Channel(self, **item))
+ pass
+ return w.WEECHAT_RC_OK
- for item in data["groups"]:
- item["prepend_name"] = "#"
- if not item["is_archived"]:
- if item["name"].startswith("mpdm-"):
- self.add_channel(MpdmChannel(self, **item))
- else:
- self.add_channel(GroupChannel(self, **item))
-
- for item in data["ims"]:
- if item["unread_count"] > 0 or item["is_open"]:
- item["is_open"] = True
- item['name'] = self.users.find(item["user"]).name
- self.add_channel(DmChannel(self, **item))
-
- for item in data['self']['prefs']['muted_channels'].split(','):
- if item == '':
- continue
- maybe_muted_chan = self.channels.find(item)
- if maybe_muted_chan is not None:
- maybe_muted_chan.muted = True
-
- #for item in self.channels:
- # item.get_history()
-
- def buffer_prnt(self, message='no message', user="SYSTEM", backlog=False):
- message = message.encode('ascii', 'ignore')
- if backlog:
- tags = "no_highlight,notify_none,logger_backlog_end"
+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):
+ self.buffers[buffer_ptr] = channel
else:
- tags = ""
- if user == "SYSTEM":
- user = w.config_string(w.config_get('weechat.look.prefix_network'))
- if self.buffer:
- w.prnt_date_tags(self.buffer, 0, tags, "{}\t{}".format(user, message))
+ 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):
+ try:
+ self.buffers[buffer_ptr].destroy_buffer(update_remote)
+ if close_buffer:
+ w.buffer_close(buffer_ptr)
+ del self.buffers[buffer_ptr]
+ except:
+ dbg("Tried to close unknown buffer")
else:
- pass
- # w.prnt("", "%s\t%s" % (user, message))
+ 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
+ Sends an API request to Slack. You'll need to give this a well formed SlackRequest object.
+ DEBUGGING!!! The context here cannot be very large. Weechat will crash.
+ """
+ if not event_router.shutting_down:
+ weechat_request = 'url:{}'.format(request.request_string())
+ weechat_request += '&nonce={}'.format(''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(4)))
+ 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
+ w.hook_process_hashtable('url:', params, config.slack_timeout, "", context)
+ w.hook_process_hashtable(weechat_request, params, config.slack_timeout, "receive_httprequest_callback", context)
- def set_away(self, msg):
- async_slack_api_request(self.domain, self.token, 'presence.set', {"presence": "away"})
- for c in self.channels:
- if c.channel_buffer is not None:
- w.buffer_set(c.channel_buffer, "localvar_set_away", msg)
+###### New Callbacks
- def set_active(self):
- async_slack_api_request(self.domain, self.token, 'presence.set', {"presence": "active"})
- for c in self.channels:
- if c.channel_buffer is not None:
- w.buffer_set(c.channel_buffer, "localvar_set_away", '')
- w.buffer_set(c.channel_buffer, "localvar_del_away", '')
+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):
+ EVENTROUTER.receive_httprequest_callback(data, command, return_code, out, err)
+ return w.WEECHAT_RC_OK
+def receive_ws_callback(*args):
+ """
+ complete
+ The first arg is all we want here. It contains the team
+ hash which is set when we _hook the descriptor.
+ This is a dirty hack. There must be a better way.
+ """
+ EVENTROUTER.receive_ws_callback(args[0])
+ return w.WEECHAT_RC_OK
+
+def reconnect_callback(*args):
+ EVENTROUTER.reconnect_if_disconnected()
+ return w.WEECHAT_RC_OK
-def buffer_input_cb(b, buffer, data):
- channel = channels.find(buffer)
+def buffer_closing_callback(signal, sig_type, data):
+ """
+ complete
+ Receives a callback from weechat when a buffer is being closed.
+ We pass the eventrouter variable name in as a string, as
+ that is the only way we can do dependency injection via weechat
+ callback, hence the eval.
+ """
+ eval(signal).weechat_controller.unregister_buffer(data, True, False)
+ return w.WEECHAT_RC_OK
+
+def buffer_input_callback(signal, buffer_ptr, data):
+ """
+ incomplete
+ Handles everything a user types in the input bar. In our case
+ this includes add/remove reactions, modifying messages, and
+ sending messages.
+ """
+ eventrouter = eval(signal)
+ channel = eventrouter.weechat_controller.get_channel_from_buffer_ptr(buffer_ptr)
if not channel:
return w.WEECHAT_RC_OK_EAT
+
reaction = re.match("^\s*(\d*)(\+|-):(.*):\s*$", data)
if reaction:
if reaction.group(2) == "+":
@@ -411,598 +548,1095 @@ def buffer_input_cb(b, buffer, data):
# rid of escapes.
new = new.replace(r'\/', '/')
old = old.replace(r'\/', '/')
- channel.change_previous_message(old.decode("utf-8"), new.decode("utf-8"), flags)
+ channel.edit_previous_message(old.decode("utf-8"), new.decode("utf-8"), flags)
else:
channel.send_message(data)
- # channel.buffer_prnt(channel.server.nick, data)
- channel.mark_read(True)
+ #this is probably wrong channel.mark_read(update_remote=True, force=True)
return w.WEECHAT_RC_ERROR
+def buffer_switch_callback(signal, sig_type, data):
+ """
+ incomplete
+ Every time we change channels in weechat, we call this to:
+ 1) set read marker 2) determine if we have already populated
+ channel history data
+ """
+ eventrouter = eval(signal)
+
+ prev_buffer_ptr = eventrouter.weechat_controller.get_previous_buffer_ptr()
+ # this is to see if we need to gray out things in the buffer list
+ prev = eventrouter.weechat_controller.get_channel_from_buffer_ptr(prev_buffer_ptr)
+ if prev:
+ prev.mark_read()
+
+ new_channel = eventrouter.weechat_controller.get_channel_from_buffer_ptr(data)
+ if new_channel:
+ if not new_channel.got_history:
+ new_channel.get_history()
+
+ eventrouter.weechat_controller.set_previous_buffer(data)
+ return w.WEECHAT_RC_OK
-class Channel(object):
+def buffer_list_update_callback(data, somecount):
"""
- Represents a single channel and is the source of truth
- for channel <> weechat buffer
+ incomplete
+ A simple timer-based callback that will update the buffer list
+ if needed. We only do this max 1x per second, as otherwise it
+ uses a lot of cpu for minimal changes. We use buffer short names
+ to indicate typing via "#channel" <-> ">channel" and
+ user presence via " name" <-> "+name".
"""
- #def __init__(self, server, name, identifier, active, last_read=0, prepend_name="", members=[], topic="", unread_count=0):
- def __init__(self, server, **kwargs):
+ eventrouter = eval(data)
+ #global buffer_list_update
+
+ for b in eventrouter.weechat_controller.iter_buffers():
+ b[1].refresh()
+# buffer_list_update = True
+# if eventrouter.weechat_controller.check_refresh_buffer_list():
+# # gray_check = False
+# # if len(servers) > 1:
+# # gray_check = True
+# eventrouter.weechat_controller.set_refresh_buffer_list(False)
+ return w.WEECHAT_RC_OK
- self.name = kwargs.get('prepend_name', "") + kwargs.get('name')
- self.current_short_name = kwargs.get('prepend_name', "") + kwargs.get('name')
- self.identifier = kwargs.get('id', 0)
- self.active = kwargs.get('is_open', False)
- self.last_read = float(kwargs.get('last_read', 0))
- self.members = set(kwargs.get('members', []))
- self.topic = kwargs.get('topic', {"value": ""})["value"]
- self.unread_count = kwargs.get('unread_count_display', 0)
+def quit_notification_callback(signal, sig_type, data):
+ stop_talking_to_slack()
- self.members_table = {}
- self.channel_buffer = None
- self.type = "channel"
- self.server = server
- self.typing = {}
- self.last_received = None
- self.messages = []
- self.scrolling = False
- self.last_active_user = None
- self.muted = False
- self.got_history = False
- #w.prnt("", "unread: {}".format(self.unread_count))
- if self.active:
- self.create_buffer()
- self.attach_buffer()
- self.create_members_table()
- self.update_nicklist()
- self.set_topic(self.topic)
- buffer_list_update_next()
+def typing_notification_cb(signal, sig_type, data):
+ msg = w.buffer_get_string(data, "input")
+ if len(msg) > 8 and msg[:1] != "/":
+ global typing_timer
+ now = time.time()
+ if typing_timer + 4 < now:
+ current_buffer = w.current_buffer()
+ channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer, None)
+ if channel:
+ 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
- def __str__(self):
- return self.name
+def typing_update_cb(data, remaining_calls):
+ w.bar_item_update("slack_typing_notice")
+ return w.WEECHAT_RC_OK
- def __repr__(self):
- return self.name
+def slack_never_away_cb(data, remaining_calls):
+ if config.never_away:
+ for t in EVENTROUTER.teams.values():
+ slackbot = t.get_channel_map()['slackbot']
+ channel = t.channels[slackbot]
+ request = {"type": "typing", "channel": channel.identifier}
+ channel.team.send_to_websocket(request, expect_reply=False)
+ return w.WEECHAT_RC_OK
- def __eq__(self, compare_str):
- if compare_str == self.fullname() or compare_str == self.name or compare_str == self.identifier or compare_str == self.name[1:] or (compare_str == self.channel_buffer and self.channel_buffer is not None):
- return True
- else:
- return False
+def typing_bar_item_cb(data, current_buffer, args):
+ """
+ Privides a bar item indicating who is typing in the current channel AND
+ why is typing a DM to you globally.
+ """
+ typers = []
+ current_buffer = w.current_buffer()
+ current_channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer, None)
- def get_aliases(self):
- aliases = [self.fullname(), self.name, self.identifier, self.name[1:], ]
- if self.channel_buffer is not None:
- aliases.append(self.channel_buffer)
- return aliases
+ # first look for people typing in this channel
+ if current_channel:
+ # this try is mostly becuase server buffers don't implement is_someone_typing
+ try:
+ if current_channel.type != 'im' and current_channel.is_someone_typing():
+ typers += current_channel.get_typing_list()
+ except:
+ pass
- def create_members_table(self):
- for user in self.members:
- self.members_table[user] = self.server.users.find(user)
+ # here is where we notify you that someone is typing in DM
+ # regardless of which buffer you are in currently
+ for t in EVENTROUTER.teams.values():
+ for channel in t.channels.values():
+ if channel.type == "im":
+ if channel.is_someone_typing():
+ typers.append("D/" + channel.slack_name)
+ pass
- def create_buffer(self):
- channel_buffer = w.buffer_search("", "{}.{}".format(self.server.server_buffer_name, self.name))
- if channel_buffer:
- self.channel_buffer = channel_buffer
- else:
- self.channel_buffer = w.buffer_new("{}.{}".format(self.server.server_buffer_name, self.name), "buffer_input_cb", self.name, "", "")
- if self.type == "im":
- w.buffer_set(self.channel_buffer, "localvar_set_type", 'private')
- else:
- w.buffer_set(self.channel_buffer, "localvar_set_type", 'channel')
- if self.server.alias:
- w.buffer_set(self.channel_buffer, "localvar_set_server", self.server.alias)
- else:
- w.buffer_set(self.channel_buffer, "localvar_set_server", self.server.team)
- w.buffer_set(self.channel_buffer, "localvar_set_channel", self.name)
- w.buffer_set(self.channel_buffer, "short_name", self.name)
- buffer_list_update_next()
- if self.unread_count != 0 and not self.muted:
- w.buffer_set(self.channel_buffer, "hotlist", "1")
-
- def attach_buffer(self):
- channel_buffer = w.buffer_search("", "{}.{}".format(self.server.server_buffer_name, self.name))
- if channel_buffer != main_weechat_buffer:
- self.channel_buffer = channel_buffer
- w.buffer_set(self.channel_buffer, "localvar_set_nick", self.server.nick)
- w.buffer_set(self.channel_buffer, "highlight_words", self.server.nick)
- else:
- self.channel_buffer = None
- channels.update_hashtable()
- self.server.channels.update_hashtable()
+ typing = ", ".join(typers)
+ if typing != "":
+ typing = w.color('yellow') + "typing: " + typing
- def detach_buffer(self):
- if self.channel_buffer is not None:
- w.buffer_close(self.channel_buffer)
- self.channel_buffer = None
- channels.update_hashtable()
- self.server.channels.update_hashtable()
+ return typing
- def update_nicklist(self, user=None):
- if not self.channel_buffer:
- return
+def nick_completion_cb(data, completion_item, current_buffer, completion):
+ """
+ Adds all @-prefixed nicks to completion list
+ """
- w.buffer_set(self.channel_buffer, "nicklist", "1")
+ current_buffer = w.current_buffer()
+ current_channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer, None)
- # create nicklists for the current channel if they don't exist
- # if they do, use the existing pointer
- 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:
- user = self.members_table[user]
- 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..
- w.nicklist_add_nick(self.channel_buffer, here, user.name, user.color_name, "", "", 1)
+ if current_channel is None or current_channel.members is None:
+ return w.WEECHAT_RC_OK
+ 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)
+ return w.WEECHAT_RC_OK
- # if we didn't get a user, build a complete list. this is expensive.
- else:
- try:
- for user in self.members:
- user = self.members_table[user]
- if user.deleted:
- continue
- w.nicklist_add_nick(self.channel_buffer, here, user.name, user.color_name, "", "", 1)
- except Exception as e:
- dbg("DEBUG: {} {} {}".format(self.identifier, self.name, e))
-
- def fullname(self):
- return "{}.{}".format(self.server.server_buffer_name, self.name)
-
- def has_user(self, name):
- return name in self.members
-
- def user_join(self, name):
- self.members.add(name)
- self.create_members_table()
- self.update_nicklist()
+def emoji_completion_cb(data, completion_item, current_buffer, completion):
+ """
+ Adds all :-prefixed emoji to completion list
+ """
- def user_leave(self, name):
- if name in self.members:
- self.members.remove(name)
- self.create_members_table()
- self.update_nicklist()
+ current_buffer = w.current_buffer()
+ current_channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer, None)
- def set_active(self):
- self.active = True
+ if current_channel is None:
+ return w.WEECHAT_RC_OK
+ for e in EMOJI['emoji']:
+ w.hook_completion_list_add(completion, ":" + e + ":", 0, w.WEECHAT_LIST_POS_SORT)
+ return w.WEECHAT_RC_OK
- def set_inactive(self):
- self.active = False
+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
+ completion lists, then let Weechat's internal completion do its
+ thing
- def set_typing(self, user):
- if self.channel_buffer:
- if w.buffer_get_integer(self.channel_buffer, "hidden") == 0:
- self.typing[user] = time.time()
- buffer_list_update_next()
+ """
- def unset_typing(self, user):
- if self.channel_buffer:
- if w.buffer_get_integer(self.channel_buffer, "hidden") == 0:
- try:
- del self.typing[user]
- buffer_list_update_next()
- except:
- pass
+ current_buffer = w.current_buffer()
+ current_channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer, None)
- def send_message(self, message):
- message = self.linkify_text(message)
- dbg(message)
- request = {"type": "message", "channel": self.identifier, "text": message, "_server": self.server.domain}
- self.server.send_to_websocket(request)
-
- def linkify_text(self, message):
- message = message.split(' ')
- for item in enumerate(message):
- targets = re.match('.*([@#])([\w.]+\w)(\W*)', item[1])
- if targets and targets.groups()[0] == '@':
- named = targets.groups()
- if named[1] in ["group", "channel", "here"]:
- message[item[0]] = "<!{}>".format(named[1])
- if self.server.users.find(named[1]):
- message[item[0]] = "<@{}>{}".format(self.server.users.find(named[1]).identifier, named[2])
- if targets and targets.groups()[0] == '#':
- named = targets.groups()
- if self.server.channels.find(named[1]):
- message[item[0]] = "<#{}|{}>{}".format(self.server.channels.find(named[1]).identifier, named[1], named[2])
- dbg(message)
- return " ".join(message)
+ #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
- def set_topic(self, topic):
- self.topic = topic.encode('utf-8')
- w.buffer_set(self.channel_buffer, "title", self.topic)
+ line_input = w.buffer_get_string(current_buffer, "input")
+ current_pos = w.buffer_get_integer(current_buffer, "input_pos") - 1
+ input_length = w.buffer_get_integer(current_buffer, "input_length")
- def open(self, update_remote=True):
- self.create_buffer()
- self.active = True
- self.get_history()
- if "info" in SLACK_API_TRANSLATOR[self.type]:
- async_slack_api_request(self.server.domain, self.server.token, SLACK_API_TRANSLATOR[self.type]["info"], {"name": self.name.lstrip("#")})
- if update_remote:
- if "join" in SLACK_API_TRANSLATOR[self.type]:
- async_slack_api_request(self.server.domain, self.server.token, SLACK_API_TRANSLATOR[self.type]["join"], {"name": self.name.lstrip("#")})
-
- def close(self, update_remote=True):
- # remove from cache so messages don't reappear when reconnecting
- if self.active:
- self.active = False
- self.current_short_name = ""
- self.detach_buffer()
- if update_remote:
- async_slack_api_request(self.server.domain, self.server.token, SLACK_API_TRANSLATOR[self.type]["leave"], {"channel": self.identifier})
+ word_start = 0
+ word_end = input_length
+ # If we're on a non-word, look left for something to complete
+ while current_pos >= 0 and line_input[current_pos] != '@' and not line_input[current_pos].isalnum():
+ current_pos = current_pos - 1
+ if current_pos < 0:
+ current_pos = 0
+ for l in range(current_pos, 0, -1):
+ if line_input[l] != '@' and not line_input[l].isalnum():
+ word_start = l + 1
+ break
+ for l in range(current_pos, input_length):
+ if not line_input[l].isalnum():
+ word_end = l
+ break
+ word = line_input[word_start:word_end]
- def closed(self):
- self.channel_buffer = None
- self.last_received = None
- self.close()
+ for m in current_channel.members:
+ u = current_channel.team.users.get(m, None)
+ if u and u.slack_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:])
+ w.buffer_set(current_buffer, "input_pos", str(w.buffer_get_integer(current_buffer, "input_pos") + 1))
+ return w.WEECHAT_RC_OK_EAT
+ return w.WEECHAT_RC_OK
- def is_someone_typing(self):
- for user in self.typing.keys():
- if self.typing[user] + 4 > time.time():
- return True
- if len(self.typing) > 0:
- self.typing = {}
- buffer_list_update_next()
- return False
+def script_unloaded():
+ stop_talking_to_slack()
+ return w.WEECHAT_RC_OK
- def get_typing_list(self):
- typing = []
- for user in self.typing.keys():
- if self.typing[user] + 4 > time.time():
- typing.append(user)
- return typing
+def stop_talking_to_slack():
+ """
+ complete
+ Prevents a race condition where quitting closes buffers
+ which triggers leaving the channel because of how close
+ buffer is handled
+ """
+ EVENTROUTER.shutdown()
+ return w.WEECHAT_RC_OK
- def mark_read(self, update_remote=True):
- if self.channel_buffer:
- w.buffer_set(self.channel_buffer, "unread", "")
- if update_remote:
- self.last_read = time.time()
- self.update_read_marker(self.last_read)
- def update_read_marker(self, time):
- async_slack_api_request(self.server.domain, self.server.token, SLACK_API_TRANSLATOR[self.type]["mark"], {"channel": self.identifier, "ts": time})
+##### 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)
+ self.tries = 0
+ self.start_time = time.time()
+ self.domain = 'api.slack.com'
+ self.request = request
+ self.request_normalized = re.sub(r'\W+', '', request)
+ self.token = token
+ 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.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_counter = 0
+ self.ws_replies = {}
+ self.eventrouter = eventrouter
+ self.token = token
+ self.team = self
+ self.subdomain = subdomain
+ self.domain = subdomain + ".slack.com"
+ self.preferred_name = self.domain
+ self.nick = nick
+ self.myidentifier = myidentifier
+ try:
+ if self.channels:
+ for c in channels.keys():
+ if not self.channels.get(c):
+ self.channels[c] = channels[c]
+ except:
+ self.channels = channels
+ 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
+ self.create_buffer()
+ self.set_muted_channels(kwargs.get('muted_channels', ""))
+ for c in self.channels.keys():
+ channels[c].set_related_server(self)
+ channels[c].check_should_open()
+ # self.channel_set_related_server(c)
+ # Last step is to make sure my nickname is the set color
+ self.users[self.myidentifier].force_color(w.config_string(w.config_get('weechat.color.chat_nick_self')))
+ # This highlight step must happen after we have set related server
+ self.set_highlight_words(kwargs.get('highlight_words', ""))
+ 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 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):
+ # 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:
+ self.preferred_name = self.subdomain
+ elif config.server_aliases not in ['', None]:
+ name = config.server_aliases.get(self.subdomain, None)
+ if name:
+ self.preferred_name = name
+ else:
+ self.preferred_name = self.domain
+ 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')
+ 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)
+ 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()}
+ 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):
- if self.is_someone_typing():
- new_name = ">{}".format(self.name[1:])
+ pass
+ #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:
- new_name = self.name
+ return False
+ def mark_read(self):
+ 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())
+ ws.sock.setblocking(0)
+ self.ws = ws
+ #self.attach_websocket(ws)
+ self.set_connected()
+ self.connecting = False
+ except Exception as e:
+ dbg("websocket connection error: {}".format(e))
+ self.connecting = False
+ return False
+ else:
+ #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)
+ self.eventrouter.receive(s)
+ self.connecting = False
+ #del self.eventrouter.teams[self.get_team_hash()]
+ self.set_reconnect_url(None)
+ def set_connected(self):
+ self.connected = True
+ def set_disconnected(self):
+ 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)
+ dbg("Sent {}...".format(message[:100]))
+ except:
+ print "WS ERROR"
+ dbg("Unexpected error: {}\nSent: {}".format(sys.exc_info()[0], data))
+ self.set_connected()
+
+class SlackChannel(object):
+ """
+ Represents an individual slack channel.
+ """
+ def __init__(self, eventrouter, **kwargs):
+ # We require these two things for a vaid 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.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.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
+ self.current_short_name = self.name
+ self.update_nicklist()
+ 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")
if self.current_short_name != new_name:
self.current_short_name = new_name
w.buffer_set(self.channel_buffer, "short_name", new_name)
-
- def buffer_prnt(self, user='unknown_user', message='no message', time=0):
- """
- writes output (message) to a buffer (channel)
- """
- set_read_marker = False
- time_float = float(time)
- tags = "nick_" + user
- user_obj = self.server.users.find(user)
- # XXX: we should not set log1 for robots.
- if time_float != 0 and self.last_read >= time_float:
- tags += ",no_highlight,notify_none,logger_backlog_end"
- set_read_marker = True
- elif message.find(self.server.nick.encode('utf-8')) > -1:
- tags += ",notify_highlight,log1"
- elif user != self.server.nick and self.name in self.server.users:
- tags += ",notify_private,notify_message,log1,irc_privmsg"
- elif self.muted:
- tags += ",no_highlight,notify_none,logger_backlog_end"
- elif user in [x.strip() for x in w.prefix("join"), w.prefix("quit")]:
- tags += ",irc_smart_filter"
+ return True
+ return False
+ def formatted_name(self, style="default", typing=False, **kwargs):
+ if config.channel_name_typing_indicator:
+ if not typing:
+ prepend = "#"
+ else:
+ prepend = ">"
else:
- tags += ",notify_message,log1,irc_privmsg"
- # don't write these to local log files
- # tags += ",no_log"
- time_int = int(time_float)
+ prepend = "#"
+ select = {
+ "default": prepend + self.slack_name,
+ "sidebar": prepend + self.slack_name,
+ "base": self.slack_name,
+ "long_default": "{}.{}{}".format(self.team.preferred_name, prepend, self.slack_name),
+ "long_base": "{}.{}".format(self.team.preferred_name, self.slack_name),
+ }
+ return select[style]
+ def render_topic(self, topic=None):
if self.channel_buffer:
- prefix_same_nick = w.config_string(w.config_get('weechat.look.prefix_same_nick'))
- if user == self.last_active_user and prefix_same_nick != "":
- if config.colorize_nicks and user_obj:
- name = user_obj.color + prefix_same_nick
+ if not topic:
+ if self.slack_topic['value'] != "":
+ encoded_topic = self.slack_topic['value'].encode('utf-8')
else:
- name = prefix_same_nick
+ encoded_topic = self.slack_purpose['value'].encode('utf-8')
else:
- 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)
-
- if user_obj:
- name = user_obj.formatted_name()
- self.last_active_user = user
- # XXX: handle bots properly here.
- else:
- name = user
- self.last_active_user = None
- name = nick_prefix_color + nick_prefix + w.color("reset") + name + nick_suffix_color + nick_suffix + w.color("reset")
- name = name.decode('utf-8')
- # colorize nicks in each line
- chat_color = w.config_string(w.config_get('weechat.color.chat'))
- if type(message) is not unicode:
- message = message.decode('UTF-8', 'replace')
- curr_color = w.color(chat_color)
- if config.colorize_nicks and config.colorize_messages and user_obj:
- curr_color = user_obj.color
- message = curr_color + message
- for user in self.server.users:
- if user.name in message:
- message = user.name_regex.sub(
- r'\1\2{}\3'.format(user.formatted_name() + curr_color),
- message)
-
- message = HTMLParser.HTMLParser().unescape(message)
- data = u"{}\t{}".format(name, message).encode('utf-8')
- w.prnt_date_tags(self.channel_buffer, time_int, tags, data)
-
- if set_read_marker:
- self.mark_read(False)
+ encoded_topic = topic.encode('utf-8')
+ self.encoded_topic = topic.encode('utf-8')
+ w.buffer_set(self.channel_buffer, "title", encoded_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)
+ 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()
+ def check_should_open(self, force=False):
+ try:
+ if self.is_archived:
+ return
+ except:
+ pass
+ if force:
+ self.create_buffer()
else:
- self.open(False)
- self.last_received = time
- self.unset_typing(user)
-
- def buffer_redraw(self):
- if self.channel_buffer and not self.scrolling:
- w.buffer_clear(self.channel_buffer)
- self.messages.sort()
- for message in self.messages:
- process_message(message.message_json, False)
-
- def set_scrolling(self):
- self.scrolling = True
-
- def unset_scrolling(self):
- self.scrolling = False
-
- def has_message(self, ts):
- return self.messages.count(ts) > 0
-
- def change_message(self, ts, text=None, suffix=''):
- if self.has_message(ts):
- message_index = self.messages.index(ts)
-
- if text is not None:
- self.messages[message_index].change_text(text)
- text = render_message(self.messages[message_index].message_json, True)
-
- # if there is only one message with this timestamp, modify it directly.
- # we do this because time resolution in weechat is less than slack
- int_time = int(float(ts))
- if self.messages.count(str(int_time)) == 1:
- modify_buffer_line(self.channel_buffer, text + suffix, int_time)
- # otherwise redraw the whole buffer, which is expensive
+ 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
+ def set_related_server(self, team):
+ self.team = team
+ def set_highlights(self):
+ #highlight my own name and any set highlights
+ if self.channel_buffer:
+ highlights = self.team.highlight_words.union({'@' + self.team.nick, "!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)
+ Creates the weechat buffer where the channel magic happens.
+ """
+ if not self.channel_buffer:
+ self.active = True
+ 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)
+ if self.type == "im":
+ w.buffer_set(self.channel_buffer, "localvar_set_type", 'private')
else:
- self.buffer_redraw()
- return True
-
- def add_reaction(self, ts, reaction, user):
- if self.has_message(ts):
- message_index = self.messages.index(ts)
- self.messages[message_index].add_reaction(reaction, user)
- self.change_message(ts)
- return True
-
- def remove_reaction(self, ts, reaction, user):
- if self.has_message(ts):
- message_index = self.messages.index(ts)
- self.messages[message_index].remove_reaction(reaction, user)
- self.change_message(ts)
- return True
-
- def send_add_reaction(self, msg_number, reaction):
- self.send_change_reaction("reactions.add", msg_number, reaction)
+ 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, "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:
+ w.buffer_set(self.channel_buffer, "localvar_set_server", self.team.preferred_name)
+ #else:
+ # self.eventrouter.weechat_controller.register_buffer(self.channel_buffer, self)
+ try:
+ for c in range(self.unread_count_display):
+ if self.type == "im":
+ w.buffer_set(self.channel_buffer, "hotlist", "2")
+ else:
+ w.buffer_set(self.channel_buffer, "hotlist", "1")
+ else:
+ pass
+ #dbg("no unread in {}".format(self.name))
+ except:
+ pass
- def send_remove_reaction(self, msg_number, reaction):
- self.send_change_reaction("reactions.remove", msg_number, reaction)
+ 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")
+ def destroy_buffer(self, update_remote):
+ if self.channel_buffer is not None:
+ self.channel_buffer = None
+ self.messages = {}
+ self.hashed_messages = {}
+ self.got_history = False
+ #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)
+ ts = SlackTS(timestamp)
+ last_read = SlackTS(self.last_read)
+ #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 = 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
+ elif ts <= last_read:
+ tags = tag("backlog", user=tag_nick)
+ elif self.type in ["im", "mpdm"]:
+ if nick != self.team.nick:
+ tags = tag("dm", user=tag_nick)
+ self.new_messages = True
+ else:
+ tags = tag("dmfromme")
+ else:
+ tags = tag("default", user=tag_nick)
+ self.new_messages = True
- def send_change_reaction(self, method, msg_number, reaction):
- if 0 < msg_number < len(self.messages):
- timestamp = self.messages[-msg_number].message_json["ts"]
- data = {"channel": self.identifier, "timestamp": timestamp, "name": reaction}
- async_slack_api_request(self.server.domain, self.server.token, method, data)
+ try:
+ if config.unhide_buffers_with_activity and not self.is_visible() and (self.identifier not in self.team.muted_channels):
+ w.buffer_set(self.channel_buffer, "hidden", "0")
- def change_previous_message(self, old, new, flags):
+ w.prnt_date_tags(self.channel_buffer, ts.major, tags, data)
+ modify_print_time(self.channel_buffer, ts.minorstr(), ts.major)
+ if backlog:
+ 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]
+ 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]
+ def change_message(self, ts, text=None, suffix=None):
+ ts = SlackTS(ts)
+ if ts in self.messages:
+ m = self.messages[ts]
+ if text:
+ m.change_text(text)
+ if suffix:
+ m.change_suffix(suffix)
+ 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()
if new == "" and old == "":
- async_slack_api_request(self.server.domain, self.server.token, 'chat.delete', {"channel": self.identifier, "ts": message['ts']})
+ 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)
else:
num_replace = 1
if 'g' in flags:
num_replace = 0
new_message = re.sub(old, new, message["text"], num_replace)
if new_message != message["text"]:
- async_slack_api_request(self.server.domain, self.server.token, 'chat.update', {"channel": self.identifier, "ts": message['ts'], "text": new_message.encode("utf-8")})
-
+ 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)
+ self.eventrouter.receive(s)
def my_last_message(self):
- for message in reversed(self.messages):
- if "user" in message.message_json and "text" in message.message_json and message.message_json["user"] == self.server.users.find(self.server.nick).identifier:
- return message.message_json
-
- def cache_message(self, message_json, from_me=False):
- if from_me:
- message_json["user"] = self.server.users.find(self.server.nick).identifier
- self.messages.append(Message(message_json))
- if len(self.messages) > SCROLLBACK_SIZE:
- self.messages = self.messages[-SCROLLBACK_SIZE:]
-
- def get_history(self):
- if self.active:
- for message in message_cache[self.identifier]:
- process_message(json.loads(message), True)
- async_slack_api_request(self.server.domain, self.server.token, SLACK_API_TRANSLATOR[self.type]["history"], {"channel": self.identifier, "count": BACKLOG_SIZE})
- self.got_history = True
-
-
-class GroupChannel(Channel):
-
- def __init__(self, server, **kwargs):
- super(GroupChannel, self).__init__(server, **kwargs)
- self.type = "group"
-
-
-class MpdmChannel(Channel):
-
- def __init__(self, server, **kwargs):
- n = kwargs.get('name')
- name = "|".join("-".join(n.split("-")[1:-1]).split("--"))
- kwargs["name"] = name
- super(MpdmChannel, self).__init__(server, **kwargs)
- self.type = "group"
-
-
-class DmChannel(Channel):
-
- def __init__(self, server, **kwargs):
- super(DmChannel, self).__init__(server, **kwargs)
- self.type = "im"
+ for message in reversed(self.sorted_message_keys()):
+ m = self.messages[message]
+ if "user" in m.message_json and "text" in m.message_json and m.message_json["user"] == self.team.myidentifier:
+ return m.message_json
+ 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
+ if self.team.connected:
+ w.buffer_clear(self.channel_buffer)
+ self.buffer_prnt('', 'getting channel history...', tagset='backlog')
+ s = SlackRequest(self.team.token, SLACK_API_TRANSLATOR[self.type]["history"], {"channel": self.identifier, "count": BACKLOG_SIZE}, team_hash=self.team.team_hash, channel_identifier=self.identifier, clear=True)
+ if not slow_queue:
+ self.eventrouter.receive(s)
+ 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]
+ 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)
+ # 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
+ returns if any of them is actively typing. If none are,
+ nulls the dict and returns false.
+ """
+ for user, timestamp in self.typing.iteritems():
+ if timestamp + 4 > time.time():
+ return True
+ if len(self.typing) > 0:
+ 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.
+ """
+ typing = []
+ for user, timestamp in self.typing.iteritems():
+ if timestamp + 4 > time.time():
+ typing.append(user)
+ else:
+ del self.typing[user]
+ return typing
+ def mark_read(self, ts=None, update_remote=True, force=False):
+ if not ts:
+ ts = SlackTS()
+ if self.new_messages or force:
+ if self.channel_buffer:
+ w.buffer_set(self.channel_buffer, "unread", "")
+ w.buffer_set(self.channel_buffer, "hotlist", "-1")
+ if update_remote:
+ 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
+ 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"]:
+ 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)
+
+ if user and len(self.members) < 1000:
+ user = self.team.users[user]
+ nick = w.nicklist_search_nick(self.channel_buffer, "", user.slack_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..
+ 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)
- def rename(self):
- if self.server.users.find(self.name).presence == "active":
- new_name = self.server.users.find(self.name).formatted_name('+', config.colorize_private_chats)
+ # if we didn't get a user, build a complete list. this is expensive.
else:
- new_name = self.server.users.find(self.name).formatted_name(' ', config.colorize_private_chats)
-
+ if len(self.members) < 1000:
+ try:
+ for user in self.members:
+ 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)
+ except Exception as e:
+ dbg("DEBUG: {} {} {}".format(self.identifier, self.name, e))
+ else:
+ 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)
+
+ def calc_hash(msg):
+ return sha.sha(str(msg.ts)).hexdigest()
+
+ if ts in self.messages and not self.messages[ts].hash:
+ message = self.messages[ts]
+ tshash = calc_hash(message)
+ l = 3
+ shorthash = tshash[:l]
+ while any(x.startswith(shorthash) for x in self.hashed_messages):
+ l += 1
+ shorthash = tshash[:l]
+
+ if shorthash[:-1] in self.hashed_messages:
+ col_msg = self.hashed_messages.pop(shorthash[:-1])
+ col_new_hash = calc_hash(col_msg)[:l]
+ col_msg.hash = col_new_hash
+ self.hashed_messages[col_new_hash] = col_msg
+ self.change_message(str(col_msg.ts))
+ if col_msg.thread_channel:
+ col_msg.thread_channel.rename()
+
+ self.hashed_messages[shorthash] = message
+ message.hash = shorthash
+
+
+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
+ super(SlackDMChannel, self).__init__(eventrouter, **kwargs)
+ self.type = 'im'
+ self.update_color()
+ self.set_name(self.slack_name)
+ def set_name(self, slack_name):
+ self.name = slack_name
+ 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 = 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
+ else:
+ print_color = ""
+ if not present:
+ prepend = " "
+ else:
+ prepend = "+"
+ select = {
+ "default": self.slack_name,
+ "sidebar": prepend + self.slack_name,
+ "base": self.slack_name,
+ "long_default": "{}.{}".format(self.team.preferred_name, self.slack_name),
+ "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.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)
+ 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)
if self.current_short_name != new_name:
self.current_short_name = new_name
w.buffer_set(self.channel_buffer, "short_name", new_name)
-
- def update_nicklist(self, user=None):
- pass
-
-
-class User(object):
-
- def __init__(self, server, name, identifier, presence="away", deleted=False, is_bot=False):
- self.server = server
- self.name = name
- self.identifier = identifier
- self.deleted = deleted
- self.presence = presence
-
- self.channel_buffer = w.info_get("irc_buffer", "{}.{}".format(domain, self.name))
- self.update_color()
- self.name_regex = re.compile(r"([\W]|\A)(@{0,1})" + self.name + "('s|[^'\w]|\Z)")
- self.is_bot = is_bot
-
- if deleted:
- return
- self.nicklist_pointer = w.nicklist_add_nick(server.buffer, "", self.name, self.color_name, "", "", 1)
- if self.presence == 'away':
- w.nicklist_nick_set(self.server.buffer, self.nicklist_pointer, "visible", "0")
- else:
- w.nicklist_nick_set(self.server.buffer, self.nicklist_pointer, "visible", "1")
-# w.nicklist_add_nick(server.buffer, "", self.formatted_name(), "", "", "", 1)
-
- def __str__(self):
- return self.name
-
- def __repr__(self):
- return self.name
-
- def __eq__(self, compare_str):
- try:
- if compare_str == self.name or compare_str == self.identifier:
- return True
- elif compare_str[0] == '@' and compare_str[1:] == self.name:
return True
- else:
- return False
- except:
- return False
+ return False
+ def refresh(self):
+ return self.rename()
- def get_aliases(self):
- return [self.name, "@" + self.name, self.identifier]
-
- def set_active(self):
- if not self.deleted:
- self.presence = "active"
- dm_channel = self.server.channels.find(self.name)
- if dm_channel and dm_channel.active:
- buffer_list_update_next()
-
- return #temporarily noop this
- for channel in self.server.channels:
- if channel.has_user(self.identifier):
- channel.update_nicklist(self.identifier)
- w.nicklist_nick_set(self.server.buffer, self.nicklist_pointer, "visible", "1")
-
- def set_inactive(self):
- if not self.deleted:
- self.presence = "away"
- dm_channel = self.server.channels.find(self.name)
- if dm_channel and dm_channel.active:
- buffer_list_update_next()
-
- return #temporarily noop this
- if self.deleted:
- return
- for channel in self.server.channels:
- if channel.has_user(self.identifier):
- channel.update_nicklist(self.identifier)
- w.nicklist_nick_set(self.server.buffer, self.nicklist_pointer, "visible", "0")
+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):
+ # return prepend + self.slack_name
- def update_color(self):
- if config.colorize_nicks:
- if self.name == self.server.nick:
- self.color_name = w.config_string(w.config_get('weechat.color.chat_nick_self'))
+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.create_buffer()
+ self.active = True
+ self.get_history()
+ if "info" in SLACK_API_TRANSLATOR[self.type]:
+ s = SlackRequest(self.team.token, SLACK_API_TRANSLATOR[self.type]["info"], {"name": self.identifier}, team_hash=self.team.team_hash, channel_identifier=self.identifier)
+ self.eventrouter.receive(s)
+ #self.create_buffer()
+ def set_name(self, n):
+ self.name = "|".join("-".join(n.split("-")[1:-1]).split("--"))
+ 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:
- self.color_name = w.info_get('irc_nick_color_name', self.name)
- self.color = w.color(self.color_name)
+ prepend = ">"
else:
- self.color = ""
- self.color_name = ""
+ prepend = "#"
+ select = {
+ "default": adjusted_name,
+ "sidebar": prepend + adjusted_name,
+ "base": adjusted_name,
+ "long_default": "{}.{}".format(self.team.preferred_name, adjusted_name),
+ "long_base": "{}.{}".format(self.team.preferred_name, adjusted_name),
+ }
+ return select[style]
+
+# def formatted_name(self, **kwargs):
+# return self.name
+ def rename(self):
+ pass
- def formatted_name(self, prepend="", enable_color=True):
- if config.colorize_nicks and enable_color:
- print_color = self.color
- else:
- print_color = ""
- return print_color + prepend + self.name
+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.type = "thread"
+ self.got_history = False
+ self.label = None
+ #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 = {
+ "default": " +{}".format(hash_or_ts),
+ "long_default": "{}.{}".format(self.parent_message.channel.formatted_name(style="long_default"), hash_or_ts),
+ "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 create_dm_channel(self):
- async_slack_api_request(self.server.domain, self.server.token, "im.open", {"user": self.identifier})
+ def buffer_prnt(self, nick, text, timestamp, **kwargs):
+ data = "{}\t{}".format(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):
+ # tags = tag("backlog")
+ # backlog = True
+ #elif self.type in ["im", "mpdm"]:
+ # tags = tag("dm")
+ # self.new_messages = True
+ #else:
+ tags = tag("default")
+ #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:
+ # 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)
+ text = message.render()
+ #print text
-class Bot(object):
+ suffix = ''
+ if 'edited' in message.message_json:
+ suffix = ' (edited)'
+ #try:
+ # channel.unread_count += 1
+ #except:
+ # channel.unread_count = 1
+ self.buffer_prnt(message.sender, text + suffix, message.ts)
- def __init__(self, server, name, identifier, deleted=False):
- self.server = server
- self.name = name
- self.identifier = identifier
- self.deleted = deleted
- self.update_color()
+ def send_message(self, message):
+ #team = self.eventrouter.teams[self.team]
+ message = linkify_text(message, self.parent_message.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)
+ self.mark_read(update_remote=False, force=True)
- def __eq__(self, compare_str):
- if compare_str == self.identifier or compare_str == self.name:
- return True
- else:
- return False
+ 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)
+ # 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"], {"name": self.name}, team_hash=self.team.team_hash, channel_identifier=self.identifier)
+ # self.eventrouter.receive(s)
+ self.create_buffer()
- def __str__(self):
- return "{}".format(self.identifier)
+ def rename(self):
+ if self.channel_buffer and not self.label:
+ w.buffer_set(self.channel_buffer, "short_name", self.formatted_name(style="sidebar", enable_color=True))
- def __repr__(self):
- return "{}".format(self.identifier)
+ def create_buffer(self):
+ """
+ incomplete (muted doesn't work)
+ Creates the weechat buffer where the thread magic happens.
+ """
+ if not self.channel_buffer:
+ 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_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'))
+
+ #self.eventrouter.weechat_controller.set_refresh_buffer_list(True)
+
+ #try:
+ # if self.unread_count != 0:
+ # for c in range(1, self.unread_count):
+ # 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
+ #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:
+ 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,
+ # the rest we can just learn from slack
+ self.identifier = kwargs["id"]
+ self.slack_name = kwargs["name"]
+ self.name = kwargs["name"]
+ for key, value in kwargs.items():
+ setattr(self, key, value)
+ 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):
if config.colorize_nicks:
self.color_name = w.info_get('irc_nick_color_name', self.name.encode('utf-8'))
self.color = w.color(self.color_name)
else:
- self.color_name = ""
self.color = ""
-
+ self.color_name = ""
def formatted_name(self, prepend="", enable_color=True):
if config.colorize_nicks and enable_color:
print_color = self.color
@@ -1010,857 +1644,561 @@ class Bot(object):
print_color = ""
return print_color + prepend + self.name
+class SlackBot(SlackUser):
+ """
+ Basically the same as a user, but split out to identify and for future
+ needs
+ """
+ def __init__(self, **kwargs):
+ super(SlackBot, self).__init__(**kwargs)
-class Message(object):
-
- def __init__(self, message_json):
+class SlackMessage(object):
+ """
+ Represents a single slack message and associated context/metadata.
+ These are modifiable and can be rerendered to change a message,
+ delete a message, add a reaction, add a thread.
+ Note: these can't be tied to a SlackUser object because users
+ can be deleted, so we have to store sender in each one.
+ """
+ def __init__(self, message_json, team, channel, override_sender=None):
+ self.team = team
+ self.channel = channel
self.message_json = message_json
- self.ts = message_json['ts']
- # split timestamp into time and counter
- self.ts_time, self.ts_counter = message_json['ts'].split('.')
-
+ self.submessages = []
+ self.thread_channel = None
+ self.hash = None
+ if override_sender:
+ self.sender = override_sender
+ self.sender_plain = override_sender
+ else:
+ senders = self.get_sender()
+ self.sender, self.sender_plain = senders[0], senders[1]
+ self.suffix = ''
+ self.ts = SlackTS(message_json['ts'])
+ 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):
- if not isinstance(new_text, unicode):
- new_text = unicode(new_text, 'utf-8')
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:
+ 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
+ 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))
+ elif 'username' in self.message_json:
+ name = u"-{}-".format(self.message_json["username"])
+ name_plain = u"{}".format(self.message_json["username"])
+ 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'))
+ else:
+ return (name, name_plain)
def add_reaction(self, reaction, user):
- if "reactions" in self.message_json:
+ m = self.message_json.get('reactions', None)
+ if m:
found = False
- for r in self.message_json["reactions"]:
+ for r in m:
if r["name"] == reaction and user not in r["users"]:
r["users"].append(user)
found = True
-
if not found:
self.message_json["reactions"].append({u"name": reaction, u"users": [user]})
else:
self.message_json["reactions"] = [{u"name": reaction, u"users": [user]}]
-
def remove_reaction(self, reaction, user):
- if "reactions" in self.message_json:
- for r in self.message_json["reactions"]:
+ m = self.message_json.get('reactions', None)
+ if m:
+ for r in m:
if r["name"] == reaction and user in r["users"]:
r["users"].remove(user)
else:
pass
- def __eq__(self, other):
- return self.ts_time == other or self.ts == other
-
- def __repr__(self):
- return "{} {} {} {}\n".format(self.ts_time, self.ts_counter, self.ts, self.message_json)
-
- def __lt__(self, other):
- return self.ts < other.ts
-
-
-def slack_buffer_or_ignore(f):
- """
- Only run this function if we're in a slack buffer, else ignore
- """
- @wraps(f)
- def wrapper(current_buffer, *args, **kwargs):
- server = servers.find(current_domain_name())
- if not server:
- return w.WEECHAT_RC_OK
- return f(current_buffer, *args, **kwargs)
- return wrapper
-
-
-def slack_command_cb(data, current_buffer, args):
- a = args.split(' ', 1)
- if len(a) > 1:
- function_name, args = a[0], " ".join(a[1:])
- else:
- function_name, args = a[0], None
-
- try:
- cmds[function_name](current_buffer, args)
- except KeyError:
- w.prnt("", "Command not found: " + function_name)
- return w.WEECHAT_RC_OK
-
-
-@slack_buffer_or_ignore
-def me_command_cb(data, current_buffer, args):
- if channels.find(current_buffer):
- # channel = channels.find(current_buffer)
- # nick = channel.server.nick
- message = "_{}_".format(args)
- buffer_input_cb("", current_buffer, message)
- return w.WEECHAT_RC_OK
-
-
-@slack_buffer_or_ignore
-def join_command_cb(data, current_buffer, args):
- args = args.split()
- if len(args) < 2:
- w.prnt(current_buffer, "Missing channel argument")
- return w.WEECHAT_RC_OK_EAT
- elif command_talk(current_buffer, args[1]):
- return w.WEECHAT_RC_OK_EAT
- else:
- return w.WEECHAT_RC_OK
-
-
-@slack_buffer_or_ignore
-def part_command_cb(data, current_buffer, args):
- if channels.find(current_buffer) or servers.find(current_buffer):
- args = args.split()
- if len(args) > 1:
- channel = args[1:]
- servers.find(current_domain_name()).channels.find(channel).close(True)
- else:
- channels.find(current_buffer).close(True)
- return w.WEECHAT_RC_OK_EAT
- else:
- return w.WEECHAT_RC_OK
-
-
-# Wrap command_ functions that require they be performed in a slack buffer
-def slack_buffer_required(f):
- @wraps(f)
- def wrapper(current_buffer, *args, **kwargs):
- server = servers.find(current_domain_name())
- if not server:
- w.prnt(current_buffer, "This command must be used in a slack buffer")
- return w.WEECHAT_RC_ERROR
- return f(current_buffer, *args, **kwargs)
- return wrapper
-
-
-def command_register(current_buffer, args):
- CLIENT_ID = "2468770254.51917335286"
- CLIENT_SECRET = "dcb7fe380a000cba0cca3169a5fe8d70" # this is not really a secret
- if not args:
- message = """
-# ### 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]`
-6) Add the returned token per the normal wee-slack setup instructions
-
-
-"""
- w.prnt(current_buffer, message)
- else:
- aargs = args.split(None, 2)
- if len(aargs) != 1:
- w.prnt(current_buffer, "ERROR: invalid args to register")
- else:
- # w.prnt(current_buffer, "https://slack.com/api/oauth.access?client_id={}&client_secret={}&code={}".format(CLIENT_ID, CLIENT_SECRET, aargs[0]))
- ret = urllib.urlopen("https://slack.com/api/oauth.access?client_id={}&client_secret={}&code={}".format(CLIENT_ID, CLIENT_SECRET, aargs[0])).read()
- d = json.loads(ret)
- if d["ok"] == True:
- w.prnt(current_buffer, "Success! Access token is: " + d['access_token'])
- else:
- w.prnt(current_buffer, "Failed! Error is: " + d['error'])
-
-
-@slack_buffer_or_ignore
-def msg_command_cb(data, current_buffer, args):
- dbg("msg_command_cb")
- aargs = args.split(None, 2)
- who = aargs[1]
-
- command_talk(current_buffer, who)
-
- if len(aargs) > 2:
- message = aargs[2]
- server = servers.find(current_domain_name())
- if server:
- channel = server.channels.find(who)
- channel.send_message(message)
- return w.WEECHAT_RC_OK_EAT
+class SlackThreadMessage(SlackMessage):
+ def __init__(self, parent_id, *args):
+ super(SlackThreadMessage, self).__init__(*args)
+ #super(SlackBot, self).__init__(**kwargs)
+ self.parent_id = parent_id
-@slack_buffer_required
-def command_upload(current_buffer, args):
+class WeeSlackMetadata(object):
"""
- Uploads a file to the current buffer
- /slack upload [file_path]
+ A simple container that we pickle/unpickle to hold data.
"""
- post_data = {}
- channel = current_buffer_name(short=True)
- domain = current_domain_name()
- token = servers.find(domain).token
-
- if servers.find(domain).channels.find(channel):
- channel_identifier = servers.find(domain).channels.find(channel).identifier
-
- if channel_identifier:
- post_data["token"] = token
- post_data["channels"] = channel_identifier
- post_data["file"] = args
- async_slack_api_upload_request(token, "files.upload", post_data)
-
-
-def command_talk(current_buffer, args):
- """
- Open a chat with the specified user
- /slack talk [user]
- """
-
- server = servers.find(current_domain_name())
- if server:
- channel = server.channels.find(args)
- if channel is None:
- user = server.users.find(args)
- if user:
- user.create_dm_channel()
- else:
- server.buffer_prnt("User or channel {} not found.".format(args))
+ 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:
- channel.open()
- if config.switch_buffer_on_join:
- w.buffer_set(channel.channel_buffer, "display", "1")
- return True
- else:
- return False
-
-
-def command_join(current_buffer, args):
- """
- Join the specified channel
- /slack join [channel]
- """
- domain = current_domain_name()
- if domain == "":
- if len(servers) == 1:
- domain = servers[0]
+ self.major = int(time.time())
+ self.minor = 0
+ def __cmp__(self, other):
+ if isinstance(other, SlackTS):
+ if self.major < other.major:
+ return -1
+ elif self.major > other.major:
+ return 1
+ elif self.major == other.major:
+ if self.minor < other.minor:
+ return -1
+ elif self.minor > other.minor:
+ return 1
+ else:
+ return 0
else:
- w.prnt(current_buffer, "You are connected to multiple Slack instances, please execute /join from a server buffer. i.e. (domain).slack.com")
- return
- channel = servers.find(domain).channels.find(args)
- if channel is not None:
- servers.find(domain).channels.find(args).open()
- else:
- w.prnt(current_buffer, "Channel not found.")
-
-
-@slack_buffer_required
-def command_channels(current_buffer, args):
- """
- List all the channels for the slack instance (name, id, active)
- /slack channels
- """
- server = servers.find(current_domain_name())
- for channel in server.channels:
- line = "{:<25} {} {}".format(channel.name, channel.identifier, channel.active)
- server.buffer_prnt(line)
-
-
-def command_nodistractions(current_buffer, args):
- global hide_distractions
- hide_distractions = not hide_distractions
- if config.distracting_channels != ['']:
- for channel in config.distracting_channels:
- try:
- channel_buffer = channels.find(channel).channel_buffer
- if channel_buffer:
- w.buffer_set(channels.find(channel).channel_buffer, "hidden", str(int(hide_distractions)))
- except:
- dbg("Can't hide channel {} .. removing..".format(channel), main_buffer=True)
- config.distracting_channels.pop(config.distracting_channels.index(channel))
- save_distracting_channels()
-
-
-def command_distracting(current_buffer, args):
- if channels.find(current_buffer) is None:
- w.prnt(current_buffer, "This command must be used in a channel buffer")
- return
- fullname = channels.find(current_buffer).fullname()
- if config.distracting_channels.count(fullname) == 0:
- config.distracting_channels.append(fullname)
- else:
- 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))
+ s = self.__str__()
+ if s < other:
+ return -1
+ elif s > other:
+ 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
-@slack_buffer_required
-def command_users(current_buffer, args):
+def handle_rtmstart(login_data, eventrouter):
"""
- List all the users for the slack instance (name, id, away)
- /slack users
+ This handles the main entry call to slack, rtm.start
"""
- server = servers.find(current_domain_name())
- for user in server.users:
- line = "{:<40} {} {}".format(user.formatted_name(), user.identifier, user.presence)
- server.buffer_prnt(line)
+ if login_data["ok"]:
+ metadata = pickle.loads(login_data["wee_slack_request_metadata"])
-def command_setallreadmarkers(current_buffer, args):
- """
- Sets the read marker for all channels
- /slack setallreadmarkers
- """
- for channel in channels:
- channel.mark_read()
+ #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))
-def command_changetoken(current_buffer, args):
- w.config_set_plugin('slack_api_token', args)
+ 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)
-def command_test(current_buffer, args):
- w.prnt(current_buffer, "worked!")
+ 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)
-def away_command_cb(data, current_buffer, args):
- (all, message) = re.match("^/away(?:\s+(-all))?(?:\s+(.+))?", args).groups()
- if all is None:
- server = servers.find(current_domain_name())
- if not server:
- return w.WEECHAT_RC_OK
- if message is None:
- server.set_active()
else:
- server.set_away(message)
- return w.WEECHAT_RC_OK_EAT
- for server in servers:
- if message is None:
- server.set_active()
- else:
- server.set_away(message)
- return w.WEECHAT_RC_OK
-
-
-@slack_buffer_required
-def command_away(current_buffer, args):
- """
- Sets your status as 'away'
- /slack away
- """
- server = servers.find(current_domain_name())
- async_slack_api_request(server.domain, server.token, 'presence.set', {"presence": "away"})
-
-
-@slack_buffer_required
-def command_back(current_buffer, args):
- """
- Sets your status as 'back'
- /slack back
- """
- server = servers.find(current_domain_name())
- async_slack_api_request(server.domain, server.token, 'presence.set', {"presence": "active"})
-
-
-@slack_buffer_required
-def command_markread(current_buffer, args):
- """
- Marks current channel as read
- /slack markread
- """
- # refactor this - one liner i think
- channel = current_buffer_name(short=True)
- domain = current_domain_name()
- if servers.find(domain).channels.find(channel):
- servers.find(domain).channels.find(channel).mark_read()
-
-
-@slack_buffer_required
-def command_slash(current_buffer, args):
- """
- Support for custom slack commands
- /slack slash /customcommand arg1 arg2 arg3
- """
-
- server = servers.find(current_domain_name())
- channel = current_buffer_name(short=True)
- domain = current_domain_name()
-
- if args is None:
- server.buffer_prnt("Usage: /slack slash /someslashcommand [arguments...].")
- return
-
- split_args = args.split(None, 1)
-
- command = split_args[0]
- text = split_args[1] if len(split_args) > 1 else ""
-
- if servers.find(domain).channels.find(channel):
- channel_identifier = servers.find(domain).channels.find(channel).identifier
-
- if channel_identifier:
- async_slack_api_request(server.domain, server.token, 'chat.command', {'command': command, 'text': text, 'channel': channel_identifier})
- else:
- server.buffer_prnt("User or channel not found.")
+ 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_history(message_json, eventrouter, **kwargs):
+ request_metadata = pickle.loads(message_json["wee_slack_request_metadata"])
+ kwargs['team'] = eventrouter.teams[request_metadata.team_hash]
+ kwargs['channel'] = kwargs['team'].channels[request_metadata.channel_identifier]
+ try:
+ clear = request_metadata.clear
+ except:
+ clear = False
+ dbg(clear)
+ kwargs['output_type'] = "backlog"
+ if clear:
+ w.buffer_clear(kwargs['channel'].channel_buffer)
+ for message in reversed(message_json["messages"]):
+ process_message(message, eventrouter, **kwargs)
+###### New/converted process_ and subprocess_ methods
-def command_flushcache(current_buffer, args):
- global message_cache
- message_cache = collections.defaultdict(list)
- cache_write_cb("", "")
+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 command_cachenow(current_buffer, args):
- cache_write_cb("", "")
+def process_presence_change(message_json, eventrouter, **kwargs):
+ kwargs["user"].presence = message_json["presence"]
-def command_neveraway(current_buffer, args):
- global never_away
- if never_away:
- never_away = False
- dbg("unset never_away", main_buffer=True)
+def process_pref_change(message_json, eventrouter, **kwargs):
+ team = kwargs["team"]
+ if message_json['name'] == u'muted_channels':
+ team.set_muted_channels(message_json['value'])
+ elif message_json['name'] == u'highlight_words':
+ team.set_highlight_words(message_json['value'])
else:
- never_away = True
- dbg("set never_away", main_buffer=True)
-
-
-def command_printvar(current_buffer, args):
- w.prnt("", "{}".format(eval(args)))
-
-
-def command_p(current_buffer, args):
- w.prnt("", "{}".format(eval(args)))
-
-
-def command_debug(current_buffer, args):
- create_slack_debug_buffer()
-
+ dbg("Preference change not implemented: {}\n".format(message_json['name']))
-def command_debugstring(current_buffer, args):
- global debug_string
- if args == '':
- debug_string = None
- else:
- debug_string = args
+def process_user_typing(message_json, eventrouter, **kwargs):
+ channel = kwargs["channel"]
+ team = kwargs["team"]
+ if channel:
+ 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 command_search(current_buffer, args):
- pass
-# if not slack_buffer:
-# create_slack_buffer()
-# w.buffer_set(slack_buffer, "display", "1")
-# query = args
-# w.prnt(slack_buffer,"\nSearched for: %s\n\n" % (query))
-# reply = slack_api_request('search.messages', {"query":query}).read()
-# data = json.loads(reply)
-# for message in data['messages']['matches']:
-# message["text"] = message["text"].encode('ascii', 'ignore')
-# formatted_message = "%s / %s:\t%s" % (message["channel"]["name"], message['username'], message['text'])
-# w.prnt(slack_buffer,str(formatted_message))
-
-
-def command_nick(current_buffer, args):
+def process_pong(message_json, eventrouter, **kwargs):
pass
-# urllib.urlopen("https://%s/account/settings" % (domain))
-# browser.select_form(nr=0)
-# browser.form['username'] = args
-# reply = browser.submit()
-
-
-def command_help(current_buffer, args):
- help_cmds = {k[8:]: v.__doc__ for k, v in globals().items() if k.startswith("command_")}
-
- if args:
- try:
- help_cmds = {args: help_cmds[args]}
- except KeyError:
- w.prnt("", "Command not found: " + args)
- return
-
- for cmd, helptext in help_cmds.items():
- w.prnt('', w.color("bold") + cmd)
- w.prnt('', (helptext or 'No help text').strip())
- w.prnt('', '')
-# Websocket handling methods
+def process_message(message_json, eventrouter, store=True, **kwargs):
+ channel = kwargs["channel"]
+ team = kwargs["team"]
+ #try:
+ # send these subtype messages elsewhere
+ known_subtypes = [
+ 'thread_message',
+ 'message_replied',
+ 'message_changed',
+ 'message_deleted',
+ 'channel_join',
+ 'channel_leave',
+ 'channel_topic',
+ #'group_join',
+ #'group_leave',
+ ]
+ if "thread_ts" in message_json and "reply_count" not in message_json:
+ message_json["subtype"] = "thread_message"
+ subtype = message_json.get("subtype", None)
+ if subtype and subtype in known_subtypes:
+ f = eval('subprocess_' + subtype)
+ f(message_json, eventrouter, channel, team)
-
-def command_openweb(current_buffer, args):
- trigger = config.trigger_value
- if trigger != "0":
- if args is None:
- channel = channels.find(current_buffer)
- url = "{}/messages/{}".format(channel.server.server_buffer_name, channel.name)
- topic = w.buffer_get_string(channel.channel_buffer, "title")
- w.buffer_set(channel.channel_buffer, "title", "{}:{}".format(trigger, url))
- w.hook_timer(1000, 0, 1, "command_openweb", json.dumps({"topic": topic, "buffer": current_buffer}))
- else:
- # TODO: fix this dirty hack because i don't know the right way to send multiple args.
- args = current_buffer
- data = json.loads(args)
- channel_buffer = channels.find(data["buffer"]).channel_buffer
- w.buffer_set(channel_buffer, "title", data["topic"])
- return w.WEECHAT_RC_OK
-
-
-@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(current_buffer, args.split(None, 1)[1]):
- return w.WEECHAT_RC_OK_EAT
else:
- return w.WEECHAT_RC_ERROR
-
-
-def command_topic(current_buffer, args):
- """
- Change the topic of a channel
- /slack topic [<channel>] [<topic>|-delete]
- """
- server = servers.find(current_domain_name())
- if server:
- arrrrgs = args.split(None, 1)
- if arrrrgs[0].startswith('#'):
- channel = server.channels.find(arrrrgs[0])
- topic = arrrrgs[1]
- else:
- channel = server.channels.find(current_buffer)
- topic = args
+ message = SlackMessage(message_json, team, channel)
+ text = message.render()
+ dbg(text)
+
+ # special case with actions.
+ if text.startswith("_") and text.endswith("_"):
+ text = text[1:-1]
+ if message.sender != channel.team.nick:
+ text = message.sender + " " + text
+ try:
+ channel.unread_count_display += 1
+ except:
+ channel.unread_count_display += 1
+ channel.buffer_prnt(w.prefix("action").rstrip(), text, message.ts, tag_nick=message.sender_plain, **kwargs)
- if channel:
- if topic == "-delete":
- async_slack_api_request(server.domain, server.token, 'channels.setTopic', {"channel": channel.identifier, "topic": ""})
- else:
- async_slack_api_request(server.domain, server.token, 'channels.setTopic', {"channel": channel.identifier, "topic": topic})
- return True
else:
- return False
+ suffix = ''
+ if 'edited' in message_json:
+ suffix = ' (edited)'
+ try:
+ channel.unread_count_display += 1
+ except:
+ channel.unread_count_display = 1
+ channel.buffer_prnt(message.sender, text + suffix, message.ts, tag_nick=message.sender_plain, **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)
+ # traceback.print_exc()
+
+def subprocess_thread_message(message_json, eventrouter, channel, team):
+ #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)
+ parent_message.submessages.append(message)
+ channel.hash_message(parent_ts)
+ channel.store_message(message, team)
+ channel.change_message(parent_ts)
+
+ text = message.render()
+ #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)
+
+# channel = channels.find(message_json["channel"])
+# server = channel.server
+# #threadinfo = channel.get_message(message_json["thread_ts"])
+# message = Message(message_json, server=server, channel=channel)
+# dbg(message, main_buffer=True)
+#
+# orig = channel.get_message(message_json['thread_ts'])
+# if orig[0]:
+# channel.get_message(message_json['thread_ts'])[2].add_thread_message(message)
+# else:
+# dbg("COULDN'T find orig message {}".format(message_json['thread_ts']), main_buffer=True)
+
+ #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)
+
+def subprocess_channel_join(message_json, eventrouter, channel, team):
+ joinprefix = w.prefix("join")
+ message = SlackMessage(message_json, team, channel, override_sender=joinprefix)
+ 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()
+
+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)
+ #if "attachments" in m:
+ # message_json["attachments"] = m["attachments"]
+ #if "text" in m:
+ # if "text" in message_json:
+ # message_json["text"] += m["text"]
+ # dbg("added text!")
+ # else:
+ # message_json["text"] = m["text"]
+ #if "fallback" in m:
+ # if "fallback" in message_json:
+ # message_json["fallback"] += m["fallback"]
+ # else:
+ # message_json["fallback"] = m["fallback"]
+
+ text_before = (len(new_message['text']) > 0)
+ new_message["text"] += unwrap_attachments(message_json, text_before)
+ if "edited" in new_message:
+ channel.change_message(new_message["ts"], new_message["text"], ' (edited)')
else:
- return False
+ 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 slack_websocket_cb(server, fd):
- try:
- data = servers.find(server).ws.recv()
- message_json = json.loads(data)
- # this magic attaches json that helps find the right dest
- message_json['_server'] = server
- except WebSocketConnectionClosedException:
- servers.find(server).ws.close()
- return w.WEECHAT_RC_OK
- except Exception:
- dbg("socket issue: {}\n".format(traceback.format_exc()))
- return w.WEECHAT_RC_OK
- # dispatch here
- if "reply_to" in message_json:
- function_name = "reply"
- elif "type" in message_json:
- function_name = message_json["type"]
- else:
- function_name = "unknown"
+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')
+ channel.buffer_prnt(w.prefix("network").rstrip(), text, message_json["ts"], tagset="muted")
+ channel.render_topic(message_json["topic"])
+
+def process_reply(message_json, eventrouter, **kwargs):
+ dbg('processing reply')
+ team = kwargs["team"]
+ identifier = message_json["reply_to"]
try:
- proc[function_name](message_json)
- except KeyError:
- if function_name:
- dbg("Function not implemented: {}\n{}".format(function_name, message_json))
+ original_message_json = team.ws_replies[identifier]
+ del team.ws_replies[identifier]
+ if "ts" in message_json:
+ original_message_json["ts"] = message_json["ts"]
else:
- dbg("Function not implemented\n{}".format(message_json))
- w.bar_item_update("slack_typing_notice")
- return w.WEECHAT_RC_OK
-
-
-def process_reply(message_json):
- server = servers.find(message_json["_server"])
- identifier = message_json["reply_to"]
- item = server.message_buffer.pop(identifier)
- if 'text' in item and type(item['text']) is not unicode:
- item['text'] = item['text'].decode('UTF-8', 'replace')
- if "type" in item:
- if item["type"] == "message" and "channel" in item.keys():
- item["ts"] = message_json["ts"]
- channels.find(item["channel"]).cache_message(item, from_me=True)
- text = unfurl_refs(item["text"], ignore_alt_text=config.unfurl_ignore_alt_text)
-
- channels.find(item["channel"]).buffer_prnt(item["user"], text, item["ts"])
- dbg("REPLY {}".format(item))
+ dbg("no reply ts {}".format(message_json))
+ 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)
-def process_pong(message_json):
- pass
+ #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)
+ # channels.find(message_json["channel"]).buffer_prnt(server.nick, m.render(), m.ts)
-def process_pref_change(message_json):
- server = servers.find(message_json["_server"])
- if message_json['name'] == u'muted_channels':
- muted = message_json['value'].split(',')
- for c in server.channels:
- if c.identifier in muted:
- c.muted = True
- else:
- c.muted = False
- else:
- dbg("Preference change not implemented: {}\n".format(message_json['name']))
-
-
-def process_team_join(message_json):
- server = servers.find(message_json["_server"])
- item = message_json["user"]
- server.add_user(User(server, item["name"], item["id"], item["presence"]))
- server.buffer_prnt("New user joined: {}".format(item["name"]))
-
-
-def process_manual_presence_change(message_json):
- process_presence_change(message_json)
-
+ process_message(m.message_json, eventrouter, channel=channel, team=team)
+ channel.mark_read(update_remote=True, force=True)
+ dbg("REPLY {}".format(message_json))
+ except KeyError:
+ dbg("Unexpected reply {}".format(message_json))
-def process_presence_change(message_json):
- server = servers.find(message_json["_server"])
- identifier = message_json.get("user", server.nick)
- if message_json["presence"] == 'active':
- server.users.find(identifier).set_active()
+def process_channel_marked(message_json, eventrouter, **kwargs):
+ """
+ complete
+ """
+ channel = kwargs["channel"]
+ ts = message_json.get("ts", None)
+ if ts:
+ channel.mark_read(ts=ts, force=True, update_remote=False)
else:
- server.users.find(identifier).set_inactive()
-
-
-def process_channel_marked(message_json):
- channel = channels.find(message_json["channel"])
- channel.mark_read(False)
- w.buffer_set(channel.channel_buffer, "hotlist", "-1")
-
-
-def process_group_marked(message_json):
- channel = channels.find(message_json["channel"])
- channel.mark_read(False)
- w.buffer_set(channel.channel_buffer, "hotlist", "-1")
-
-
-def process_channel_created(message_json):
- server = servers.find(message_json["_server"])
+ 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"]
- if server.channels.find(message_json["channel"]["name"]):
- server.channels.find(message_json["channel"]["name"]).open(False)
- else:
- item = message_json["channel"]
- item["prepend_name"] = "#"
- server.add_channel(Channel(server, **item))
- server.buffer_prnt("New channel created: {}".format(item["name"]))
-
-
-def process_channel_left(message_json):
- server = servers.find(message_json["_server"])
- server.channels.find(message_json["channel"]).close(False)
-
-
-def process_channel_join(message_json):
- server = servers.find(message_json["_server"])
- channel = server.channels.find(message_json["channel"])
- text = unfurl_refs(message_json["text"], ignore_alt_text=False)
- channel.buffer_prnt(w.prefix("join").rstrip(), text, message_json["ts"])
- channel.user_join(message_json["user"])
-
-
-def process_channel_topic(message_json):
- server = servers.find(message_json["_server"])
- channel = server.channels.find(message_json["channel"])
- text = unfurl_refs(message_json["text"], ignore_alt_text=False)
- channel.buffer_prnt(w.prefix("network").rstrip(), text, message_json["ts"])
- channel.set_topic(message_json["topic"])
-
-
-def process_channel_joined(message_json):
- server = servers.find(message_json["_server"])
- if server.channels.find(message_json["channel"]["name"]):
- server.channels.find(message_json["channel"]["name"]).open(False)
- else:
- item = message_json["channel"]
- item["prepend_name"] = "#"
- server.add_channel(Channel(server, **item))
-
-
-def process_channel_leave(message_json):
- server = servers.find(message_json["_server"])
- channel = server.channels.find(message_json["channel"])
- text = unfurl_refs(message_json["text"], ignore_alt_text=False)
- channel.buffer_prnt(w.prefix("quit").rstrip(), text, message_json["ts"])
- channel.user_leave(message_json["user"])
-
-
-def process_channel_archive(message_json):
- server = servers.find(message_json["_server"])
- channel = server.channels.find(message_json["channel"])
- channel.detach_buffer()
-
+ kwargs['team'].channels[item["id"]].update_from_message_json(item)
+ kwargs['team'].channels[item["id"]].open()
-def process_group_join(message_json):
- process_channel_join(message_json)
-
-
-def process_group_leave(message_json):
- process_channel_leave(message_json)
-
-
-def process_group_topic(message_json):
- process_channel_topic(message_json)
-
-
-def process_group_left(message_json):
- server = servers.find(message_json["_server"])
- server.channels.find(message_json["channel"]).close(False)
-
-
-def process_group_joined(message_json):
- server = servers.find(message_json["_server"])
- if server.channels.find(message_json["channel"]["name"]):
- server.channels.find(message_json["channel"]["name"]).open(False)
- else:
- item = message_json["channel"]
- item["prepend_name"] = "#"
- if item["name"].startswith("mpdm-"):
- server.add_channel(MpdmChannel(server, **item))
- else:
- server.add_channel(GroupChannel(server, **item))
-
-def process_group_archive(message_json):
- channel = server.channels.find(message_json["channel"])
- channel.detach_buffer()
-
-
-def process_mpim_close(message_json):
- server = servers.find(message_json["_server"])
- server.channels.find(message_json["channel"]).close(False)
-
-
-def process_mpim_open(message_json):
- server = servers.find(message_json["_server"])
- server.channels.find(message_json["channel"]).open(False)
-
-
-def process_im_close(message_json):
- server = servers.find(message_json["_server"])
- server.channels.find(message_json["channel"]).close(False)
-
-
-def process_im_open(message_json):
- server = servers.find(message_json["_server"])
- server.channels.find(message_json["channel"]).open()
-
-
-def process_im_marked(message_json):
- channel = channels.find(message_json["channel"])
- channel.mark_read(False)
- if channel.channel_buffer is not None:
- w.buffer_set(channel.channel_buffer, "hotlist", "-1")
+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):
- server = servers.find(message_json["_server"])
+def process_im_created(message_json, eventrouter, **kwargs):
+ team = kwargs['team']
+ item = message_json["channel"]
+ c = SlackDMChannel(eventrouter, team=team, users=team.users, **item)
+ 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"]
- channel_name = server.users.find(item["user"]).name
- if server.channels.find(channel_name):
- server.channels.find(channel_name).open(False)
+ if item["name"].startswith("mpdm-"):
+ c = SlackMPDMChannel(eventrouter, team=kwargs["team"], **item)
else:
- item = message_json["channel"]
- item['name'] = server.users.find(item["user"]).name
- server.add_channel(DmChannel(server, **item))
- server.buffer_prnt("New direct message channel created: {}".format(item["name"]))
-
-
-def process_user_typing(message_json):
- server = servers.find(message_json["_server"])
- channel = server.channels.find(message_json["channel"])
- if channel:
- channel.set_typing(server.users.find(message_json["user"]).name)
-
-
-def process_bot_enable(message_json):
- process_bot_integration(message_json)
-
-
-def process_bot_disable(message_json):
- process_bot_integration(message_json)
+ c = SlackGroupChannel(eventrouter, team=kwargs["team"], **item)
+ kwargs['team'].channels[item["id"]] = c
+ kwargs['team'].channels[item["id"]].open()
-
-def process_bot_integration(message_json):
- server = servers.find(message_json["_server"])
- channel = server.channels.find(message_json["channel"])
-
- time = message_json['ts']
- text = "{} {}".format(server.users.find(message_json['user']).formatted_name(),
- render_message(message_json))
- bot_name = get_user(message_json, server)
- bot_name = bot_name.encode('utf-8')
- channel.buffer_prnt(bot_name, text, time)
-
-# todo: does this work?
-
-
-def process_error(message_json):
- pass
-
-
-def process_reaction_added(message_json):
+def process_reaction_added(message_json, eventrouter, **kwargs):
+ channel = kwargs['team'].channels[message_json["item"]["channel"]]
if message_json["item"].get("type") == "message":
- channel = channels.find(message_json["item"]["channel"])
- channel.add_reaction(message_json["item"]["ts"], message_json["reaction"], message_json["user"])
- else:
- dbg("Reaction to item type not supported: " + str(message_json))
+ ts = SlackTS(message_json['item']["ts"])
-
-def process_reaction_removed(message_json):
- if message_json["item"].get("type") == "message":
- channel = channels.find(message_json["item"]["channel"])
- channel.remove_reaction(message_json["item"]["ts"], message_json["reaction"], message_json["user"])
+ message = channel.messages.get(ts, None)
+ if message:
+ message.add_reaction(message_json["reaction"], message_json["user"])
+ channel.change_message(ts)
else:
- dbg("Reaction to item type not supported: " + str(message_json))
+ 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"]]
+ if message_json["item"].get("type") == "message":
+ ts = SlackTS(message_json['item']["ts"])
-def create_reaction_string(reactions):
- count = 0
- if not isinstance(reactions, list):
- reaction_string = " [{}]".format(reactions)
+ message = channel.messages.get(ts, None)
+ if message:
+ message.remove_reaction(message_json["reaction"], message_json["user"])
+ channel.change_message(ts)
else:
- reaction_string = ' ['
- for r in reactions:
- if len(r["users"]) > 0:
- count += 1
- if config.show_reaction_nicks:
- nicks = [resolve_ref("@{}".format(user)) for user in r["users"]]
- users = "({})".format(",".join(nicks))
- else:
- users = len(r["users"])
- reaction_string += ":{}:{} ".format(r["name"], users)
- reaction_string = reaction_string[:-1] + ']'
- if count == 0:
- reaction_string = ''
- return reaction_string
-
-
-def modify_buffer_line(buffer, new_line, time):
- time = int(float(time))
- # get a pointer to this buffer's lines
- own_lines = w.hdata_pointer(w.hdata_get('buffer'), buffer, 'own_lines')
- if own_lines:
- # get a pointer to the last line
- line_pointer = w.hdata_pointer(w.hdata_get('lines'), own_lines, 'last_line')
- # 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')
-
- while 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:
- date = w.hdata_time(struct_hdata_line_data, data, 'date')
- # prefix = w.hdata_string(struct_hdata_line_data, data, 'prefix')
-
- if int(date) == int(time):
- # w.prnt("", "found matching time date is {}, time is {} ".format(date, time))
- w.hdata_update(struct_hdata_line_data, data, {"message": new_line})
- break
- else:
- pass
- # 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)
- return w.WEECHAT_RC_OK
+ dbg("Reaction to item type not supported: " + str(message_json))
+###### New module/global methods
-def render_message(message_json, force=False):
+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", ""):
return message_json["_rendered_text"]
@@ -1887,96 +2225,91 @@ def render_message(message_json, force=False):
text = text.replace("&lt;", "<")
text = text.replace("&gt;", ">")
text = text.replace("&amp;", "&")
+
+ if type(text) is not unicode:
+ text = text.decode('UTF-8', 'replace')
text = text.encode('utf-8')
- if "reactions" in message_json:
- text += create_reaction_string(message_json["reactions"])
- message_json["_rendered_text"] = text
- return 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", ""))
-def process_message(message_json, cache=True):
- try:
- # send these subtype messages elsewhere
- known_subtypes = ["message_changed", 'message_deleted', 'channel_join', 'channel_leave', 'channel_topic', 'group_join', 'group_leave', 'group_topic', 'bot_enable', 'bot_disable']
- if "subtype" in message_json and message_json["subtype"] in known_subtypes:
- proc[message_json["subtype"]](message_json)
+ message_json["_rendered_text"] = text
- else:
- server = servers.find(message_json["_server"])
- channel = channels.find(message_json["channel"])
+ return text
- # do not process messages in unexpected channels
- if not channel.active:
- channel.open(False)
- dbg("message came for closed channel {}".format(channel.name))
- return
+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.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])
+ else:
+ try:
+ if usernames[named[1]]:
+ message[item[0]] = "<@{}>{}".format(usernames[named[1]], named[2])
+ except:
+ message[item[0]] = "@{}{}".format(named[1], named[2])
+ if targets and targets.groups()[0] == '#':
+ named = targets.groups()
+ try:
+ if channels[named[1]]:
+ message[item[0]] = "<#{}|{}>{}".format(channels[named[1]], named[1], named[2])
+ except:
+ message[item[0]] = "#{}{}".format(named[1], named[2])
- time = message_json['ts']
- text = render_message(message_json)
- name = get_user(message_json, server)
- name = name.encode('utf-8')
+ #dbg(message)
+ return " ".join(message)
- # special case with actions.
- if text.startswith("_") and text.endswith("_"):
- text = text[1:-1]
- if name != channel.server.nick:
- text = name + " " + text
- channel.buffer_prnt(w.prefix("action").rstrip(), text, time)
+def unfurl_refs(text, ignore_alt_text=False):
+ """
+ input : <@U096Q7CQM|someuser> has joined the channel
+ ouput : someuser has joined the channel
+ """
+ # Find all strings enclosed by <>
+ # - <https://example.com|example with spaces>
+ # - <#C2147483705|#otherchannel>
+ # - <@U2147483697|@othernick>
+ # Test patterns lives in ./_pytest/test_unfurl.py
+ 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))
+ return text
+def unfurl_ref(ref, ignore_alt_text=False):
+ id = ref.split('|')[0]
+ display_text = ref
+ if ref.find('|') > -1:
+ if ignore_alt_text:
+ display_text = resolve_ref(id)
+ else:
+ if id.startswith("#C") or id.startswith("@U"):
+ display_text = ref.split('|')[1]
else:
- suffix = ''
- if 'edited' in message_json:
- suffix = ' (edited)'
- channel.buffer_prnt(name, text + suffix, time)
-
- if cache:
- channel.cache_message(message_json)
-
- except Exception:
- channel = channels.find(message_json["channel"])
- dbg("cannot process message {}\n{}".format(message_json, traceback.format_exc()))
- if channel and ("text" in message_json) and message_json['text'] is not None:
- channel.buffer_prnt('unknown', message_json['text'])
-
-
-def process_message_changed(message_json):
- m = message_json["message"]
- if "message" in message_json:
- if "attachments" in m:
- message_json["attachments"] = m["attachments"]
- if "text" in m:
- if "text" in message_json:
- message_json["text"] += m["text"]
- dbg("added text!")
- else:
- message_json["text"] = m["text"]
- if "fallback" in m:
- if "fallback" in message_json:
- message_json["fallback"] += m["fallback"]
- else:
- message_json["fallback"] = m["fallback"]
-
- text_before = (len(m['text']) > 0)
- m["text"] += unwrap_attachments(message_json, text_before)
- channel = channels.find(message_json["channel"])
- if "edited" in m:
- channel.change_message(m["ts"], m["text"], ' (edited)')
+ url, desc = ref.split('|', 1)
+ display_text = u"{} ({})".format(url, desc)
else:
- channel.change_message(m["ts"], m["text"])
-
-
-def process_message_deleted(message_json):
- channel = channels.find(message_json["channel"])
- channel.change_message(message_json["deleted_ts"], "(deleted)")
-
+ display_text = resolve_ref(ref)
+ return display_text
def unwrap_attachments(message_json, text_before):
attachment_text = ''
- if "attachments" in message_json:
+ a = message_json.get("attachments", None)
+ if a:
if text_before:
attachment_text = u'\n'
- for attachment in message_json["attachments"]:
+ for attachment in a:
# Attachments should be rendered roughly like:
#
# $pretext
@@ -1989,423 +2322,474 @@ def unwrap_attachments(message_json, text_before):
prepend_title_text = attachment['author_name'] + ": "
if 'pretext' in attachment:
t.append(attachment['pretext'])
- if "title" in attachment:
- if 'title_link' in attachment:
- t.append('%s%s (%s)' % (prepend_title_text, attachment["title"], attachment["title_link"],))
- else:
- t.append(prepend_title_text + attachment["title"])
+ title = attachment.get('title', None)
+ title_link = attachment.get('title_link', None)
+ 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)
prepend_title_text = ''
- elif "from_url" in attachment:
- t.append(attachment["from_url"])
- if "text" in attachment:
- tx = re.sub(r' *\n[\n ]+', '\n', attachment["text"])
+ t.append(attachment.get("from_url", ""))
+
+ atext = attachment.get("text", None)
+ if atext:
+ tx = re.sub(r' *\n[\n ]+', '\n', atext)
t.append(prepend_title_text + tx)
prepend_title_text = ''
- if 'fields' in attachment:
- for f in attachment['fields']:
+ fields = attachment.get("fields", None)
+ if fields:
+ for f in fields:
if f['title'] != '':
t.append('%s %s' % (f['title'], f['value'],))
else:
t.append(f['value'])
- if t == [] and "fallback" in attachment:
- t.append(attachment["fallback"])
+ 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
def resolve_ref(ref):
- if ref.startswith('@U') or ref.startswith('@W'):
- if users.find(ref[1:]):
- try:
- return "@{}".format(users.find(ref[1:]).name)
- except:
- dbg("NAME: {}".format(ref))
- elif ref.startswith('#C'):
- if channels.find(ref[1:]):
- try:
- return "{}".format(channels.find(ref[1:]).name)
- except:
- dbg("CHANNEL: {}".format(ref))
-
- # Something else, just return as-is
+ #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:
+ return "@{}".format(e.teams[t].users[ref[1:]].name)
+ #except:
+ # dbg("NAME: {}".format(ref))
+ elif ref.startswith('#C'):
+ for t in e.teams.keys():
+ if ref[1:] in e.teams[t].channels:
+ #try:
+ return "{}".format(e.teams[t].channels[ref[1:]].name)
+ #except:
+ # dbg("CHANNEL: {}".format(ref))
+
+ # Something else, just return as-is
return ref
-
-def unfurl_ref(ref, ignore_alt_text=False):
- id = ref.split('|')[0]
- display_text = ref
- if ref.find('|') > -1:
- if ignore_alt_text:
- display_text = resolve_ref(id)
- else:
- if id.startswith("#C") or id.startswith("@U"):
- display_text = ref.split('|')[1]
- else:
- url, desc = ref.split('|', 1)
- display_text = u"{} ({})".format(url, desc)
- else:
- display_text = resolve_ref(ref)
- return display_text
-
-
-def unfurl_refs(text, ignore_alt_text=False):
- """
- input : <@U096Q7CQM|someuser> has joined the channel
- ouput : someuser has joined the channel
- """
- # Find all strings enclosed by <>
- # - <https://example.com|example with spaces>
- # - <#C2147483705|#otherchannel>
- # - <@U2147483697|@othernick>
- # Test patterns lives in ./_pytest/test_unfurl.py
- 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))
- return text
-
-
-def get_user(message_json, server):
- if 'bot_id' in message_json and message_json['bot_id'] is not None:
- name = u"{} :]".format(server.bots.find(message_json["bot_id"]).formatted_name())
- elif 'user' in message_json:
- u = server.users.find(message_json['user'])
- if u.is_bot:
- name = u"{} :]".format(u.formatted_name())
- else:
- name = u.name
- elif 'username' in message_json:
- name = u"-{}-".format(message_json["username"])
- elif 'service_name' in message_json:
- name = u"-{}-".format(message_json["service_name"])
+def create_reaction_string(reactions):
+ count = 0
+ if not isinstance(reactions, list):
+ reaction_string = " [{}]".format(reactions)
else:
- name = u""
- return name
-
-# END Websocket handling methods
-
-
-def typing_bar_item_cb(data, buffer, args):
- typers = [x for x in channels if x.is_someone_typing()]
- if len(typers) > 0:
- direct_typers = []
- channel_typers = []
- for dm in channels.find_by_class(DmChannel):
- direct_typers.extend(dm.get_typing_list())
- direct_typers = ["D/" + x for x in direct_typers]
- current_channel = w.current_buffer()
- channel = channels.find(current_channel)
- try:
- if channel and channel.__class__ != DmChannel:
- channel_typers = channels.find(current_channel).get_typing_list()
- except:
- w.prnt("", "Bug on {}".format(channel))
- typing_here = ", ".join(channel_typers + direct_typers)
- if len(typing_here) > 0:
- color = w.color('yellow')
- return color + "typing: " + typing_here
- return ""
-
-
-def typing_update_cb(data, remaining_calls):
- w.bar_item_update("slack_typing_notice")
- return w.WEECHAT_RC_OK
-
-
-def buffer_list_update_cb(data, remaining_calls):
- global buffer_list_update
-
- now = time.time()
- if buffer_list_update and previous_buffer_list_update + 1 < now:
- # gray_check = False
- # if len(servers) > 1:
- # gray_check = True
- for channel in channels:
- channel.rename()
- buffer_list_update = False
- return w.WEECHAT_RC_OK
-
+ reaction_string = ' ['
+ for r in reactions:
+ if len(r["users"]) > 0:
+ count += 1
+ if config.show_reaction_nicks:
+ nicks = [resolve_ref("@{}".format(user)) for user in r["users"]]
+ users = "({})".format(",".join(nicks))
+ else:
+ users = len(r["users"])
+ reaction_string += ":{}:{} ".format(r["name"], users)
+ reaction_string = reaction_string[:-1] + ']'
+ if count == 0:
+ reaction_string = ''
+ return reaction_string
-def buffer_list_update_next():
- global buffer_list_update
- buffer_list_update = True
+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')
+ if own_lines:
+ # get a pointer to the last line
+ line_pointer = w.hdata_pointer(w.hdata_get('lines'), own_lines, 'last_line')
+ # 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')
+ while 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:
+ line_timestamp = w.hdata_time(struct_hdata_line_data, data, 'date')
+ line_time_id = w.hdata_integer(struct_hdata_line_data, data, 'date_printed')
+ # prefix = w.hdata_string(struct_hdata_line_data, data, 'prefix')
-def hotlist_cache_update_cb(data, remaining_calls):
- # this keeps the hotlist dupe up to date for the buffer switch, but is prob technically a race condition. (meh)
- global hotlist
- prev_hotlist = hotlist
- hotlist = w.infolist_get("hotlist", "", "")
- w.infolist_free(prev_hotlist)
+ 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})
+ break
+ else:
+ pass
+ # 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)
return w.WEECHAT_RC_OK
-def buffer_closing_cb(signal, sig_type, data):
- if channels.find(data):
- channels.find(data).closed()
- return w.WEECHAT_RC_OK
+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:
+ # get a pointer to the last line
+ line_pointer = w.hdata_pointer(w.hdata_get('lines'), own_lines, 'last_line')
+ # 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')
+ # 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})
-def buffer_opened_cb(signal, sig_type, data):
- channels.update_hashtable()
return w.WEECHAT_RC_OK
+def tag(tagset, user=None):
+ if user:
+ user.replace(" ", "_")
+ default_tag = "nick_" + user
+ 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",
+ }
+ return default_tag + "," + tagsets[tagset]
-def buffer_switch_cb(signal, sig_type, data):
- global previous_buffer, hotlist
- # this is to see if we need to gray out things in the buffer list
- if channels.find(previous_buffer):
- channels.find(previous_buffer).mark_read()
-
- new_channel = channels.find(data)
- if new_channel:
- if new_channel.got_history == False:
- new_channel.get_history()
- # channel_name = current_buffer_name()
- previous_buffer = data
- return w.WEECHAT_RC_OK
+###### New/converted command_ commands
-def typing_notification_cb(signal, sig_type, data):
- msg = w.buffer_get_string(data, "input")
- if len(msg) > 8 and msg[:1] != "/":
- global typing_timer
- now = time.time()
- if typing_timer + 4 < now:
- channel = channels.find(current_buffer_name())
- if channel:
- identifier = channel.identifier
- request = {"type": "typing", "channel": identifier}
- channel.server.send_to_websocket(request, expect_reply=False)
- typing_timer = now
- return w.WEECHAT_RC_OK
+@slack_buffer_or_ignore
+def part_command_cb(data, current_buffer, args):
+ e = EVENTROUTER
+ args = args.split()
+ if len(args) > 1:
+ team = e.weechat_controller.buffers[current_buffer].team
+ cmap = team.get_channel_map()
+ channel = "".join(args[1:])
+ if channel in cmap:
+ buffer_ptr = team.channels[cmap[channel]].channel_buffer
+ e.weechat_controller.unregister_buffer(buffer_ptr, update_remote=True, close_buffer=True)
+ else:
+ 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
-def slack_ping_cb(data, remaining):
+@slack_buffer_required
+def command_topic(data, current_buffer, args):
"""
- Periodic websocket ping to detect broken connection.
+ Change the topic of a channel
+ /slack topic [<channel>] [<topic>|-delete]
"""
- servers.find(data).ping()
- return w.WEECHAT_RC_OK
-
+ 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:])
+ else:
+ channel = e.weechat_controller.buffers[current_buffer]
+ topic = " ".join(args[1:])
-def slack_connection_persistence_cb(data, remaining_calls):
- """
- Reconnect if a connection is detected down
- """
- for server in servers:
- if not server.connected:
- server.buffer_prnt("Disconnected from slack, trying to reconnect..")
- if server.ws_hook is not None:
- w.unhook(server.ws_hook)
- server.connect_to_slack()
- return w.WEECHAT_RC_OK
+ if channel:
+ if topic == "-delete":
+ topic = ''
+ 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
+ else:
+ return w.WEECHAT_RC_ERROR_EAT
+@slack_buffer_or_ignore
+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 slack_never_away_cb(data, remaining):
- global never_away
- if never_away:
- for server in servers:
- identifier = server.channels.find("slackbot").identifier
- request = {"type": "typing", "channel": identifier}
- # request = {"type":"typing","channel":"slackbot"}
- server.send_to_websocket(request, expect_reply=False)
- return w.WEECHAT_RC_OK
+@slack_buffer_or_ignore
+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 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.send_message(message)
+ return w.WEECHAT_RC_OK_EAT
-def nick_completion_cb(data, completion_item, buffer, completion):
+@slack_buffer_or_ignore
+def command_talk(data, current_buffer, args):
"""
- Adds all @-prefixed nicks to completion list
+ Open a chat with the specified user
+ /slack talk [user]
"""
+ 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 = arg[1:]
+ if channel_name in c:
+ chan = team.channels[c[channel_name]]
+ 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
- channel = channels.find(buffer)
- if channel is None or channel.members is None:
- return w.WEECHAT_RC_OK
- for m in channel.members:
- user = channel.server.users.find(m)
- w.hook_completion_list_add(completion, "@" + user.name, 1, w.WEECHAT_LIST_POS_SORT)
- return w.WEECHAT_RC_OK
-
-
-def complete_next_cb(data, 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
- completion lists, then let Weechat's internal completion do its
- thing
-
- """
+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))
- channel = channels.find(buffer)
- if channel is None or channel.members is None:
- return w.WEECHAT_RC_OK
- input = w.buffer_get_string(buffer, "input")
- current_pos = w.buffer_get_integer(buffer, "input_pos") - 1
- input_length = w.buffer_get_integer(buffer, "input_length")
- word_start = 0
- word_end = input_length
- # If we're on a non-word, look left for something to complete
- while current_pos >= 0 and input[current_pos] != '@' and not input[current_pos].isalnum():
- current_pos = current_pos - 1
- if current_pos < 0:
- current_pos = 0
- for l in range(current_pos, 0, -1):
- if input[l] != '@' and not input[l].isalnum():
- word_start = l + 1
- break
- for l in range(current_pos, input_length):
- if not input[l].isalnum():
- word_end = l
- break
- word = input[word_start:word_end]
- for m in channel.members:
- user = channel.server.users.find(m)
- if user.name == word:
- # Here, we cheat. Insert a @ in front and rely in the @
- # nicks being in the completion list
- w.buffer_set(buffer, "input", input[:word_start] + "@" + input[word_start:])
- w.buffer_set(buffer, "input_pos", str(w.buffer_get_integer(buffer, "input_pos") + 1))
+def thread_command_callback(data, current_buffer, args):
+ current = w.current_buffer()
+ channel = EVENTROUTER.weechat_controller.buffers.get(current)
+ if channel:
+ args = args.split()
+ if args[0] == '/thread':
+ if len(args) == 2:
+ try:
+ pm = channel.messages[SlackTS(args[1])]
+ except:
+ pm = channel.hashed_messages[args[1]]
+ tc = SlackThreadChannel(EVENTROUTER, pm)
+ pm.thread_channel = tc
+ tc.open()
+ #tc.create_buffer()
+ 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])
+ channel.send_message(msg, request_dict_ext={"thread_ts": parent_id})
return w.WEECHAT_RC_OK_EAT
- return w.WEECHAT_RC_OK
+ w.prnt(current, "Invalid thread command.")
+ return w.WEECHAT_RC_OK_EAT
+def rehistory_command_callback(data, current_buffer, args):
+ current = w.current_buffer()
+ channel = EVENTROUTER.weechat_controller.buffers.get(current)
+ channel.got_history = False
+ w.buffer_clear(channel.channel_buffer)
+ channel.get_history()
+ return w.WEECHAT_RC_OK_EAT
-# Slack specific requests
-def async_slack_api_request(domain, token, request, post_data, priority=False):
- if not STOP_TALKING_TO_SLACK:
- post_data["token"] = token
- url = 'url:https://{}/api/{}?{}'.format(domain, request, urllib.urlencode(post_data))
- context = pickle.dumps({"request": request, "token": token, "post_data": post_data})
- params = {'useragent': 'wee_slack {}'.format(SCRIPT_VERSION)}
- dbg("URL: {} context: {} params: {}".format(url, context, params))
- w.hook_process_hashtable(url, params, config.slack_timeout, "url_processor_cb", context)
+@slack_buffer_required
+def hide_command_callback(data, current_buffer, args):
+ c = EVENTROUTER.weechat_controller.buffers.get(current_buffer, None)
+ if c:
+ name = c.formatted_name(style='long_default')
+ if name in config.distracting_channels:
+ w.buffer_set(c.channel_buffer, "hidden", "1")
+ return w.WEECHAT_RC_OK_EAT
+def slack_command_cb(data, current_buffer, args):
+ a = args.split(' ', 1)
+ if len(a) > 1:
+ function_name, args = a[0], args
+ else:
+ function_name, args = a[0], args
-def async_slack_api_upload_request(token, request, post_data, priority=False):
- if not STOP_TALKING_TO_SLACK:
- url = 'https://slack.com/api/{}'.format(request)
- file_path = os.path.expanduser(post_data["file"])
- if ' ' in file_path:
- file_path = file_path.replace(' ','\ ')
- command = 'curl -F file=@{} -F channels={} -F token={} {}'.format(file_path, post_data["channels"], token, url)
- context = pickle.dumps({"request": request, "token": token, "post_data": post_data})
- w.hook_process(command, config.slack_timeout, "url_processor_cb", context)
+ try:
+ EVENTROUTER.cmds[function_name]("", current_buffer, args)
+ except KeyError:
+ 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)
+ if channel:
+ fullname = channel.formatted_name(style="long_default")
+ if config.distracting_channels.count(fullname) == 0:
+ config.distracting_channels.append(fullname)
+ else:
+ config.distracting_channels.pop(config.distracting_channels.index(fullname))
+ save_distracting_channels()
-# funny, right?
-big_data = {}
+def save_distracting_channels():
+ w.config_set_plugin('distracting_channels', ','.join(config.distracting_channels))
+@slack_buffer_required
+def command_slash(data, current_buffer, args):
+ """
+ Support for custom slack commands
+ /slack slash /customcommand arg1 arg2 arg3
+ """
+ e = EVENTROUTER
+ channel = e.weechat_controller.buffers.get(current_buffer, None)
+ if channel:
+ team = channel.team
-def url_processor_cb(data, command, return_code, out, err):
- global big_data
- data = pickle.loads(data)
- identifier = sha.sha("{}{}".format(data, command)).hexdigest()
- if identifier not in big_data:
- big_data[identifier] = ''
- big_data[identifier] += out
- if return_code == 0:
- try:
- my_json = json.loads(big_data[identifier])
- except:
- dbg("request failed, doing again...")
- dbg("response length: {} identifier {}\n{}".format(len(big_data[identifier]), identifier, data))
- my_json = False
+ if args is None:
+ server.buffer_prnt("Usage: /slack slash /someslashcommand [arguments...].")
+ return
- big_data.pop(identifier, None)
+ split_args = args.split(None, 2)
+ command = split_args[1]
+ text = split_args[2] if len(split_args) > 2 else ""
- if my_json:
- if data["request"] == 'rtm.start':
- servers.find(data["token"]).connected_to_slack(my_json)
- servers.update_hashtable()
+ 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)
- else:
- if "channel" in data["post_data"]:
- channel = data["post_data"]["channel"]
- token = data["token"]
- if "messages" in my_json:
- my_json["messages"].reverse()
- for message in my_json["messages"]:
- message["_server"] = servers.find(token).domain
- message["channel"] = servers.find(token).channels.find(channel).identifier
- process_message(message)
- if "channel" in my_json:
- if "members" in my_json["channel"]:
- channels.find(my_json["channel"]["id"]).members = set(my_json["channel"]["members"])
+@slack_buffer_required
+def command_mute(data, current_buffer, args):
+ current = w.current_buffer()
+ channel_id = EVENTROUTER.weechat_controller.buffers[current].identifier
+ team = EVENTROUTER.weechat_controller.buffers[current].team
+ if channel_id not in team.muted_channels:
+ team.muted_channels.add(channel_id)
else:
- if return_code != -1:
- big_data.pop(identifier, None)
- dbg("return code: {}, data: {}, output: {}, error: {}".format(return_code, data, out, err))
-
- return w.WEECHAT_RC_OK
-
-
-def cache_write_cb(data, remaining):
- cache_file = open("{}/{}".format(WEECHAT_HOME, CACHE_NAME), 'w')
- cache_file.write(CACHE_VERSION + "\n")
- for channel in channels:
- if channel.active:
- for message in channel.messages:
- cache_file.write("{}\n".format(json.dumps(message.message_json)))
- return w.WEECHAT_RC_OK
+ team.muted_channels.discard(channel_id)
+ 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
+ channel = EVENTROUTER.weechat_controller.buffers[current_buffer]
+ if isinstance(channel, SlackTeam):
+ url = "https://{}".format(channel.team.domain)
+ else:
+ now = SlackTS()
+ 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 cache_load():
- global message_cache
- try:
- file_name = "{}/{}".format(WEECHAT_HOME, CACHE_NAME)
- cache_file = open(file_name, 'r')
- if cache_file.readline() == CACHE_VERSION + "\n":
- dbg("Loading messages from cache.", main_buffer=True)
- for line in cache_file:
- j = json.loads(line)
- message_cache[j["channel"]].append(line)
- dbg("Completed loading messages from cache.", main_buffer=True)
- except ValueError:
- w.prnt("", "Failed to load cache file, probably illegal JSON.. Ignoring")
- pass
- except IOError:
- w.prnt("", "cache file not found")
- pass
+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:
+ 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:
+ # dbg("Can't hide channel {} .. removing..".format(channel), main_buffer=True)
+# config.distracting_channels.pop(config.distracting_channels.index(channel))
+# save_distracting_channels()
-# END Slack specific requests
+@slack_buffer_required
+def command_upload(data, current_buffer, args):
+ channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer)
+ url = 'https://slack.com/api/files.upload'
+ fname = args.split(' ', 1)
+ file_path = os.path.expanduser(fname[1])
+ team = EVENTROUTER.weechat_controller.buffers[current_buffer].team
+ if ' ' in file_path:
+ file_path = file_path.replace(' ', '\ ')
+
+ command = 'curl -F file=@{} -F channels={} -F token={} {}'.format(file_path, channel.identifier, team.token, url)
+ w.hook_process(command, config.slack_timeout, '', '')
-# Utility Methods
+def away_command_cb(data, current_buffer, args):
+ #TODO: reimplement all.. maybe
+ (all, message) = re.match("^/away(?:\s+(-all))?(?:\s+(.+))?", args).groups()
+ if message is None:
+ command_back(data, current_buffer, args)
+ else:
+ command_away(data, current_buffer, args)
+ return w.WEECHAT_RC_OK
+@slack_buffer_required
+def command_away(data, current_buffer, args):
+ """
+ Sets your status as 'away'
+ /slack away
+ """
+ team = EVENTROUTER.weechat_controller.buffers[current_buffer].team
+ s = SlackRequest(team.token, "presence.set", {"presence": "away"}, team_hash=team.team_hash)
+ EVENTROUTER.receive(s)
-def current_domain_name():
- buffer = w.current_buffer()
- if servers.find(buffer):
- return servers.find(buffer).domain
- else:
- # number = w.buffer_get_integer(buffer, "number")
- name = w.buffer_get_string(buffer, "name")
- name = ".".join(name.split(".")[:-1])
- return name
+@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)
+ EVENTROUTER.receive(s)
-def current_buffer_name(short=False):
- buffer = w.current_buffer()
- # number = w.buffer_get_integer(buffer, "number")
- name = w.buffer_get_string(buffer, "name")
- if short:
- try:
- name = name.split('.')[-1]
- except:
- pass
- return name
+@slack_buffer_required
+def label_command_cb(data, current_buffer, args):
+ channel = EVENTROUTER.weechat_controller.buffers.get(current_buffer)
+ if channel and channel.type == 'thread':
+ aargs = args.split(None, 2)
+ new_name = " +" + aargs[1]
+ channel.label = new_name
+ w.buffer_set(channel.channel_buffer, "short_name", new_name)
+def command_p(data, current_buffer, args):
+ args = args.split(' ', 1)[1]
+ w.prnt("", "{}".format(eval(args)))
-def closed_slack_buffer_cb(data, buffer):
- global slack_buffer
- slack_buffer = None
- return w.WEECHAT_RC_OK
+###### NEW EXCEPTIONS
+class ProcessNotImplemented(Exception):
+ """
+ Raised when we try to call process_(something), but
+ (something) has not been defined as a function.
+ """
+ def __init__(self, function_name):
+ super(ProcessNotImplemented, self).__init__(function_name)
-def create_slack_buffer():
- global slack_buffer
- slack_buffer = w.buffer_new("slack", "", "", "closed_slack_buffer_cb", "")
- w.buffer_set(slack_buffer, "notify", "0")
- # w.buffer_set(slack_buffer, "display", "1")
- return w.WEECHAT_RC_OK
+class InvalidType(Exception):
+ """
+ Raised when we do type checking to ensure objects of the wrong
+ type are not used improperly.
+ """
+ def __init__(self, type_str):
+ super(InvalidType, self).__init__(type_str)
+###### New but probably old and need to migrate
def closed_slack_debug_buffer_cb(data, buffer):
global slack_debug
@@ -2422,39 +2806,100 @@ 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 quit_notification_cb(signal, sig_type, data):
- stop_talking_to_slack()
-
-
-def script_unloaded():
- stop_talking_to_slack()
+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
-
-def stop_talking_to_slack():
+def setup_hooks():
+ cmds = {k[8:]: v for k, v in globals().items() if k.startswith("command_")}
+
+ w.bar_item_new('slack_typing_notice', 'typing_bar_item_cb', '')
+
+ w.hook_timer(1000, 0, 0, "typing_update_cb", "")
+ w.hook_timer(1000, 0, 0, "buffer_list_update_callback", "EVENTROUTER")
+ w.hook_timer(3000, 0, 0, "reconnect_callback", "EVENTROUTER")
+ w.hook_timer(1000 * 60 * 5, 0, 0, "slack_never_away_cb", "")
+
+ 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_command(
+ # Command name and description
+ 'slack', 'Plugin to allow typing notification and sync of read markers for slack.com',
+ # Usage
+ '[command] [command options]',
+ # Description of arguments
+ 'Commands:\n' +
+ '\n'.join(cmds.keys()) +
+ '\nUse /slack help [command] to find out more\n',
+ # Completions
+ '|'.join(cmds.keys()),
+ # Function name
+ 'slack_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('/thread', 'thread_command_callback', '')
+ w.hook_command_run('/reply', 'thread_command_callback', '')
+ w.hook_command_run('/rehistory', 'rehistory_command_callback', '')
+ w.hook_command_run('/hide', 'hide_command_callback', '')
+ 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('/away', 'away_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", "")
+
+
+##### END NEW
+
+
+def dbg(message, level=0, main_buffer=False, fout=False):
"""
- Prevents a race condition where quitting closes buffers
- which triggers leaving the channel because of how close
- buffer is handled
+ send debug output to the slack-debug buffer and optionally write to a file.
"""
- global STOP_TALKING_TO_SLACK
- STOP_TALKING_TO_SLACK = True
- cache_write_cb("", "")
- return w.WEECHAT_RC_OK
-
-
-def scrolled_cb(signal, sig_type, data):
- try:
- if w.window_get_integer(data, "scrolling") == 1:
- channels.find(w.current_buffer()).set_scrolling()
+ #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("", "slack: " + message)
else:
- channels.find(w.current_buffer()).unset_scrolling()
- except:
- pass
- return w.WEECHAT_RC_OK
+ if slack_debug and (not debug_string or debug_string in message):
+ #w.prnt(slack_debug, "---------")
+ w.prnt(slack_debug, message)
+
-# END Utility Methods
+###### Config code
class PluginConfig(object):
# Default settings.
@@ -2468,6 +2913,7 @@ class PluginConfig(object):
'colorize_nicks': 'true',
'colorize_private_chats': 'false',
'debug_mode': 'false',
+ 'debug_level': '3',
'distracting_channels': '',
'show_reaction_nicks': 'false',
'slack_api_token': 'INSERT VALID KEY HERE!',
@@ -2475,12 +2921,22 @@ class PluginConfig(object):
'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': '',
}
# Set missing settings to their defaults. Load non-missing settings from
# weechat configs.
def __init__(self):
- for key,default in self.settings.iteritems():
+ self.migrate()
+ for key, default in self.settings.iteritems():
if not w.config_get_plugin(key):
w.config_set_plugin(key, default)
self.config_changed(None, None, None)
@@ -2514,6 +2970,11 @@ class PluginConfig(object):
def get_distracting_channels(self, key):
return [x.strip() for x in w.config_get_plugin(key).split(',')]
+ def get_server_aliases(self, key):
+ alias_list = w.config_get_plugin(key)
+ if len(alias_list) > 0:
+ return dict(item.split(":") for item in alias_list.split(","))
+
def get_slack_api_token(self, key):
token = w.config_get_plugin("slack_api_token")
if token.startswith('${sec.data'):
@@ -2521,9 +2982,56 @@ 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
+ """
+ if not w.config_get_plugin("migrated"):
+ for k in self.settings.keys():
+ if not w.config_is_set_plugin(k):
+ p = w.config_get("plugins.var.python.slack_extension.{}".format(k))
+ data = w.config_string(p)
+ if data != "":
+ w.config_set_plugin(k, data)
+ w.config_set_plugin("migrated", "true")
+
+
+# to Trace execution, add `setup_trace()` to startup
+# and to a function and sys.settrace(trace_calls) to a function
+def setup_trace():
+ global f
+ now = time.time()
+ f = open('{}/{}-trace.json'.format(RECORD_DIR, now), 'w')
+
+def trace_calls(frame, event, arg):
+ global f
+ if event != 'call':
+ return
+ co = frame.f_code
+ func_name = co.co_name
+ if func_name == 'write':
+ # Ignore write() calls from print statements
+ return
+ func_line_no = frame.f_lineno
+ func_filename = co.co_filename
+ caller = frame.f_back
+ caller_line_no = caller.f_lineno
+ caller_filename = caller.f_code.co_filename
+ print >> f, 'Call to %s on line %s of %s from line %s of %s' % \
+ (func_name, func_line_no, func_filename,
+ caller_line_no, caller_filename)
+ f.flush()
+ return
+
# Main
if __name__ == "__main__":
@@ -2536,81 +3044,46 @@ if __name__ == "__main__":
w.prnt("", "\nERROR: Weechat version 1.3+ is required to use {}.\n\n".format(SCRIPT_NAME))
else:
- WEECHAT_HOME = w.info_get("weechat_dir", "")
- CACHE_NAME = "slack.cache"
- STOP_TALKING_TO_SLACK = False
+ global EVENTROUTER
+ EVENTROUTER = EventRouter()
+ #setup_trace()
+
+ #WEECHAT_HOME = w.info_get("weechat_dir", "")
+ #CACHE_NAME = "slack.cache"
+ #STOP_TALKING_TO_SLACK = False
# Global var section
slack_debug = None
config = PluginConfig()
config_changed_cb = config.config_changed
- cmds = {k[8:]: v for k, v in globals().items() if k.startswith("command_")}
- proc = {k[8:]: v for k, v in globals().items() if k.startswith("process_")}
-
typing_timer = time.time()
- domain = None
- previous_buffer = None
- slack_buffer = None
-
- buffer_list_update = False
- previous_buffer_list_update = 0
+ #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)
- cache_load()
+ #hotlist = w.infolist_get("hotlist", "", "")
+ #main_weechat_buffer = w.info_get("irc_buffer", "{}.{}".format(domain, "DOESNOTEXIST!@#$"))
- servers = SearchList()
- for token in config.slack_api_token.split(','):
- server = SlackServer(token)
- servers.append(server)
- channels = SearchList()
- users = SearchList()
+ #message_cache = collections.defaultdict(list)
+ #if config.cache_messages:
+ # cache_load()
w.hook_config("plugins.var.python." + SCRIPT_NAME + ".*", "config_changed_cb", "")
- w.hook_timer(3000, 0, 0, "slack_connection_persistence_cb", "")
+
+ load_emoji()
+ setup_hooks()
# attach to the weechat hooks we need
- w.hook_timer(1000, 0, 0, "typing_update_cb", "")
- w.hook_timer(1000, 0, 0, "buffer_list_update_cb", "")
- w.hook_timer(1000, 0, 0, "hotlist_cache_update_cb", "")
- w.hook_timer(1000 * 60 * 29, 0, 0, "slack_never_away_cb", "")
- w.hook_timer(1000 * 60 * 5, 0, 0, "cache_write_cb", "")
- w.hook_signal('buffer_closing', "buffer_closing_cb", "")
- w.hook_signal('buffer_opened', "buffer_opened_cb", "")
- w.hook_signal('buffer_switch', "buffer_switch_cb", "")
- w.hook_signal('window_switch', "buffer_switch_cb", "")
- w.hook_signal('input_text_changed', "typing_notification_cb", "")
- w.hook_signal('quit', "quit_notification_cb", "")
- w.hook_signal('window_scrolled', "scrolled_cb", "")
- w.hook_command(
- # Command name and description
- 'slack', 'Plugin to allow typing notification and sync of read markers for slack.com',
- # Usage
- '[command] [command options]',
- # Description of arguments
- 'Commands:\n' +
- '\n'.join(cmds.keys()) +
- '\nUse /slack help [command] to find out more\n',
- # Completions
- '|'.join(cmds.keys()),
- # Function name
- 'slack_command_cb', '')
- # w.hook_command('me', 'me_command_cb', '')
- w.hook_command('me', '', 'stuff', 'stuff2', '', 'me_command_cb', '')
- w.hook_command_run('/query', 'join_command_cb', '')
- w.hook_command_run('/join', 'join_command_cb', '')
- w.hook_command_run('/part', 'part_command_cb', '')
- w.hook_command_run('/leave', 'part_command_cb', '')
- w.hook_command_run('/topic', 'topic_command_cb', '')
- w.hook_command_run('/msg', 'msg_command_cb', '')
- w.hook_command_run("/input complete_next", "complete_next_cb", "")
- w.hook_command_run('/away', 'away_command_cb', '')
- w.hook_completion("nicks", "complete @-nicks for slack",
- "nick_completion_cb", "")
- w.bar_item_new('slack_typing_notice', 'typing_bar_item_cb', '')
+
+ tokens = config.slack_api_token.split(',')
+ for t in tokens:
+ s = SlackRequest(t, 'rtm.start', {})
+ EVENTROUTER.receive(s)
+ if config.record_events:
+ EVENTROUTER.record()
+ EVENTROUTER.handle_next()
+ w.hook_timer(10, 0, 0, "handle_next", "")
# END attach to the weechat hooks we need
diff --git a/weemoji.json b/weemoji.json
new file mode 100644
index 0000000..eb19849
--- /dev/null
+++ b/weemoji.json
@@ -0,0 +1 @@
+{"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"]}