aboutsummaryrefslogtreecommitdiffstats
path: root/warmachine/connections/slack.py
diff options
context:
space:
mode:
authorjason2016-10-06 15:00:41 -0600
committerjason2016-10-06 15:00:41 -0600
commit04d0c39881dbdd6e9a53435a0ee9aa831716427c (patch)
tree89b132268d52164d9371973a207c42821bd1b137 /warmachine/connections/slack.py
parent17206d877ce948c826fa06d88cfd553dec775126 (diff)
downloadwarmachine-ng-04d0c39881dbdd6e9a53435a0ee9aa831716427c.tar.gz
warmachine-ng-04d0c39881dbdd6e9a53435a0ee9aa831716427c.zip
Add support for Slack's RTM ping
- warmachine freezes and seems to disconnect sometimes. This adds a ping to slack every few seconds to help mitigate that undetected disconnection
Diffstat (limited to '')
-rw-r--r--warmachine/connections/slack.py197
1 files changed, 144 insertions, 53 deletions
diff --git a/warmachine/connections/slack.py b/warmachine/connections/slack.py
index 41323a8..684aea3 100644
--- a/warmachine/connections/slack.py
+++ b/warmachine/connections/slack.py
@@ -2,13 +2,13 @@ import asyncio
2import json 2import json
3import logging 3import logging
4from pprint import pformat 4from pprint import pformat
5import time
5from urllib.parse import urlencode 6from urllib.parse import urlencode
6import urllib.request 7import urllib.request
7import sys
8 8
9import websockets 9import websockets
10 10
11from .base import Connection, INITALIZED, CONNECTED 11from .base import Connection, INITALIZED, CONNECTED, CONNECTING
12from ..utils.decorators import memoize 12from ..utils.decorators import memoize
13 13
14#: Define slack as a config section prefix 14#: Define slack as a config section prefix
@@ -28,7 +28,7 @@ class SlackWS(Connection):
28 self.reconnect_url = '' 28 self.reconnect_url = ''
29 29
30 self.channel_map = {} # channel and im info keyed by the slack id 30 self.channel_map = {} # channel and im info keyed by the slack id
31 self.channel_name_to_id = {} # slack channel/group name mapped to the id 31 self.channel_name_to_id = {} # slack channel/group name mapped to id
32 self.user_map = {} # user info keyed by their slack id 32 self.user_map = {} # user info keyed by their slack id
33 self.user_nick_to_id = {} # slack user id mapped to the (nick)name 33 self.user_nick_to_id = {} # slack user id mapped to the (nick)name
34 34
@@ -37,6 +37,11 @@ class SlackWS(Connection):
37 self.ws = None 37 self.ws = None
38 # used to give messages an id. slack requirement 38 # used to give messages an id. slack requirement
39 self._internal_msgid = 0 39 self._internal_msgid = 0
40 # used to give each ping a unique id
41 self._internal_pingid = 0
42
43 # track's lag
44 self.lag_in_ms = 0
40 45
41 self.status = INITALIZED 46 self.status = INITALIZED
42 47
@@ -52,12 +57,17 @@ class SlackWS(Connection):
52 except Exception: 57 except Exception:
53 self.log.exception('Error authenticating to slack') 58 self.log.exception('Error authenticating to slack')
54 return 59 return
60 self.STATUS = CONNECTING
55 self.log.info('Connecting to {}'.format(self.host)) 61 self.log.info('Connecting to {}'.format(self.host))
56 self.ws = await websockets.connect(self.host) 62 self.ws = await websockets.connect(self.host)
57 self.STATUS = CONNECTED
58 63
59 return True 64 return True
60 65
66 def on_hello(self, msg):
67 self.log.info('Connected to Slack')
68 self.STATUS = CONNECTED
69 self.start_ping()
70
61 async def read(self): 71 async def read(self):
62 if self.ws: 72 if self.ws:
63 try: 73 try:
@@ -68,14 +78,13 @@ class SlackWS(Connection):
68 self.error('Trying to reconnect...') 78 self.error('Trying to reconnect...')
69 await asyncio.sleep(300) 79 await asyncio.sleep(300)
70 return 80 return
81
71 # Slack is acknowledging a message was sent. Do nothing 82 # Slack is acknowledging a message was sent. Do nothing
72 if 'reply_to' in message: 83 if 'reply_to' in message and 'type' not in message:
73 # {'ok': True, 84 # {'ok': True,
74 # 'reply_to': 1, 85 # 'reply_to': 1,
75 # 'text': "['!whois', 'synic']", 86 # 'text': "['!whois', 'synic']",
76 # 'ts': '1469743355.000150'} 87 # 'ts': '1469743355.000150'}
77 self.log.debug('Ignoring reply_to message: {}'.format(
78 message))
79 return 88 return
80 89
81 # Sometimes there isn't a type in the message we receive 90 # Sometimes there isn't a type in the message we receive
@@ -112,7 +121,7 @@ class SlackWS(Connection):
112 """ 121 """
113 # If the destination is a user, figure out the DM channel id 122 # If the destination is a user, figure out the DM channel id
114 if destination and destination.startswith('#'): 123 if destination and destination.startswith('#'):
115 destination = self.channel_name_to_id[destination.replace('#','')] 124 destination = self.channel_name_to_id[destination.replace('#', '')]
116 else: 125 else:
117 _user = self.user_nick_to_id[destination] 126 _user = self.user_nick_to_id[destination]
118 127
@@ -121,7 +130,7 @@ class SlackWS(Connection):
121 destination)) 130 destination))
122 131
123 # slack doesn't allow bots to message other bots 132 # slack doesn't allow bots to message other bots
124 if '#' not in destination and (self.user_map[_user]['deleted'] or \ 133 if '#' not in destination and (self.user_map[_user]['deleted'] or
125 self.user_map[_user]['is_bot']): 134 self.user_map[_user]['is_bot']):
126 return 135 return
127 136
@@ -207,14 +216,6 @@ class SlackWS(Connection):
207 if 'text' not in msg: 216 if 'text' not in msg:
208 self.log.error('key "text" not found in message: {}'.format(msg)) 217 self.log.error('key "text" not found in message: {}'.format(msg))
209 218
210 # Built-in !whois command. Return information about a particular user.
211 if msg['text'].startswith('!whois'):
212 nicknames = msg['text'].split(' ')[1:]
213 for n in nicknames:
214 await self.say(pformat(self.user_map[self.user_nick_to_id[n]]),
215 msg['channel'])
216 return
217
218 # Map the slack ids to usernames and channels/groups names 219 # Map the slack ids to usernames and channels/groups names
219 user_nickname = self.user_map[msg['user']]['name'] 220 user_nickname = self.user_map[msg['user']]['name']
220 if msg['channel'].startswith('D'): 221 if msg['channel'].startswith('D'):
@@ -228,6 +229,19 @@ class SlackWS(Connection):
228 'channel': channel, 229 'channel': channel,
229 'message': msg['text'] 230 'message': msg['text']
230 } 231 }
232
233 _sender = retval['channel'] if retval['channel'] else retval['sender']
234 # Built-in !whois command. Return information about a particular user.
235 if retval['message'].startswith('!whois'):
236 nicknames = retval['message'].split(' ')[1:]
237 for n in nicknames:
238 await self.say(pformat(self.user_map[self.user_nick_to_id[n]]),
239 _sender)
240 return
241 elif msg['text'].startswith('!slack-lag'):
242 await self.say('{}ms'.format(self.lag_in_ms), _sender)
243 return
244
231 return retval 245 return retval
232 246
233 def on_user_change(self, msg): 247 def on_user_change(self, msg):
@@ -259,7 +273,6 @@ class SlackWS(Connection):
259 https://api.slack.com/events/reconnect_url 273 https://api.slack.com/events/reconnect_url
260 """ 274 """
261 # self.reconnect_url = msg['url'] 275 # self.reconnect_url = msg['url']
262 # self.log.debug('updated_reconnect_url: {}'.format(self.reconnect_url))
263 276
264 def on_presence_change(self, msg): 277 def on_presence_change(self, msg):
265 """ 278 """
@@ -310,14 +323,13 @@ class SlackWS(Connection):
310 return 323 return
311 324
312 url = 'https://slack.com/api/{}s.info?{}'.format( 325 url = 'https://slack.com/api/{}s.info?{}'.format(
313 key, urlencode( 326 key, urlencode({
314 {
315 'token': self.token, 327 'token': self.token,
316 'channel': channel, 328 'channel': channel,
317 })) 329 }))
318 330
319 self.log.debug('Gathering list of users for channel {} from: {}'.format( 331 self.log.debug('Gathering list of users for channel {} from: '
320 channel, url)) 332 '{}'.format(channel, url))
321 req = urllib.request.Request(url) 333 req = urllib.request.Request(url)
322 r = json.loads(urllib.request.urlopen(req).read().decode('utf-8')) 334 r = json.loads(urllib.request.urlopen(req).read().decode('utf-8'))
323 335
@@ -327,6 +339,30 @@ class SlackWS(Connection):
327 339
328 return users 340 return users
329 341
342 def start_ping(self, *args, **kwargs):
343 """
344 Starts the ping schedule to help keep the connection open.
345 """
346 asyncio.ensure_future(self.do_ping())
347
348 async def do_ping(self):
349 """
350 Send a ping to Slack
351 """
352 self._internal_pingid += 1
353 msg = json.dumps({
354 'id': self._internal_pingid,
355 'type': 'ping',
356 'time': time.time() * 1000,
357 })
358 await self._send(msg)
359 self._loop.call_later(4, self.start_ping)
360
361 def on_pong(self, msg):
362 now = time.time() * 1000
363
364 self.lag_in_ms = now - msg['time']
365
330 async def on_group_join(self, channel): 366 async def on_group_join(self, channel):
331 """ 367 """
332 The group_joined event is sent to all connections for a user when that 368 The group_joined event is sent to all connections for a user when that
@@ -417,34 +453,89 @@ class SlackWS(Connection):
417 # } 453 # }
418 # } 454 # }
419 455
420 def on_reaction_added(self, msg): 456 def on_reaction_added(self, msg):
421 """ 457 """
422 When someone adds a reaction to a message 458 When someone adds a reaction to a message
423 """ 459 """
424 460
425# Invited to a public channel 461 def on_user_typing(self, msg):
426# 2016-07-29 16:23:24,817 [DEBUG] SlackWS: on_channel_joined does not exist for message: {'type': 'channel_joined', 'chan 462 """
427# nel': {'members': ['U0286NL58', 'U1U05AF5J'], 'purpose': {'last_set': 0, 'creator': '', 'value': ''}, 'topic': {'last_s 463 When someone is typing to the bot
428# et': 0, 'creator': '', 'value': ''}, 'is_member': True, 'is_channel': True, 'creator': 'U0286NL58', 'is_archived': Fals 464 """
429# e, 'unread_count_display': 0, 'id': 'C1WJU3ZU0', 'name': 'wm-test2', 'is_general': False, 'created': 1469830985, 'unrea 465
430# d_count': 0, 'latest': {'text': '<@U0286NL58|jason> has joined the channel', 'type': 'message', 'user': 'U0286NL58', 's 466 def on_file_shared(self, msg):
431# ubtype': 'channel_join', 'ts': '1469830985.000002'}, 'last_read': '1469830985.000002'}} 467 """
432# 2016-07-29 16:23:24,878 [DEBUG] SlackWS: on_message_channel_join does not exist for message: {'channel': 'C1WJU3ZU0', ' 468 When someone shares a file
433# text': '<@U1U05AF5J|wm-standup-test> has joined the channel', 'type': 'message', 'inviter': 'U0286NL58', 'subtype': 'ch 469 """
434# annel_join', 'user_profile': {'real_name': '', 'name': 'wm-standup-test', 'image_72': 'https://avatars.slack-edge.com/2 470
435# 016-07-21/62015427159_1da65a3cf7a85e85c3cb_72.png', 'first_name': None, 'avatar_hash': '1da65a3cf7a8'}, 'ts': '14698310 471 def on_file_public(self, msg):
436# 04.000003', 'user': 'U1U05AF5J', 'team': 'T027XPE12'} 472 """
437 473 When someone shares a file publically
438# Someone else joins a public channel 474 """
439# 2016-07-29 16:26:19,966 [DEBUG] SlackWS: on_message_channel_join does not exist for message: {'type': 'message', 'invit 475
440# er': 'U0286NL58', 'ts': '1469831179.000004', 'team': 'T027XPE12', 'user': 'U0286167T', 'channel': 'C1WJU3ZU0', 'user_pr 476 def on_channel_joined(self, msg):
441# ofile': {'name': 'synic', 'image_72': 'https://avatars.slack-edge.com/2016-06-24/54136624065_49ec8bc368966c152817_72.jp 477 """
442# g', 'real_name': 'Adam Olsen', 'first_name': 'Adam', 'avatar_hash': '49ec8bc36896'}, 'subtype': 'channel_join', 'text': 478 When joining an public channel
443# '<@U0286167T|synic> has joined the channel'} 479 """
444 480 # {'type': 'channel_joined',
445# Invited to a private channel 481 # 'channel': {
446# 2016-07-29 16:27:29,376 [DEBUG] SlackWS: on_message_group_join does not exist for message: {'type': 'message', 'inviter 482 # 'members': ['U0286NL58', 'U1U05AF5J'],
447# ': 'U0286NL58', 'ts': '1469831249.000047', 'team': 'T027XPE12', 'user': 'U0286167T', 'channel': 'G1W837CGP', 'user_prof 483 # 'purpose': {'last_set': 0, 'creator': '', 'value': ''},
448# ile': {'name': 'synic', 'image_72': 'https://avatars.slack-edge.com/2016-06-24/54136624065_49ec8bc368966c152817_72.jpg' 484 # 'topic': {
449# , 'real_name': 'Adam Olsen', 'first_name': 'Adam', 'avatar_hash': '49ec8bc36896'}, 'subtype': 'group_join', 'text': '<@ 485 # 'last_set': 0,
450# U0286167T|synic> has joined the group'} 486 # 'creator': '',
487 # 'value': ''},
488 # 'is_member': True,
489 # 'is_channel': True,
490 # 'creator': 'U0286NL58',
491 # 'is_archived': False,
492 # 'unread_count_display': 0,
493 # 'id': 'C1WJU3ZU0',
494 # 'name': 'wm-test2',
495 # 'is_general': False,
496 # 'created': 1469830985,
497 # 'unread_count': 0,
498 # 'latest': {
499 # 'text': '<@U0286NL58|jason> has joined the channel',
500 # 'type': 'message', 'user': 'U0286NL58',
501 # 'subtype': 'channel_join',
502 # 'ts': '1469830985.000002'},
503 # 'last_read': '1469830985.000002'}}
504
505 def on_message_channel_join(self, msg):
506 """
507 Public channel join message
508 """
509 # {'channel': 'C1WJU3ZU0',
510 # 'text': '<@U1U05AF5J|wm-standup-test> has joined the channel',
511 # 'type': 'message',
512 # 'inviter': 'U0286NL58',
513 # 'subtype': 'channel_join',
514 # 'user_profile': {
515 # 'real_name': '',
516 # 'name': 'wm-standup-test',
517 # 'image_72': 'https://avatars.slack-edge.com/2016-07-....png',
518 # 'first_name': None,
519 # 'avatar_hash': '1da65a3cf7a8'},
520 # 'ts': '1469831004.000003',
521 # 'user': 'U1U05AF5J',
522 # 'team': 'T027XPE12'}
523
524 def on_message_group_join(self, msg):
525 """
526 Private channel join message
527 """
528 # {'type': 'message',
529 # 'inviter': 'U0286NL58',
530 # 'ts': '1469831249.000047',
531 # 'team': 'T027XPE12',
532 # 'user': 'U0286167T',
533 # 'channel': 'G1W837CGP',
534 # 'user_profile': {
535 # 'name': 'synic',
536 # 'image_72': 'https://avatars.s....jpg',
537 # 'real_name': 'Adam Olsen',
538 # 'first_name': 'Adam',
539 # 'avatar_hash': '49ec8bc36896'},
540 # 'subtype': 'group_join',
541 # 'text': '<@U0286167T|synic> has joined the group'}