diff options
| author | jason | 2016-10-06 15:00:41 -0600 |
|---|---|---|
| committer | jason | 2016-10-06 15:00:41 -0600 |
| commit | 04d0c39881dbdd6e9a53435a0ee9aa831716427c (patch) | |
| tree | 89b132268d52164d9371973a207c42821bd1b137 | |
| parent | 17206d877ce948c826fa06d88cfd553dec775126 (diff) | |
| download | warmachine-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
| -rwxr-xr-x | bin/dbolla | 3 | ||||
| -rw-r--r-- | warmachine/connections/base.py | 1 | ||||
| -rw-r--r-- | warmachine/connections/slack.py | 197 |
3 files changed, 146 insertions, 55 deletions
| @@ -1,14 +1,13 @@ | |||
| 1 | #!/usr/bin/env python3 | 1 | #!/usr/bin/env python3 |
| 2 | # -*- mode: python -*- | 2 | # -*- mode: python -*- |
| 3 | import asyncio | 3 | import asyncio |
| 4 | import datetime | ||
| 5 | import functools | 4 | import functools |
| 6 | import logging.config | 5 | import logging.config |
| 7 | import os | 6 | import os |
| 8 | 7 | ||
| 9 | 8 | ||
| 10 | from warmachine.config import Config | 9 | from warmachine.config import Config |
| 11 | from warmachine.connections.irc import AioIRC | 10 | # from warmachine.connections.irc import AioIRC |
| 12 | from warmachine.connections.slack import SlackWS | 11 | from warmachine.connections.slack import SlackWS |
| 13 | 12 | ||
| 14 | log_config = { | 13 | log_config = { |
diff --git a/warmachine/connections/base.py b/warmachine/connections/base.py index 56ce108..8b81c14 100644 --- a/warmachine/connections/base.py +++ b/warmachine/connections/base.py | |||
| @@ -1,5 +1,6 @@ | |||
| 1 | INITALIZED = 'Initalized' | 1 | INITALIZED = 'Initalized' |
| 2 | CONNECTED = 'Connected' | 2 | CONNECTED = 'Connected' |
| 3 | CONNECTING = 'Connecting' | ||
| 3 | 4 | ||
| 4 | 5 | ||
| 5 | class Connection(object): | 6 | class Connection(object): |
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 | |||
| 2 | import json | 2 | import json |
| 3 | import logging | 3 | import logging |
| 4 | from pprint import pformat | 4 | from pprint import pformat |
| 5 | import time | ||
| 5 | from urllib.parse import urlencode | 6 | from urllib.parse import urlencode |
| 6 | import urllib.request | 7 | import urllib.request |
| 7 | import sys | ||
| 8 | 8 | ||
| 9 | import websockets | 9 | import websockets |
| 10 | 10 | ||
| 11 | from .base import Connection, INITALIZED, CONNECTED | 11 | from .base import Connection, INITALIZED, CONNECTED, CONNECTING |
| 12 | from ..utils.decorators import memoize | 12 | from ..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'} | ||