aboutsummaryrefslogtreecommitdiffstats
path: root/warmachine/connections/slack.py
diff options
context:
space:
mode:
Diffstat (limited to 'warmachine/connections/slack.py')
-rw-r--r--warmachine/connections/slack.py281
1 files changed, 281 insertions, 0 deletions
diff --git a/warmachine/connections/slack.py b/warmachine/connections/slack.py
new file mode 100644
index 0000000..b1a5390
--- /dev/null
+++ b/warmachine/connections/slack.py
@@ -0,0 +1,281 @@
1import asyncio
2import json
3import logging
4from pprint import pformat
5from urllib.parse import urlencode
6
7import websockets
8
9from .base import Connection, INITALIZED, CONNECTED
10
11
12#: Define slack as a config section prefix
13__config_prefix__ = 'slack'
14
15
16class SlackWS(Connection):
17 def __init__(self, options, *args, **kwargs):
18 super().__init__(*args, **kwargs)
19
20 self._loop = asyncio.get_event_loop()
21 self.log = logging.getLogger(self.__class__.__name__)
22 self.host = None
23 self.token = options['token']
24
25 self._info = None
26 self.reconnect_url = ''
27
28 self.channel_map = {} # channel and im info keyed by the slack id
29 self.user_map = {} # user info keyed by their slack id
30 self.user_nick_to_id = {} # slack user id mapped to the (nick)name
31
32 self.ws = None
33
34 self.status = INITALIZED
35
36 async def connect(self):
37 self.host = self.authenticate()
38 self.log.info('Connecting to {}'.format(self.host))
39 self.ws = await websockets.connect(self.host)
40
41 async def read(self):
42 if self.ws:
43 message = json.loads(await self.ws.recv())
44
45 # Slack is acknowledging a message was sent. Do nothing
46 if 'type' not in message and 'reply_to' in message:
47 # {'ok': True,
48 # 'reply_to': 1,
49 # 'text': "['!whois', 'synic']",
50 # 'ts': '1469743355.000150'}
51 return
52
53 # Handle actual messages
54 elif message['type'] == 'message' and 'subtype' not in message:
55 return await self.process_message(message)
56 else:
57 if 'subtype' in message:
58 # This is a message with a subtype and should be processed
59 # differently
60 msgtype = '{}_{}'.format(
61 message['type'], message['subtype'])
62 else:
63 msgtype = message['type']
64
65 # Look for on_{type} methods to pass the dictionary to for
66 # additional processing
67 func_name = 'on_{}'.format(msgtype)
68 if hasattr(self, func_name):
69 getattr(self, func_name)(message)
70 else:
71 self.log.debug('{} does not exist for message: {}'.format(
72 func_name, message))
73
74 async def say(self, message, destination_id):
75 """
76 Say something in the provided channel or IM by id
77 """
78 await self._send(json.dumps({
79 'id': 1, # TODO: this should be a get_msgid call or something
80 'type': 'message',
81 'channel': destination_id,
82 'text': message
83 }))
84
85 async def _send(self, message):
86 """
87 Send ``message`` to the connected slack server
88 """
89 await self.ws.send(message)
90
91 def authenticate(self):
92 """
93 Populate ``self._info``
94
95 Returns:
96 str: websocket url to connect to
97 """
98 import urllib.request
99 url = 'https://slack.com/api/rtm.start?{}'.format(
100 urlencode(
101 {'token':
102 self.token}))
103 self.log.debug('Connecting to {}'.format(url))
104 req = urllib.request.Request(url)
105
106 r = urllib.request.urlopen(req).read().decode('utf-8')
107 self._info = json.loads(r)
108
109 if not self._info.get('ok', True):
110 raise Exception('Slack Error: {}'.format(
111 self._info.get('error', 'Unknown Error')))
112
113 self.process_connect_info()
114
115 self.log.debug('Got websocket url: {}'.format(self._info.get('url')))
116 return self._info.get('url')
117
118 def process_connect_info(self):
119 """
120 Processes the connection info provided by slack
121 """
122 if not self._info:
123 return
124
125 self.status = CONNECTED
126
127 # Map users
128 for u in self._info.get('users', []):
129 self.user_map[u['id']] = u
130 self.user_nick_to_id[u['name']] = u['id']
131
132 # Map IM
133 for i in self._info.get('ims', []):
134 self.channel_map[i['id']] = i
135
136 # Map Channels
137 for c in self._info.get('channels', []):
138 self.channel_map[c['id']] = c
139
140 async def process_message(self, msg):
141 # Built-in !whois action
142 if 'text' not in msg:
143 raise Exception(msg)
144 if msg['text'].startswith('!whois'):
145 nicknames = msg['text'].split(' ')[1:]
146 for n in nicknames:
147 await self.say(pformat(self.user_map[self.user_nick_to_id[n]]),
148 msg['channel'])
149 return
150
151 retval = {
152 'sender': msg['user'],
153 'channel': msg['channel'],
154 'message': msg['text']
155 }
156 return retval
157
158 def on_user_change(self, msg):
159 """
160 The user_change event is sent to all connections for a team when a team
161 member updates their profile or data. Clients can use this to update
162 their local cache of team members.
163
164 https://api.slack.com/events/user_change
165 """
166 user_info = msg['user']
167 old_nick = self.user_map[user_info['id']]['nick']
168
169 self.user_map[user_info['id']] = user_info
170
171 # Update the nick mapping if the user changed their nickname
172 if old_nick != user_info['nick']:
173 del self.user_nick_to_id[old_nick]
174 self.user_nick_to_id[user_info['nick']] = user_info['id']
175
176 def on_reconnect_url(self, msg):
177 """
178 The reconnect_url event is currently unsupported and experimental.
179
180 https://api.slack.com/events/reconnect_url
181 """
182 # self.reconnect_url = msg['url']
183 # self.log.debug('updated_reconnect_url: {}'.format(self.reconnect_url))
184
185 def on_presence_change(self, msg):
186 self.log.debug('updated_presence: {} ({}) was: {} is_now: {}'.format(
187 msg['user'], self.user_map[msg['user']]['name'],
188 self.user_map[msg['user']]['presence'],
189 msg['presence']
190 ))
191 self.user_map[msg['user']]['presence'] = msg['presence']
192
193 async def on_group_join(self, channel):
194 """
195 The group_joined event is sent to all connections for a user when that
196 user joins a private channel. In addition to this message, all existing
197 members of the private channel will receive a group_join message event.
198
199 https://api.slack.com/events/group_joined
200 """
201 # {
202 # 'channel': {
203 # 'members': ['U0286NL58', 'U1U05AF5J'],
204 # 'id': 'G1W837CGP',
205 # 'is_group': True,
206 # 'is_archived': False,
207 # 'latest': {
208 # 'user': 'U0286NL58',
209 # 'subtype': 'group_join',
210 # 'ts': '1469746594 .000002',
211 # 'type': 'message',
212 # 'text': '<@U0286NL58|jason> has joined the group'
213 # },
214 # 'is_mpim': False,
215 # 'unread_count': 0,
216 # 'purpose': {
217 # 'creator': '',
218 # 'value': '',
219 # 'last_set': 0
220 # },
221 # 'is_open': True,
222 # 'topic': {
223 # 'creator': '',
224 # 'value': '',
225 # 'last_set': 0
226 # },
227 # 'creator': 'U0286NL58',
228 # 'unread_count_display': 0,
229 # 'name': 'wm-test',
230 # 'last_read': '1469746594.000002',
231 # 'created': 1469746594
232 # },
233 # 'type': 'group_joined'
234 # }
235
236 def on_message_message_changed(self, msg):
237 """
238 A message_changed message is sent when a message in a channel is edited
239 using the chat.update method. The message property contains the updated
240 message object.
241
242 When clients receive this message type, they should look for an
243 existing message with the same message.ts in that channel. If they
244 find one the existing message should be replaced with the new one.
245
246 https://api.slack.com/events/message/message_changed
247 """
248 # {
249 # 'hidden': True,
250 # 'event_ts': '1469748743.218081',
251 # 'subtype': 'message_changed',
252 # 'message': {
253 # 'attachments': [{
254 # 'id': 1,
255 # 'image_width': 800,
256 # 'fallback': '800x450px image',
257 # 'from_url':
258 # 'http://media1.giphy.com/media/3o85fPE3Irg8Wazl9S/giphy.gif',
259 # 'image_bytes': 4847496,
260 # 'image_url':
261 # 'http://media1.giphy.com/media/3o85fPE3Irg8Wazl9S/giphy.gif',
262 # 'image_height': 450,
263 # 'is_animated': True
264 # }],
265 # 'type': 'message',
266 # 'ts': '1469748743.000019',
267 # 'text':
268 # '<http://media1.giphy.com/media/3o85fPE3Irg8Wazl9S/giphy.gif>',
269 # 'user': 'U1U05AF5J'
270 # },
271 # 'channel': 'G1W837CGP',
272 # 'ts': '1469748743.000020',
273 # 'type': 'message',
274 # 'previous_message': {
275 # 'type': 'message',
276 # 'ts': '1469748743.000019',
277 # 'text':
278 # '<http://media1.giphy.com/media/3o85fPE3Irg8Wazl9S/giphy.gif>',
279 # 'user': 'U1U05AF5J'
280 # }
281 # }