aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.org12
-rwxr-xr-xbin/dbolla2
-rw-r--r--warmachine/addons/standup.py190
-rw-r--r--warmachine/connections/base.py14
-rw-r--r--warmachine/connections/irc.py5
-rw-r--r--warmachine/connections/slack.py81
6 files changed, 183 insertions, 121 deletions
diff --git a/README.org b/README.org
index 238e929..a968cbf 100644
--- a/README.org
+++ b/README.org
@@ -24,7 +24,7 @@ Simply run the command:
24* Writing a Connection 24* Writing a Connection
25To write a new connection protocol you must inherit from 25To 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
27must implement to support the plugins plugins. 27must implement to support the plugins.
28** ~__config_prefix__~ 28** ~__config_prefix__~
29This global is used to decide which connection to use when it is found in the 29This global is used to decide which connection to use when it is found in the
30config file. E.g. IRC uses ~'irc'~ and Slack uses ~'slack'~. It should be 30config 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)~
51This method is used by plugins to send a message to a channel or user.
50** ~id~ 52** ~id~
51This should return a unique id used to identify this particular connection. As 53This should return a unique id used to identify this particular connection. This
52an example, the IRC connection uses something like this: 54is used by plugins when saving state. As an example, the IRC connection uses
55something 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)~
67This method should return a list of all users (including the bot) for the
68connection.
diff --git a/bin/dbolla b/bin/dbolla
index 6c0b573..e00143b 100755
--- a/bin/dbolla
+++ b/bin/dbolla
@@ -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 """