diff options
| author | jason | 2016-08-17 15:52:43 -0600 |
|---|---|---|
| committer | jason | 2016-08-17 15:52:43 -0600 |
| commit | b5322db38973661b08be27185420163291a1766a (patch) | |
| tree | 91198f948756ad98c0cb09f1cb959d9dbfc2bf33 | |
| parent | 1c123d6f199e1d0b88019d56a4bb1da082bdc79f (diff) | |
| download | warmachine-ng-b5322db38973661b08be27185420163291a1766a.tar.gz warmachine-ng-b5322db38973661b08be27185420163291a1766a.zip | |
Commit updates that i haven't commited yet
| -rw-r--r-- | README.org | 12 | ||||
| -rwxr-xr-x | bin/dbolla | 2 | ||||
| -rw-r--r-- | warmachine/addons/standup.py | 190 | ||||
| -rw-r--r-- | warmachine/connections/base.py | 14 | ||||
| -rw-r--r-- | warmachine/connections/irc.py | 5 | ||||
| -rw-r--r-- | warmachine/connections/slack.py | 81 |
6 files changed, 183 insertions, 121 deletions
| @@ -24,7 +24,7 @@ Simply run the command: | |||
| 24 | * Writing a Connection | 24 | * Writing a Connection |
| 25 | To write a new connection protocol you must inherit from | 25 | To write a new connection protocol you must inherit from |
| 26 | ~warmachine.connections.base.Connection~. This class defines an interface you | 26 | ~warmachine.connections.base.Connection~. This class defines an interface you |
| 27 | must implement to support the plugins plugins. | 27 | must implement to support the plugins. |
| 28 | ** ~__config_prefix__~ | 28 | ** ~__config_prefix__~ |
| 29 | This global is used to decide which connection to use when it is found in the | 29 | This global is used to decide which connection to use when it is found in the |
| 30 | config file. E.g. IRC uses ~'irc'~ and Slack uses ~'slack'~. It should be | 30 | config file. E.g. IRC uses ~'irc'~ and Slack uses ~'slack'~. It should be |
| @@ -47,9 +47,12 @@ return value should be formatted in the following format: | |||
| 47 | 'message': 'The message that was received', | 47 | 'message': 'The message that was received', |
| 48 | } | 48 | } |
| 49 | #+END_SRC | 49 | #+END_SRC |
| 50 | ** ~say(message, destination)~ | ||
| 51 | This method is used by plugins to send a message to a channel or user. | ||
| 50 | ** ~id~ | 52 | ** ~id~ |
| 51 | This should return a unique id used to identify this particular connection. As | 53 | This should return a unique id used to identify this particular connection. This |
| 52 | an example, the IRC connection uses something like this: | 54 | is used by plugins when saving state. As an example, the IRC connection uses |
| 55 | something like this: | ||
| 53 | 56 | ||
| 54 | #+BEGIN_SRC python | 57 | #+BEGIN_SRC python |
| 55 | @property | 58 | @property |
| @@ -60,3 +63,6 @@ def id(self): | |||
| 60 | value = '{}-{}'.format(self.host, self.nick) | 63 | value = '{}-{}'.format(self.host, self.nick) |
| 61 | return md5(value.encode()).hexdigest() | 64 | return md5(value.encode()).hexdigest() |
| 62 | #+END_SRC | 65 | #+END_SRC |
| 66 | ** ~get_users_by_channel(channel)~ | ||
| 67 | This method should return a list of all users (including the bot) for the | ||
| 68 | connection. | ||
| @@ -106,7 +106,7 @@ class Bot(object): | |||
| 106 | if hasattr(mod, cls_name): | 106 | if hasattr(mod, cls_name): |
| 107 | cls = getattr(mod, cls_name)() | 107 | cls = getattr(mod, cls_name)() |
| 108 | 108 | ||
| 109 | self.loaded_plugins.append(cls) | 109 | self.loaded_plugins.append(cls) |
| 110 | 110 | ||
| 111 | def reload_plugin(self, path): | 111 | def reload_plugin(self, path): |
| 112 | """ | 112 | """ |
diff --git a/warmachine/addons/standup.py b/warmachine/addons/standup.py index dfcb6ab..0d46408 100644 --- a/warmachine/addons/standup.py +++ b/warmachine/addons/standup.py | |||
| @@ -32,9 +32,10 @@ class StandUpPlugin(WarMachinePlugin): | |||
| 32 | 32 | ||
| 33 | # 'DM_CHANNEL': { | 33 | # 'DM_CHANNEL': { |
| 34 | # 'user': 'UID', | 34 | # 'user': 'UID', |
| 35 | # 'for_channel': 'CHID', | 35 | # 'for_channels': ['CHID',], |
| 36 | # } | 36 | # } |
| 37 | self.users_awaiting_reply = {} | 37 | self.users_awaiting_reply = {} |
| 38 | self.log.info('Loaded standup plugin') | ||
| 38 | 39 | ||
| 39 | def on_connect(self, connection): | 40 | def on_connect(self, connection): |
| 40 | self.load_schedule(connection) | 41 | self.load_schedule(connection) |
| @@ -48,40 +49,52 @@ class StandUpPlugin(WarMachinePlugin): | |||
| 48 | connection (Connection): the warmachine connection object | 49 | connection (Connection): the warmachine connection object |
| 49 | message (dict): the warmachine formatted message | 50 | message (dict): the warmachine formatted message |
| 50 | """ | 51 | """ |
| 51 | if not message['message'].startswith('!standup'): | 52 | if not message['message'].startswith('!standup') \ |
| 52 | if message['channel'] in self.users_awaiting_reply: | 53 | and not message['channel'] \ |
| 53 | self.log.debug("Probable reply recvd from {}: {}".format( | 54 | and message['sender'] in self.users_awaiting_reply: |
| 54 | message['channel'], | 55 | self.log.debug("Probable standup reply recvd from {}: {}".format( |
| 55 | message['message'] | 56 | message['sender'], message['message'])) |
| 56 | )) | 57 | |
| 57 | data = self.users_awaiting_reply[message['channel']] | 58 | user_nick = message['sender'] |
| 58 | for_channel = data['for_channel'] | 59 | |
| 59 | 60 | data = self.users_awaiting_reply[user_nick] | |
| 60 | try: | 61 | |
| 61 | user_nick = connection.user_map[data['user']]['name'] | 62 | for_channels = data['for_channels'] |
| 62 | except KeyError: | 63 | |
| 63 | user_nick = data['user'] | 64 | if 'pester_task' in data: |
| 64 | 65 | self.log.debug('Stopping pester for {}'.format(user_nick)) | |
| 65 | if 'pester_task' in data: | 66 | data['pester_task'].cancel() |
| 66 | self.log.debug('Stopping pester for {}'.format(user_nick)) | 67 | data['pester_task'] = None |
| 67 | data['pester_task'].cancel() | 68 | |
| 68 | 69 | announce_message = '{}: {}'.format( | |
| 69 | announce_message = '{}: {}'.format( | 70 | user_nick, |
| 70 | user_nick, | 71 | message['message'] |
| 71 | message['message'] | 72 | ) |
| 72 | ) | 73 | |
| 73 | 74 | self.users_awaiting_reply[user_nick]['standup_msg'] = \ | |
| 74 | await connection.say( | 75 | message['message'] |
| 75 | announce_message, | 76 | |
| 76 | for_channel) | 77 | f = self._loop.call_later( |
| 77 | 78 | 16*(60*60), # 16 hours | |
| 78 | del data | 79 | self.clear_old_standup_message_schedule_func, user_nick |
| 79 | del self.users_awaiting_reply[message['channel']] | 80 | ) |
| 81 | |||
| 82 | self.users_awaiting_reply[user_nick]['clear_standup_msg_f'] = f | ||
| 83 | |||
| 84 | for i in range(0, len(for_channels)): | ||
| 85 | c = self.users_awaiting_reply[user_nick]['for_channels'].pop() | ||
| 86 | await connection.say(announce_message, c) | ||
| 87 | |||
| 88 | del data | ||
| 89 | # del self.users_awaiting_reply[user_nick] | ||
| 80 | return | 90 | return |
| 81 | 91 | ||
| 92 | # Otherwise parse for the commands: | ||
| 93 | |||
| 82 | cmd = message['message'].split(' ')[0] | 94 | cmd = message['message'].split(' ')[0] |
| 83 | parts = message['message'].split(' ')[1:] | 95 | parts = message['message'].split(' ')[1:] |
| 84 | channel = message['channel'] | 96 | channel = message['channel'] |
| 97 | user_nick = message['sender'] | ||
| 85 | 98 | ||
| 86 | # ====================================================================== | 99 | # ====================================================================== |
| 87 | # !standup-add <24h time> | 100 | # !standup-add <24h time> |
| @@ -89,7 +102,7 @@ class StandUpPlugin(WarMachinePlugin): | |||
| 89 | # Add (or update if one exists) a schedule for standup at the given 24h | 102 | # Add (or update if one exists) a schedule for standup at the given 24h |
| 90 | # time M-F | 103 | # time M-F |
| 91 | # ====================================================================== | 104 | # ====================================================================== |
| 92 | if cmd == '!standup-add' and not channel.startswith('D'): | 105 | if cmd == '!standup-add' and channel: |
| 93 | # If there is already a schedule, kill the task for the old one. | 106 | # If there is already a schedule, kill the task for the old one. |
| 94 | if channel in self.standup_schedules: | 107 | if channel in self.standup_schedules: |
| 95 | self.standup_schedules[channel]['future'].cancel() | 108 | self.standup_schedules[channel]['future'].cancel() |
| @@ -100,12 +113,13 @@ class StandUpPlugin(WarMachinePlugin): | |||
| 100 | 113 | ||
| 101 | self.schedule_standup(connection, channel, parts[0]) | 114 | self.schedule_standup(connection, channel, parts[0]) |
| 102 | self.save_schedule(connection) | 115 | self.save_schedule(connection) |
| 116 | |||
| 103 | # ====================================================================== | 117 | # ====================================================================== |
| 104 | # !standup-remove | 118 | # !standup-remove |
| 105 | # | 119 | # |
| 106 | # Remove an existing schedule from the channel | 120 | # Remove an existing schedule from the channel |
| 107 | # ====================================================================== | 121 | # ====================================================================== |
| 108 | elif cmd == '!standup-remove' and not channel.startswith('D'): | 122 | elif cmd == '!standup-remove' and channel: |
| 109 | if channel in self.standup_schedules: | 123 | if channel in self.standup_schedules: |
| 110 | self.standup_schedules[channel]['future'].cancel() | 124 | self.standup_schedules[channel]['future'].cancel() |
| 111 | del self.standup_schedules[channel] | 125 | del self.standup_schedules[channel] |
| @@ -120,15 +134,17 @@ class StandUpPlugin(WarMachinePlugin): | |||
| 120 | # questions. | 134 | # questions. |
| 121 | # If no users are provided, display the users currently being ignored | 135 | # If no users are provided, display the users currently being ignored |
| 122 | # ====================================================================== | 136 | # ====================================================================== |
| 123 | elif cmd == '!standup-ignore' and not channel.startswith('D') \ | 137 | elif cmd == '!standup-ignore' and channel \ |
| 124 | and channel in self.standup_schedules: | 138 | and channel in self.standup_schedules: |
| 125 | if parts: | 139 | if parts: |
| 126 | users = ''.join(parts).split(',') | 140 | users_to_ignore = ''.join(parts).split(',') |
| 127 | for u in users: | 141 | for u in users_to_ignore: |
| 128 | if u not in self.standup_schedules[channel]['ignoring']: | 142 | if u not in self.standup_schedules[channel]['ignoring']: |
| 129 | self.log.info('Ignoring {} in channel {}'.format( | 143 | self.log.info('Ignoring {} in channel {}'.format( |
| 130 | u, channel)) | 144 | u, channel)) |
| 131 | self.standup_schedules[channel]['ignoring'].append(u) | 145 | self.standup_schedules[channel]['ignoring'].append(u) |
| 146 | |||
| 147 | # Save the new users to ignore for this channel | ||
| 132 | self.save_schedule(connection) | 148 | self.save_schedule(connection) |
| 133 | 149 | ||
| 134 | ignoring = ', '.join( | 150 | ignoring = ', '.join( |
| @@ -138,22 +154,26 @@ class StandUpPlugin(WarMachinePlugin): | |||
| 138 | 154 | ||
| 139 | await connection.say('Currently ignoring {}'.format(ignoring), | 155 | await connection.say('Currently ignoring {}'.format(ignoring), |
| 140 | channel) | 156 | channel) |
| 157 | elif cmd == '!standup-unignore' and channel \ | ||
| 158 | and channel in self.standup_schedules: | ||
| 159 | if not parts: | ||
| 160 | return | ||
| 141 | 161 | ||
| 142 | # ====================================================================== | 162 | # ====================================================================== |
| 143 | # !standup-schedules | 163 | # !standup-schedules |
| 144 | # | 164 | # |
| 145 | # Report the current standup schedule dict to the requesting user | 165 | # Report the current standup schedule dict to the requesting user |
| 146 | # ====================================================================== | 166 | # ====================================================================== |
| 147 | elif channel.startswith('D') and cmd == '!standup-schedules': | 167 | elif not channel and cmd == '!standup-schedules': |
| 148 | self.log.info('Reporting standup schedules to DM {}'.format( | 168 | self.log.info('Reporting standup schedules to {}'.format( |
| 149 | channel)) | 169 | user_nick)) |
| 150 | await connection.say('Standup Schedules', channel) | 170 | await connection.say('Standup Schedules', user_nick) |
| 151 | await connection.say('-----------------', channel) | 171 | await connection.say('-----------------', user_nick) |
| 152 | await connection.say( | 172 | await connection.say( |
| 153 | 'Current Loop Time: {}'.format(self._loop.time()), channel) | 173 | 'Current Loop Time: {}'.format(self._loop.time()), user_nick) |
| 154 | await connection.say( | 174 | await connection.say( |
| 155 | 'Current Time: {}'.format(datetime.now()), channel) | 175 | 'Current Time: {}'.format(datetime.now()), user_nick) |
| 156 | await connection.say(pformat(self.standup_schedules), channel) | 176 | await connection.say(pformat(self.standup_schedules), user_nick) |
| 157 | 177 | ||
| 158 | # ====================================================================== | 178 | # ====================================================================== |
| 159 | # !standup-waiting_replies | 179 | # !standup-waiting_replies |
| @@ -161,14 +181,13 @@ class StandUpPlugin(WarMachinePlugin): | |||
| 161 | # Report the data struct of users we are waiting on a reply from to the | 181 | # Report the data struct of users we are waiting on a reply from to the |
| 162 | # requesting user. | 182 | # requesting user. |
| 163 | # ====================================================================== | 183 | # ====================================================================== |
| 164 | elif channel.startswith('D') and \ | 184 | elif not channel and cmd == '!standup-waiting_replies': |
| 165 | cmd == '!standup-waiting_replies': | 185 | self.log.info('Reporting who we are waiting on replies for to ' |
| 166 | self.log.info('Reporting who we are waiting on replies for to DM ' | 186 | ' {}'.format(user_nick)) |
| 167 | ' {}'.format(channel)) | 187 | await connection.say('Waiting for Replies From', user_nick) |
| 168 | await connection.say('Waiting for Replies From', channel) | 188 | await connection.say('------------------------', user_nick) |
| 169 | await connection.say('------------------------', channel) | ||
| 170 | await connection.say( | 189 | await connection.say( |
| 171 | pformat(self.users_awaiting_reply), channel) | 190 | pformat(self.users_awaiting_reply), user_nick) |
| 172 | 191 | ||
| 173 | def schedule_standup(self, connection, channel, time24h): | 192 | def schedule_standup(self, connection, channel, time24h): |
| 174 | """ | 193 | """ |
| @@ -191,9 +210,7 @@ class StandUpPlugin(WarMachinePlugin): | |||
| 191 | } | 210 | } |
| 192 | 211 | ||
| 193 | self.log.info('New schedule added to channel {} for {}'.format( | 212 | self.log.info('New schedule added to channel {} for {}'.format( |
| 194 | connection.channel_map[channel]['name'], | 213 | channel, time24h)) |
| 195 | time24h | ||
| 196 | )) | ||
| 197 | 214 | ||
| 198 | def standup_schedule_func(self, connection, channel): | 215 | def standup_schedule_func(self, connection, channel): |
| 199 | """ | 216 | """ |
| @@ -201,75 +218,88 @@ class StandUpPlugin(WarMachinePlugin): | |||
| 201 | 218 | ||
| 202 | See :meth:`start_standup` | 219 | See :meth:`start_standup` |
| 203 | """ | 220 | """ |
| 204 | self.log.info('Executing standup for channel {}'.format( | 221 | self.log.info('Executing standup for channel {}'.format(channel)) |
| 205 | connection.channel_map[channel]['name'] | ||
| 206 | )) | ||
| 207 | asyncio.ensure_future(self.start_standup(connection, channel)) | 222 | asyncio.ensure_future(self.start_standup(connection, channel)) |
| 208 | 223 | ||
| 209 | def pester_schedule_func(self, connection, user_id, channel, pester): | 224 | def pester_schedule_func(self, connection, user, channel, pester): |
| 210 | """ | 225 | """ |
| 211 | Non-async function used to schedule pesters for a user. | 226 | Non-async function used to schedule pesters for a user. |
| 212 | 227 | ||
| 213 | See :meth:`standup_priv_msg` | 228 | See :meth:`standup_priv_msg` |
| 214 | """ | 229 | """ |
| 215 | self.log.info('Pestering user {} to give a standup for channel ' | 230 | self.log.info('Pestering user {} to give a standup for channel ' |
| 216 | '{} (interval: {}s)'.format( | 231 | '{} (interval: {}s)'.format(user, channel, pester)) |
| 217 | connection.user_map[user_id]['name'], | ||
| 218 | connection.channel_map[channel]['name'], | ||
| 219 | pester)) | ||
| 220 | asyncio.ensure_future(self.standup_priv_msg( | 232 | asyncio.ensure_future(self.standup_priv_msg( |
| 221 | connection, user_id, channel, pester)) | 233 | connection, user, channel, pester)) |
| 234 | |||
| 235 | def clear_old_standup_message_schedule_func(self, user): | ||
| 236 | """ | ||
| 237 | This function is scheduled to remove old standup messages so that the | ||
| 238 | user is asked about standup the following day. | ||
| 239 | """ | ||
| 240 | del self.users_awaiting_reply[user]['clear_standup_msg_f'] | ||
| 241 | del self.users_awaiting_reply[user]['standup_msg'] | ||
| 222 | 242 | ||
| 223 | async def start_standup(self, connection, channel): | 243 | async def start_standup(self, connection, channel): |
| 224 | """ | 244 | """ |
| 225 | Notify the channel that the standup is about to begin, then loop through | 245 | Notify the channel that the standup is about to begin, then loop through |
| 226 | all the users in the channel asking them report their standup. | 246 | all the users in the channel asking them report their standup. |
| 227 | """ | 247 | """ |
| 228 | await connection.say('@channel Time for standup', channel) | ||
| 229 | users = connection.get_users_by_channel(channel) | 248 | users = connection.get_users_by_channel(channel) |
| 249 | if not users: | ||
| 250 | self.log.error('Unable to get_users_by_channel for channel ' | ||
| 251 | '{}. Skipping standup.'.format(channel)) | ||
| 252 | return | ||
| 253 | await connection.say('@channel Time for standup', channel) | ||
| 230 | 254 | ||
| 231 | for u in users: | 255 | for u in users: |
| 232 | if u == connection.my_id or \ | 256 | if u == connection.nick or \ |
| 233 | u in self.standup_schedules[channel]['ignoring']: | 257 | u in self.standup_schedules[channel]['ignoring']: |
| 234 | continue | 258 | continue |
| 235 | 259 | ||
| 236 | await self.standup_priv_msg(connection, u, channel) | 260 | if u in self.users_awaiting_reply and \ |
| 261 | 'standup_msg' in self.users_awaiting_reply[u]: | ||
| 262 | await connection.say('{}: {}'.format( | ||
| 263 | u, self.users_awaiting_reply[u]['standup_msg']), channel) | ||
| 264 | else: | ||
| 265 | await self.standup_priv_msg(connection, u, channel) | ||
| 237 | 266 | ||
| 238 | async def standup_priv_msg(self, connection, user_id, channel, pester=600): | 267 | async def standup_priv_msg(self, connection, user, channel, pester=600): |
| 239 | """ | 268 | """ |
| 240 | Send a private message to ``user_id`` asking for their standup update. | 269 | Send a private message to ``user`` asking for their standup update. |
| 241 | 270 | ||
| 242 | Args: | 271 | Args: |
| 243 | connection (:class:`warmachine.base.Connection'): Connection object | 272 | connection (:class:`warmachine.base.Connection'): Connection object |
| 244 | to use. | 273 | to use. |
| 245 | user_id (str): User name or id to send the message to. | 274 | user (str): User to send the message to. |
| 246 | channel (str): The channel the standup is for | 275 | channel (str): The channel the standup is for |
| 247 | pester (int): Number of seconds to wait until asking the user again. | 276 | pester (int): Number of seconds to wait until asking the user again. |
| 248 | Use 0 to disable | 277 | Use 0 to disable |
| 249 | """ | 278 | """ |
| 250 | dm_id = connection.get_dm_id_by_user(user_id) | 279 | self.log.debug('Messaging user: {}'.format(user)) |
| 251 | 280 | ||
| 252 | self.log.debug('Messaging user: {} ({})'.format( | 281 | if user in self.users_awaiting_reply: |
| 253 | connection.user_map[user_id], user_id)) | 282 | self.users_awaiting_reply[user]['for_channels'].append(channel) |
| 254 | 283 | ||
| 255 | self.users_awaiting_reply[dm_id] = { | 284 | self.log.debug('Adding to list of users waiting on a reply for: ' |
| 256 | 'for_channel': channel, | 285 | '{}'.format( |
| 257 | 'user': user_id | 286 | self.users_awaiting_reply[user])) |
| 258 | } | 287 | else: |
| 288 | self.users_awaiting_reply[user] = { | ||
| 289 | 'for_channels': [channel, ], | ||
| 290 | } | ||
| 259 | 291 | ||
| 260 | self.log.debug('Adding to list of users waiting on a reply for: ' | ||
| 261 | '{}'.format(pformat(self.users_awaiting_reply[dm_id]))) | ||
| 262 | 292 | ||
| 263 | await connection.say('What did you do yesterday? What will you ' | 293 | await connection.say('What did you do yesterday? What will you ' |
| 264 | 'do today? do you have any blockers? ' | 294 | 'do today? do you have any blockers? ' |
| 265 | '(standup for:{})'.format(channel), dm_id) | 295 | '(standup for:{})'.format(channel), user) |
| 266 | 296 | ||
| 267 | if pester > 0: | 297 | if pester > 0: |
| 268 | f = self._loop.call_later( | 298 | f = self._loop.call_later( |
| 269 | pester, functools.partial( | 299 | pester, functools.partial( |
| 270 | self.pester_schedule_func, connection, user_id, channel, | 300 | self.pester_schedule_func, connection, user, channel, |
| 271 | pester)) | 301 | pester)) |
| 272 | self.users_awaiting_reply[dm_id]['pester_task'] = f | 302 | self.users_awaiting_reply[user]['pester_task'] = f |
| 273 | 303 | ||
| 274 | 304 | ||
| 275 | @classmethod | 305 | @classmethod |
diff --git a/warmachine/connections/base.py b/warmachine/connections/base.py index 4df5996..e787e35 100644 --- a/warmachine/connections/base.py +++ b/warmachine/connections/base.py | |||
| @@ -26,13 +26,6 @@ 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 | |||
| 36 | def id(self): | 29 | def id(self): |
| 37 | """ | 30 | """ |
| 38 | Unique ID for this connection. Since there can be more than one | 31 | Unique ID for this connection. Since there can be more than one |
| @@ -45,3 +38,10 @@ class Connection(object): | |||
| 45 | """ | 38 | """ |
| 46 | raise NotImplementedError('{} must implement `id` method'.format( | 39 | raise NotImplementedError('{} must implement `id` method'.format( |
| 47 | self.__class__.__name__)) | 40 | self.__class__.__name__)) |
| 41 | |||
| 42 | def say(self, message, destination): | ||
| 43 | """ | ||
| 44 | Async method that a plugin can use to send a message to a channel or user. | ||
| 45 | """ | ||
| 46 | raise NotImplementedError('{} must implement `say` method'.format( | ||
| 47 | self.__class__.__name__)) | ||
diff --git a/warmachine/connections/irc.py b/warmachine/connections/irc.py index f7f682c..86068d7 100644 --- a/warmachine/connections/irc.py +++ b/warmachine/connections/irc.py | |||
| @@ -36,12 +36,15 @@ class AioIRC(Connection): | |||
| 36 | self.writer.write('USER {} 8 * :War Machine\r\n'.format( | 36 | self.writer.write('USER {} 8 * :War Machine\r\n'.format( |
| 37 | self.user).encode()) | 37 | self.user).encode()) |
| 38 | 38 | ||
| 39 | self.status = CONNECTED | ||
| 40 | |||
| 39 | return True | 41 | return True |
| 40 | 42 | ||
| 41 | @asyncio.coroutine | 43 | @asyncio.coroutine |
| 42 | def read(self): | 44 | def read(self): |
| 43 | if self.reader.at_eof(): | 45 | if self.reader.at_eof(): |
| 44 | raise Exception('eof') | 46 | raise Exception('eof') |
| 47 | |||
| 45 | if self.reader: | 48 | if self.reader: |
| 46 | message = yield from self.reader.readline() | 49 | message = yield from self.reader.readline() |
| 47 | 50 | ||
| @@ -61,4 +64,4 @@ class AioIRC(Connection): | |||
| 61 | @property | 64 | @property |
| 62 | @memoize | 65 | @memoize |
| 63 | def id(self): | 66 | def id(self): |
| 64 | return 'asdfasdf' | 67 | from hashlib import md5 |
diff --git a/warmachine/connections/slack.py b/warmachine/connections/slack.py index 8ae52da..384a1f3 100644 --- a/warmachine/connections/slack.py +++ b/warmachine/connections/slack.py | |||
| @@ -27,6 +27,7 @@ class SlackWS(Connection): | |||
| 27 | self.reconnect_url = '' | 27 | self.reconnect_url = '' |
| 28 | 28 | ||
| 29 | self.channel_map = {} # channel and im info keyed by the slack id | 29 | self.channel_map = {} # channel and im info keyed by the slack id |
| 30 | self.channel_name_to_id = {} # slack channel/group name mapped to the id | ||
| 30 | self.user_map = {} # user info keyed by their slack id | 31 | self.user_map = {} # user info keyed by their slack id |
| 31 | self.user_nick_to_id = {} # slack user id mapped to the (nick)name | 32 | self.user_nick_to_id = {} # slack user id mapped to the (nick)name |
| 32 | 33 | ||
| @@ -46,21 +47,26 @@ class SlackWS(Connection): | |||
| 46 | self.host = self.authenticate() | 47 | self.host = self.authenticate() |
| 47 | self.log.info('Connecting to {}'.format(self.host)) | 48 | self.log.info('Connecting to {}'.format(self.host)) |
| 48 | self.ws = await websockets.connect(self.host) | 49 | self.ws = await websockets.connect(self.host) |
| 50 | self.STATUS = CONNECTED | ||
| 51 | |||
| 52 | return True | ||
| 49 | 53 | ||
| 50 | async def read(self): | 54 | async def read(self): |
| 51 | if self.ws: | 55 | if self.ws: |
| 52 | message = json.loads(await self.ws.recv()) | 56 | message = json.loads(await self.ws.recv()) |
| 53 | # Slack is acknowledging a message was sent. Do nothing | 57 | # Slack is acknowledging a message was sent. Do nothing |
| 54 | if 'type' not in message and 'reply_to' in message: | 58 | if 'reply_to' in message: |
| 55 | # {'ok': True, | 59 | # {'ok': True, |
| 56 | # 'reply_to': 1, | 60 | # 'reply_to': 1, |
| 57 | # 'text': "['!whois', 'synic']", | 61 | # 'text': "['!whois', 'synic']", |
| 58 | # 'ts': '1469743355.000150'} | 62 | # 'ts': '1469743355.000150'} |
| 63 | self.log.debug('Ignoring reply_to message: {}'.format( | ||
| 64 | pformat(message))) | ||
| 59 | return | 65 | return |
| 60 | 66 | ||
| 61 | self.log.debug('new message parsed: {}'.format(message)) | 67 | self.log.debug('new slack message: {}'.format(pformat(message))) |
| 62 | # Handle actual messages | ||
| 63 | if message['type'] == 'message' and 'subtype' not in message: | 68 | if message['type'] == 'message' and 'subtype' not in message: |
| 69 | # Handle text messages from users | ||
| 64 | return await self.process_message(message) | 70 | return await self.process_message(message) |
| 65 | else: | 71 | else: |
| 66 | if 'subtype' in message: | 72 | if 'subtype' in message: |
| @@ -69,6 +75,8 @@ class SlackWS(Connection): | |||
| 69 | msgtype = '{}_{}'.format( | 75 | msgtype = '{}_{}'.format( |
| 70 | message['type'], message['subtype']) | 76 | message['type'], message['subtype']) |
| 71 | else: | 77 | else: |
| 78 | # This is a non-message event from slack. | ||
| 79 | # https://api.slack.com/events | ||
| 72 | msgtype = message['type'] | 80 | msgtype = message['type'] |
| 73 | 81 | ||
| 74 | # Look for on_{type} methods to pass the dictionary to for | 82 | # Look for on_{type} methods to pass the dictionary to for |
| @@ -80,18 +88,21 @@ class SlackWS(Connection): | |||
| 80 | self.log.debug('{} does not exist for message: {}'.format( | 88 | self.log.debug('{} does not exist for message: {}'.format( |
| 81 | func_name, message)) | 89 | func_name, message)) |
| 82 | 90 | ||
| 83 | async def say(self, message, destination_id): | 91 | async def say(self, message, destination): |
| 84 | """ | 92 | """ |
| 85 | Say something in the provided channel or IM by id | 93 | Say something in the provided channel or IM by id |
| 86 | """ | 94 | """ |
| 87 | # If the destination is a user, figure out the DM channel id | 95 | # If the destination is a user, figure out the DM channel id |
| 88 | if destination_id.startswith('U'): | 96 | if destination and destination.startswith('#'): |
| 89 | destination_id = self.get_dm_id_by_user(destination_id) | 97 | destination = self.channel_name_to_id[destination.replace('#','')] |
| 98 | else: | ||
| 99 | _user = self.user_nick_to_id[destination] | ||
| 100 | destination = self.get_dm_id_by_user(_user) | ||
| 90 | 101 | ||
| 91 | message = { | 102 | message = { |
| 92 | 'id': 1, # TODO: this should be a get_msgid call or something | 103 | 'id': 1, # TODO: this should be a get_msgid call or something |
| 93 | 'type': 'message', | 104 | 'type': 'message', |
| 94 | 'channel': destination_id, | 105 | 'channel': destination, |
| 95 | 'text': str(message) | 106 | 'text': str(message) |
| 96 | } | 107 | } |
| 97 | self.log.debug("Saying {}".format(message)) | 108 | self.log.debug("Saying {}".format(message)) |
| @@ -124,25 +135,24 @@ class SlackWS(Connection): | |||
| 124 | raise Exception('Slack Error: {}'.format( | 135 | raise Exception('Slack Error: {}'.format( |
| 125 | self._info.get('error', 'Unknown Error'))) | 136 | self._info.get('error', 'Unknown Error'))) |
| 126 | 137 | ||
| 127 | self.process_connect_info() | 138 | # Slack returns a huge json struct with a bunch of information |
| 139 | self.process_connect_info(self._info) | ||
| 128 | 140 | ||
| 129 | self.log.debug('Got websocket url: {}'.format(self._info.get('url'))) | 141 | self.log.debug('Got websocket url: {}'.format(self._info.get('url'))) |
| 130 | return self._info.get('url') | 142 | return self._info.get('url') |
| 131 | 143 | ||
| 132 | def process_connect_info(self): | 144 | def process_connect_info(self, info): |
| 133 | """ | 145 | """ |
| 134 | Processes the connection info provided by slack | 146 | Processes the connection info provided by slack |
| 135 | """ | 147 | """ |
| 136 | if not self._info: | 148 | # If there is nothing to process then return |
| 149 | if not info: | ||
| 137 | return | 150 | return |
| 138 | with open('slack_info.json', 'w') as f: | ||
| 139 | f.write(pformat(self._info)) | ||
| 140 | |||
| 141 | self.status = CONNECTED | ||
| 142 | 151 | ||
| 143 | # Save the bot's id | 152 | # Save the bot's id |
| 144 | try: | 153 | try: |
| 145 | self.my_id = self._info['self'].get('id', '000') | 154 | self.my_id = self._info['self'].get('id', '000') |
| 155 | self.nick = self._info['self'].get('name', None) | ||
| 146 | except KeyError: | 156 | except KeyError: |
| 147 | self.log.error('Unable to read self section of connect info') | 157 | self.log.error('Unable to read self section of connect info') |
| 148 | 158 | ||
| @@ -158,26 +168,35 @@ class SlackWS(Connection): | |||
| 158 | # Map Channels | 168 | # Map Channels |
| 159 | for c in self._info.get('channels', []): | 169 | for c in self._info.get('channels', []): |
| 160 | self.channel_map[c['id']] = c | 170 | self.channel_map[c['id']] = c |
| 171 | self.channel_name_to_id[c['name']] = c['id'] | ||
| 161 | 172 | ||
| 162 | for g in self._info.get('groups', []): | 173 | for g in self._info.get('groups', []): |
| 163 | self.channel_map[g['id']] = g | 174 | self.channel_map[g['id']] = g |
| 175 | self.channel_name_to_id[g['name']] = g['id'] | ||
| 164 | 176 | ||
| 165 | async def process_message(self, msg): | 177 | async def process_message(self, msg): |
| 166 | # Built-in !whois action | ||
| 167 | if 'text' not in msg: | 178 | if 'text' not in msg: |
| 168 | raise Exception(msg) | 179 | raise Exception(msg) |
| 180 | |||
| 181 | # Built-in !whois command. Return information about a particular user. | ||
| 169 | if msg['text'].startswith('!whois'): | 182 | if msg['text'].startswith('!whois'): |
| 170 | nicknames = msg['text'].split(' ')[1:] | 183 | nicknames = msg['text'].split(' ')[1:] |
| 171 | for n in nicknames: | 184 | for n in nicknames: |
| 172 | await self.say(pformat(self.user_map[self.user_nick_to_id[n]]), | 185 | await self.say(pformat(self.user_map[self.user_nick_to_id[n]]), |
| 173 | msg['channel']) | 186 | msg['channel']) |
| 174 | return | 187 | return |
| 175 | elif msg['text'].startswith('!looptime'): | 188 | |
| 176 | await self.say(self._loop.time(), msg['channel']) | 189 | # Map the slack ids to usernames and channels/groups names |
| 190 | user_nickname = self.user_map[msg['user']]['name'] | ||
| 191 | if msg['channel'].startswith('D'): | ||
| 192 | # This is a private message | ||
| 193 | channel = None | ||
| 194 | else: | ||
| 195 | channel = '#{}'.format(self.channel_map[msg['channel']]['name']) | ||
| 177 | 196 | ||
| 178 | retval = { | 197 | retval = { |
| 179 | 'sender': msg['user'], | 198 | 'sender': user_nickname, |
| 180 | 'channel': msg['channel'], | 199 | 'channel': channel, |
| 181 | 'message': msg['text'] | 200 | 'message': msg['text'] |
| 182 | } | 201 | } |
| 183 | return retval | 202 | return retval |
| @@ -191,16 +210,15 @@ class SlackWS(Connection): | |||
| 191 | https://api.slack.com/events/user_change | 210 | https://api.slack.com/events/user_change |
| 192 | """ | 211 | """ |
| 193 | user_info = msg['user'] | 212 | user_info = msg['user'] |
| 213 | |||
| 214 | self.user_map[user_info['id']] = user_info | ||
| 215 | |||
| 216 | # Update the nick mapping if the user changed their nickname | ||
| 194 | try: | 217 | try: |
| 195 | old_nick = self.user_map[user_info['id']]['nick'] | 218 | old_nick = self.user_map[user_info['id']]['nick'] |
| 196 | except KeyError as e: | 219 | except KeyError as e: |
| 197 | old_nick = None | 220 | old_nick = None |
| 198 | self.log.exception('KeyError: {}'.format(e)) | ||
| 199 | self.log.exception('{}'.format(msg)) | ||
| 200 | |||
| 201 | self.user_map[user_info['id']] = user_info | ||
| 202 | 221 | ||
| 203 | # Update the nick mapping if the user changed their nickname | ||
| 204 | if old_nick and old_nick != user_info['nick']: | 222 | if old_nick and old_nick != user_info['nick']: |
| 205 | del self.user_nick_to_id[old_nick] | 223 | del self.user_nick_to_id[old_nick] |
| 206 | self.user_nick_to_id[user_info['nick']] = user_info['id'] | 224 | self.user_nick_to_id[user_info['nick']] = user_info['id'] |
| @@ -225,6 +243,7 @@ class SlackWS(Connection): | |||
| 225 | )) | 243 | )) |
| 226 | self.user_map[msg['user']]['presence'] = msg['presence'] | 244 | self.user_map[msg['user']]['presence'] = msg['presence'] |
| 227 | 245 | ||
| 246 | @memoize # the dm id should never change | ||
| 228 | def get_dm_id_by_user(self, user_id): | 247 | def get_dm_id_by_user(self, user_id): |
| 229 | """ | 248 | """ |
| 230 | Return the channel id for a direct message to a specific user. | 249 | Return the channel id for a direct message to a specific user. |
| @@ -251,8 +270,9 @@ class SlackWS(Connection): | |||
| 251 | 270 | ||
| 252 | return data['channel']['id'] | 271 | return data['channel']['id'] |
| 253 | 272 | ||
| 254 | |||
| 255 | def get_users_by_channel(self, channel): | 273 | def get_users_by_channel(self, channel): |
| 274 | channel = self.channel_name_to_id[channel.replace('#', '')] | ||
| 275 | |||
| 256 | if channel.startswith('G'): | 276 | if channel.startswith('G'): |
| 257 | key = 'group' | 277 | key = 'group' |
| 258 | elif channel.startswith('C'): | 278 | elif channel.startswith('C'): |
| @@ -267,14 +287,17 @@ class SlackWS(Connection): | |||
| 267 | 'channel': channel, | 287 | 'channel': channel, |
| 268 | })) | 288 | })) |
| 269 | 289 | ||
| 270 | self.log.debug(url) | 290 | self.log.debug('Gathering list of users for channel {} from: {}'.format( |
| 291 | channel, url)) | ||
| 271 | req = urllib.request.Request(url) | 292 | req = urllib.request.Request(url) |
| 272 | r = json.loads(urllib.request.urlopen(req).read().decode('utf-8')) | 293 | r = json.loads(urllib.request.urlopen(req).read().decode('utf-8')) |
| 273 | 294 | ||
| 274 | self.log.debug(r) | 295 | users = [] |
| 296 | for u_id in r[key]['members']: | ||
| 297 | users.append(self.user_map[u_id]['name']) | ||
| 275 | 298 | ||
| 276 | self.log.debug(pformat(r[key]['members'])) | 299 | self.log.debug(pformat(users)) |
| 277 | return r[key]['members'] | 300 | return users |
| 278 | 301 | ||
| 279 | async def on_group_join(self, channel): | 302 | async def on_group_join(self, channel): |
| 280 | """ | 303 | """ |