diff options
| author | jason | 2016-07-30 17:34:05 -0600 |
|---|---|---|
| committer | jason | 2016-07-30 17:34:05 -0600 |
| commit | 68f1e25218c19a8addd897d5f3e8bc02ae3c2edd (patch) | |
| tree | fbc7847fcc69a68725d67d53d23e4657d5c660b8 | |
| parent | 6c36d2c2f8e8a58e9db8fe1d29a53a95c89c6879 (diff) | |
| download | warmachine-ng-68f1e25218c19a8addd897d5f3e8bc02ae3c2edd.tar.gz warmachine-ng-68f1e25218c19a8addd897d5f3e8bc02ae3c2edd.zip | |
standup with a timer
| -rwxr-xr-x | bin/dbolla | 14 | ||||
| -rw-r--r-- | warmachine/addons/standup.py | 60 | ||||
| -rw-r--r-- | warmachine/connections/base.py | 7 | ||||
| -rw-r--r-- | warmachine/connections/slack.py | 44 |
4 files changed, 115 insertions, 10 deletions
| @@ -1,6 +1,7 @@ | |||
| 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 | ||
| 4 | import functools | 5 | import functools |
| 5 | import logging.config | 6 | import logging.config |
| 6 | 7 | ||
| @@ -49,7 +50,8 @@ class Bot(object): | |||
| 49 | 50 | ||
| 50 | self.loaded_plugins = [] | 51 | self.loaded_plugins = [] |
| 51 | 52 | ||
| 52 | self.load_plugin('asdf') | 53 | self.load_plugin('warmachine.addons.giphy.GiphySearch') |
| 54 | self.load_plugin('warmachine.addons.standup.StandUpPlugin') | ||
| 53 | 55 | ||
| 54 | def start(self): | 56 | def start(self): |
| 55 | for connection in self.connections: | 57 | for connection in self.connections: |
| @@ -75,18 +77,22 @@ class Bot(object): | |||
| 75 | 77 | ||
| 76 | for p in self.loaded_plugins: | 78 | for p in self.loaded_plugins: |
| 77 | self.log.debug('Calling {}'.format(p.__class__.__name__)) | 79 | self.log.debug('Calling {}'.format(p.__class__.__name__)) |
| 78 | await p.recv_msg(connection, message) | 80 | try: |
| 81 | await p.recv_msg(connection, message) | ||
| 82 | except Exception as e: | ||
| 83 | self.log.exception(e) | ||
| 84 | continue | ||
| 79 | 85 | ||
| 80 | self.log.debug('MSG {}: {}'.format( | 86 | self.log.debug('MSG {}: {}'.format( |
| 81 | connection.__class__.__name__, message)) | 87 | connection.__class__.__name__, message)) |
| 82 | 88 | ||
| 83 | def load_plugin(self, path): | 89 | def load_plugin(self, class_path): |
| 84 | """ | 90 | """ |
| 85 | Loads plugins | 91 | Loads plugins |
| 86 | """ | 92 | """ |
| 87 | from importlib import import_module | 93 | from importlib import import_module |
| 88 | 94 | ||
| 89 | mod_path, cls_name = 'warmachine.addons.giphy.GiphySearch'.rsplit('.', 1) | 95 | mod_path, cls_name = class_path.rsplit('.', 1) |
| 90 | 96 | ||
| 91 | mod = import_module(mod_path) | 97 | mod = import_module(mod_path) |
| 92 | 98 | ||
diff --git a/warmachine/addons/standup.py b/warmachine/addons/standup.py index a21efdd..7d1ec99 100644 --- a/warmachine/addons/standup.py +++ b/warmachine/addons/standup.py | |||
| @@ -1,3 +1,7 @@ | |||
| 1 | import asyncio | ||
| 2 | from datetime import datetime, timedelta | ||
| 3 | import functools | ||
| 4 | |||
| 1 | from .base import WarMachinePlugin | 5 | from .base import WarMachinePlugin |
| 2 | 6 | ||
| 3 | 7 | ||
| @@ -9,6 +13,11 @@ class StandUpPlugin(WarMachinePlugin): | |||
| 9 | !standup-add <24 hr time to kick off> <SunMTWThFSat> [channel] | 13 | !standup-add <24 hr time to kick off> <SunMTWThFSat> [channel] |
| 10 | !standup-remove [channel] | 14 | !standup-remove [channel] |
| 11 | """ | 15 | """ |
| 16 | def __init__(self, *args, **kwargs): | ||
| 17 | super().__init__(*args, **kwargs) | ||
| 18 | |||
| 19 | self.standup_schedules = {} | ||
| 20 | |||
| 12 | async def recv_msg(self, connection, message): | 21 | async def recv_msg(self, connection, message): |
| 13 | if not message['message'].startswith('!standup'): | 22 | if not message['message'].startswith('!standup'): |
| 14 | return | 23 | return |
| @@ -18,11 +27,52 @@ class StandUpPlugin(WarMachinePlugin): | |||
| 18 | cmd = message['message'].split(' ')[0] | 27 | cmd = message['message'].split(' ')[0] |
| 19 | parts = message['message'].split(' ')[1:] | 28 | parts = message['message'].split(' ')[1:] |
| 20 | 29 | ||
| 30 | self._loop = asyncio.get_event_loop() | ||
| 31 | |||
| 21 | if cmd == '!standup-add': | 32 | if cmd == '!standup-add': |
| 22 | await connection.say('Scheduling standup for {} on {}'.format( | 33 | pretty_next_standup, next_standup_secs = \ |
| 23 | parts[1], parts[2])) | 34 | self.get_next_standup_secs(parts[0]) |
| 35 | |||
| 36 | f = self._loop.call_later( | ||
| 37 | next_standup_secs, functools.partial( | ||
| 38 | self.standup_schedule_func, connection, message['channel'])) | ||
| 39 | |||
| 40 | self.standup_schedules[message['channel']] = { | ||
| 41 | 'future': f, | ||
| 42 | } | ||
| 43 | await connection.say('Next standup in {}'.format( | ||
| 44 | pretty_next_standup), message['channel']) | ||
| 45 | await connection.say(str(self.standup_schedules), | ||
| 46 | message['channel']) | ||
| 47 | |||
| 48 | def standup_schedule_func(self, connection, channel): | ||
| 49 | asyncio.ensure_future(self.start_standup(connection, channel)) | ||
| 50 | |||
| 51 | async def start_standup(self, connection, channel): | ||
| 52 | await connection.say('@channel Time for standup', channel) | ||
| 53 | connection.get_users_by_channel(channel) | ||
| 54 | |||
| 55 | @classmethod | ||
| 56 | def get_next_standup_secs(cls, time24h): | ||
| 57 | """ | ||
| 58 | calculate the number of seconds until the next standup time | ||
| 59 | """ | ||
| 60 | now = datetime.now() | ||
| 61 | |||
| 62 | # if it's friday, wait 72 hours | ||
| 63 | if now.isoweekday() == 5: | ||
| 64 | hours = 72 | ||
| 65 | # if it's saturday, wait 48 | ||
| 66 | elif now.isoweekday() == 6: | ||
| 67 | hours = 48 | ||
| 68 | # if it's sunday-thur wait 24 | ||
| 69 | else: | ||
| 70 | hours = 24 | ||
| 24 | 71 | ||
| 25 | # await connection.say('{}, {}'.format(cmd, parts), message['channel']) | 72 | standup_hour, standup_minute = (int(s) for s in time24h.split(':')) |
| 26 | 73 | ||
| 27 | async def start_standup(self, connection): | 74 | future = now + timedelta(hours=hours) |
| 28 | pass | 75 | next_standup = datetime(future.year, future.month, future.day, |
| 76 | standup_hour, standup_minute) | ||
| 77 | standup_in = next_standup-now | ||
| 78 | return standup_in, standup_in.seconds | ||
diff --git a/warmachine/connections/base.py b/warmachine/connections/base.py index e68e9b9..4df5996 100644 --- a/warmachine/connections/base.py +++ b/warmachine/connections/base.py | |||
| @@ -26,6 +26,13 @@ class Connection(object): | |||
| 26 | raise NotImplementedError('{} must implement `read` method'.format( | 26 | raise NotImplementedError('{} must implement `read` method'.format( |
| 27 | self.__class__.__name__)) | 27 | self.__class__.__name__)) |
| 28 | 28 | ||
| 29 | def get_users_by_channel(self, channel): | ||
| 30 | """ | ||
| 31 | Return a list of users who are in the provided channel | ||
| 32 | """ | ||
| 33 | raise NotImplementedError('{} must implement `get_users_by_channel` ' | ||
| 34 | 'method'.format(self.__class__.__name__)) | ||
| 35 | |||
| 29 | def id(self): | 36 | def id(self): |
| 30 | """ | 37 | """ |
| 31 | Unique ID for this connection. Since there can be more than one | 38 | Unique ID for this connection. Since there can be more than one |
diff --git a/warmachine/connections/slack.py b/warmachine/connections/slack.py index b1a5390..90903e7 100644 --- a/warmachine/connections/slack.py +++ b/warmachine/connections/slack.py | |||
| @@ -3,6 +3,7 @@ import json | |||
| 3 | import logging | 3 | import logging |
| 4 | from pprint import pformat | 4 | from pprint import pformat |
| 5 | from urllib.parse import urlencode | 5 | from urllib.parse import urlencode |
| 6 | import urllib.request | ||
| 6 | 7 | ||
| 7 | import websockets | 8 | import websockets |
| 8 | 9 | ||
| @@ -95,7 +96,6 @@ class SlackWS(Connection): | |||
| 95 | Returns: | 96 | Returns: |
| 96 | str: websocket url to connect to | 97 | str: websocket url to connect to |
| 97 | """ | 98 | """ |
| 98 | import urllib.request | ||
| 99 | url = 'https://slack.com/api/rtm.start?{}'.format( | 99 | url = 'https://slack.com/api/rtm.start?{}'.format( |
| 100 | urlencode( | 100 | urlencode( |
| 101 | {'token': | 101 | {'token': |
| @@ -121,6 +121,8 @@ class SlackWS(Connection): | |||
| 121 | """ | 121 | """ |
| 122 | if not self._info: | 122 | if not self._info: |
| 123 | return | 123 | return |
| 124 | with open('slack_info.json', 'w') as f: | ||
| 125 | f.write(pformat(self._info)) | ||
| 124 | 126 | ||
| 125 | self.status = CONNECTED | 127 | self.status = CONNECTED |
| 126 | 128 | ||
| @@ -190,6 +192,18 @@ class SlackWS(Connection): | |||
| 190 | )) | 192 | )) |
| 191 | self.user_map[msg['user']]['presence'] = msg['presence'] | 193 | self.user_map[msg['user']]['presence'] = msg['presence'] |
| 192 | 194 | ||
| 195 | def get_users_by_channel(self, channel): | ||
| 196 | url = 'https://slack.com/api/groups.info?{}'.format(urlencode( | ||
| 197 | { | ||
| 198 | 'token': self.token, | ||
| 199 | 'channel': channel, | ||
| 200 | })) | ||
| 201 | self.log.debug(url) | ||
| 202 | req = urllib.request.Request(url) | ||
| 203 | r = urllib.request.urlopen(req).read().decode('utf-8') | ||
| 204 | |||
| 205 | self.log.debug(r) | ||
| 206 | |||
| 193 | async def on_group_join(self, channel): | 207 | async def on_group_join(self, channel): |
| 194 | """ | 208 | """ |
| 195 | The group_joined event is sent to all connections for a user when that | 209 | The group_joined event is sent to all connections for a user when that |
| @@ -279,3 +293,31 @@ class SlackWS(Connection): | |||
| 279 | # 'user': 'U1U05AF5J' | 293 | # 'user': 'U1U05AF5J' |
| 280 | # } | 294 | # } |
| 281 | # } | 295 | # } |
| 296 | |||
| 297 | |||
| 298 | # Invited to a public channel | ||
| 299 | # 2016-07-29 16:23:24,817 [DEBUG] SlackWS: on_channel_joined does not exist for message: {'type': 'channel_joined', 'chan | ||
| 300 | # nel': {'members': ['U0286NL58', 'U1U05AF5J'], 'purpose': {'last_set': 0, 'creator': '', 'value': ''}, 'topic': {'last_s | ||
| 301 | # et': 0, 'creator': '', 'value': ''}, 'is_member': True, 'is_channel': True, 'creator': 'U0286NL58', 'is_archived': Fals | ||
| 302 | # e, 'unread_count_display': 0, 'id': 'C1WJU3ZU0', 'name': 'wm-test2', 'is_general': False, 'created': 1469830985, 'unrea | ||
| 303 | # d_count': 0, 'latest': {'text': '<@U0286NL58|jason> has joined the channel', 'type': 'message', 'user': 'U0286NL58', 's | ||
| 304 | # ubtype': 'channel_join', 'ts': '1469830985.000002'}, 'last_read': '1469830985.000002'}} | ||
| 305 | # 2016-07-29 16:23:24,878 [DEBUG] SlackWS: on_message_channel_join does not exist for message: {'channel': 'C1WJU3ZU0', ' | ||
| 306 | # text': '<@U1U05AF5J|wm-standup-test> has joined the channel', 'type': 'message', 'inviter': 'U0286NL58', 'subtype': 'ch | ||
| 307 | # annel_join', 'user_profile': {'real_name': '', 'name': 'wm-standup-test', 'image_72': 'https://avatars.slack-edge.com/2 | ||
| 308 | # 016-07-21/62015427159_1da65a3cf7a85e85c3cb_72.png', 'first_name': None, 'avatar_hash': '1da65a3cf7a8'}, 'ts': '14698310 | ||
| 309 | # 04.000003', 'user': 'U1U05AF5J', 'team': 'T027XPE12'} | ||
| 310 | |||
| 311 | # Someone else joins a public channel | ||
| 312 | # 2016-07-29 16:26:19,966 [DEBUG] SlackWS: on_message_channel_join does not exist for message: {'type': 'message', 'invit | ||
| 313 | # er': 'U0286NL58', 'ts': '1469831179.000004', 'team': 'T027XPE12', 'user': 'U0286167T', 'channel': 'C1WJU3ZU0', 'user_pr | ||
| 314 | # ofile': {'name': 'synic', 'image_72': 'https://avatars.slack-edge.com/2016-06-24/54136624065_49ec8bc368966c152817_72.jp | ||
| 315 | # g', 'real_name': 'Adam Olsen', 'first_name': 'Adam', 'avatar_hash': '49ec8bc36896'}, 'subtype': 'channel_join', 'text': | ||
| 316 | # '<@U0286167T|synic> has joined the channel'} | ||
| 317 | |||
| 318 | # Invited to a private channel | ||
| 319 | # 2016-07-29 16:27:29,376 [DEBUG] SlackWS: on_message_group_join does not exist for message: {'type': 'message', 'inviter | ||
| 320 | # ': 'U0286NL58', 'ts': '1469831249.000047', 'team': 'T027XPE12', 'user': 'U0286167T', 'channel': 'G1W837CGP', 'user_prof | ||
| 321 | # ile': {'name': 'synic', 'image_72': 'https://avatars.slack-edge.com/2016-06-24/54136624065_49ec8bc368966c152817_72.jpg' | ||
| 322 | # , 'real_name': 'Adam Olsen', 'first_name': 'Adam', 'avatar_hash': '49ec8bc36896'}, 'subtype': 'group_join', 'text': '<@ | ||
| 323 | # U0286167T|synic> has joined the group'} | ||