aboutsummaryrefslogtreecommitdiffstats
path: root/warmachine/connections/slack.py
diff options
context:
space:
mode:
Diffstat (limited to 'warmachine/connections/slack.py')
-rw-r--r--warmachine/connections/slack.py81
1 files changed, 52 insertions, 29 deletions
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 """