diff options
| -rw-r--r-- | warmachine/addons/standup.py | 83 | ||||
| -rw-r--r-- | warmachine/connections/slack.py | 44 |
2 files changed, 105 insertions, 22 deletions
diff --git a/warmachine/addons/standup.py b/warmachine/addons/standup.py index 336177e..ebc4cac 100644 --- a/warmachine/addons/standup.py +++ b/warmachine/addons/standup.py | |||
| @@ -1,6 +1,7 @@ | |||
| 1 | import asyncio | 1 | import asyncio |
| 2 | from datetime import datetime, timedelta | 2 | from datetime import datetime, timedelta |
| 3 | import functools | 3 | import functools |
| 4 | from pprint import pformat | ||
| 4 | 5 | ||
| 5 | from .base import WarMachinePlugin | 6 | from .base import WarMachinePlugin |
| 6 | 7 | ||
| @@ -18,8 +19,42 @@ class StandUpPlugin(WarMachinePlugin): | |||
| 18 | 19 | ||
| 19 | self.standup_schedules = {} | 20 | self.standup_schedules = {} |
| 20 | 21 | ||
| 22 | # 'DM_CHANNEL': { | ||
| 23 | # 'user': 'UID', | ||
| 24 | # 'for_channel': 'CHID', | ||
| 25 | # } | ||
| 26 | self.users_awaiting_reply = {} | ||
| 27 | |||
| 21 | async def recv_msg(self, connection, message): | 28 | async def recv_msg(self, connection, message): |
| 22 | if not message['message'].startswith('!standup'): | 29 | if not message['message'].startswith('!standup'): |
| 30 | if message['channel'] in self.users_awaiting_reply: | ||
| 31 | self.log.debug("Probable reply recvd from {}: {}".format( | ||
| 32 | message['channel'], | ||
| 33 | message['message'] | ||
| 34 | )) | ||
| 35 | data = self.users_awaiting_reply[message['channel']] | ||
| 36 | for_channel = data['for_channel'] | ||
| 37 | |||
| 38 | try: | ||
| 39 | user_nick = connection.user_map[data['user']]['name'] | ||
| 40 | except KeyError: | ||
| 41 | user_nick = data['user'] | ||
| 42 | |||
| 43 | if 'pester_task' in data: | ||
| 44 | self.log.debug('Stopping pester for {}'.format(user_nick)) | ||
| 45 | data['pester_task'].cancel() | ||
| 46 | |||
| 47 | announce_message = '{}: {}'.format( | ||
| 48 | user_nick, | ||
| 49 | message['message'] | ||
| 50 | ) | ||
| 51 | |||
| 52 | await connection.say( | ||
| 53 | announce_message, | ||
| 54 | for_channel) | ||
| 55 | |||
| 56 | del data | ||
| 57 | del self.users_awaiting_reply[message['channel']] | ||
| 23 | return | 58 | return |
| 24 | 59 | ||
| 25 | self.log.debug('standup recv: {}'.format(message)) | 60 | self.log.debug('standup recv: {}'.format(message)) |
| @@ -36,7 +71,7 @@ class StandUpPlugin(WarMachinePlugin): | |||
| 36 | next_standup_secs = pretty_next_standup.seconds | 71 | next_standup_secs = pretty_next_standup.seconds |
| 37 | 72 | ||
| 38 | ### DEBUG | 73 | ### DEBUG |
| 39 | next_standup_secs = 5 | 74 | # next_standup_secs = 5 |
| 40 | ### | 75 | ### |
| 41 | f = self._loop.call_later( | 76 | f = self._loop.call_later( |
| 42 | next_standup_secs, functools.partial( | 77 | next_standup_secs, functools.partial( |
| @@ -54,6 +89,10 @@ class StandUpPlugin(WarMachinePlugin): | |||
| 54 | def standup_schedule_func(self, connection, channel): | 89 | def standup_schedule_func(self, connection, channel): |
| 55 | asyncio.ensure_future(self.start_standup(connection, channel)) | 90 | asyncio.ensure_future(self.start_standup(connection, channel)) |
| 56 | 91 | ||
| 92 | def pester_schedule_func(self, connection, user_id, channel, pester): | ||
| 93 | asyncio.ensure_future(self.standup_priv_msg( | ||
| 94 | connection, user_id, channel, pester)) | ||
| 95 | |||
| 57 | async def start_standup(self, connection, channel): | 96 | async def start_standup(self, connection, channel): |
| 58 | await connection.say('@channel Time for standup', channel) | 97 | await connection.say('@channel Time for standup', channel) |
| 59 | users = connection.get_users_by_channel(channel) | 98 | users = connection.get_users_by_channel(channel) |
| @@ -62,12 +101,44 @@ class StandUpPlugin(WarMachinePlugin): | |||
| 62 | if u == connection.my_id: | 101 | if u == connection.my_id: |
| 63 | continue | 102 | continue |
| 64 | 103 | ||
| 65 | self.log.debug('Messaging user: {} ({})'.format( | 104 | await self.standup_priv_msg(connection, u, channel) |
| 66 | connection.user_map[u], u)) | 105 | |
| 106 | async def standup_priv_msg(self, connection, user_id, channel, pester=600): | ||
| 107 | """ | ||
| 108 | Send a private message to ``user_id`` asking for their standup update. | ||
| 109 | |||
| 110 | Args: | ||
| 111 | connection (:class:`warmachine.base.Connection'): Connection object | ||
| 112 | to use. | ||
| 113 | user_id (str): User name or id to send the message to. | ||
| 114 | channel (str): The channel the standup is for | ||
| 115 | pester (int): Number of seconds to wait until asking the user again. | ||
| 116 | Use 0 to disable | ||
| 117 | """ | ||
| 118 | dm_id = connection.get_dm_id_by_user(user_id) | ||
| 119 | |||
| 120 | self.log.debug('Messaging user: {} ({})'.format( | ||
| 121 | connection.user_map[user_id], user_id)) | ||
| 122 | |||
| 123 | self.users_awaiting_reply[dm_id] = { | ||
| 124 | 'for_channel': channel, | ||
| 125 | 'user': user_id | ||
| 126 | } | ||
| 127 | |||
| 128 | self.log.debug('Adding to list of users waiting on a reply for: ' | ||
| 129 | '{}'.format(pformat(self.users_awaiting_reply[dm_id]))) | ||
| 130 | |||
| 131 | await connection.say('What did you do yesterday? What will you ' | ||
| 132 | 'do today? do you have any blockers? ' | ||
| 133 | '(standup for:{})'.format(channel), dm_id) | ||
| 134 | |||
| 135 | if pester > 0: | ||
| 136 | f = self._loop.call_later( | ||
| 137 | pester, functools.partial( | ||
| 138 | self.pester_schedule_func, connection, user_id, channel, | ||
| 139 | pester)) | ||
| 140 | self.users_awaiting_reply[dm_id]['pester_task'] = f | ||
| 67 | 141 | ||
| 68 | await connection.say('What did you do yesterday? What will you ' | ||
| 69 | 'do today? do you have any blockers? ' | ||
| 70 | '(standup for:{})'.format(channel), u) | ||
| 71 | 142 | ||
| 72 | @classmethod | 143 | @classmethod |
| 73 | def get_next_standup_secs(cls, time24h): | 144 | def get_next_standup_secs(cls, time24h): |
diff --git a/warmachine/connections/slack.py b/warmachine/connections/slack.py index 845b5c8..0ef39dc 100644 --- a/warmachine/connections/slack.py +++ b/warmachine/connections/slack.py | |||
| @@ -78,24 +78,9 @@ class SlackWS(Connection): | |||
| 78 | """ | 78 | """ |
| 79 | Say something in the provided channel or IM by id | 79 | Say something in the provided channel or IM by id |
| 80 | """ | 80 | """ |
| 81 | |||
| 82 | # If the destination is a user, figure out the DM channel id | 81 | # If the destination is a user, figure out the DM channel id |
| 83 | if destination_id.startswith('U'): | 82 | if destination_id.startswith('U'): |
| 84 | url = 'https://slack.com/api/im.open?{}'.format(urlencode({ | 83 | destination_id = self.get_dm_id_by_user(destination_id) |
| 85 | 'token': self.token, | ||
| 86 | 'user': destination_id, | ||
| 87 | })) | ||
| 88 | |||
| 89 | req = urllib.request.Request(url) | ||
| 90 | r = urllib.request.urlopen(req).read().decode('utf-8') | ||
| 91 | |||
| 92 | data = json.loads(r) | ||
| 93 | |||
| 94 | if not data['ok']: | ||
| 95 | raise Exception(data) | ||
| 96 | return | ||
| 97 | |||
| 98 | destination_id = data['channel']['id'] | ||
| 99 | 84 | ||
| 100 | await self._send(json.dumps({ | 85 | await self._send(json.dumps({ |
| 101 | 'id': 1, # TODO: this should be a get_msgid call or something | 86 | 'id': 1, # TODO: this should be a get_msgid call or something |
| @@ -224,6 +209,33 @@ class SlackWS(Connection): | |||
| 224 | )) | 209 | )) |
| 225 | self.user_map[msg['user']]['presence'] = msg['presence'] | 210 | self.user_map[msg['user']]['presence'] = msg['presence'] |
| 226 | 211 | ||
| 212 | def get_dm_id_by_user(self, user_id): | ||
| 213 | """ | ||
| 214 | Return the channel id for a direct message to a specific user. | ||
| 215 | |||
| 216 | Args: | ||
| 217 | user_id (str): slack user id | ||
| 218 | |||
| 219 | Return: | ||
| 220 | str: DM channel id for the provided user. None on error | ||
| 221 | """ | ||
| 222 | url = 'https://slack.com/api/im.open?{}'.format(urlencode({ | ||
| 223 | 'token': self.token, | ||
| 224 | 'user': user_id, | ||
| 225 | })) | ||
| 226 | |||
| 227 | req = urllib.request.Request(url) | ||
| 228 | r = urllib.request.urlopen(req).read().decode('utf-8') | ||
| 229 | |||
| 230 | data = json.loads(r) | ||
| 231 | |||
| 232 | if not data['ok']: | ||
| 233 | raise Exception(data) | ||
| 234 | return | ||
| 235 | |||
| 236 | return data['channel']['id'] | ||
| 237 | |||
| 238 | |||
| 227 | def get_users_by_channel(self, channel): | 239 | def get_users_by_channel(self, channel): |
| 228 | url = 'https://slack.com/api/groups.info?{}'.format(urlencode( | 240 | url = 'https://slack.com/api/groups.info?{}'.format(urlencode( |
| 229 | { | 241 | { |