CodeCommitsIssuesPull requestsActionsInsightsSecurity
6f5c802fd4c550b56566c1ab62c16ebc73522197

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

hangouts_chat.py

318lines · modecode

1import json
2import httplib2
3import logging
4from typing import Iterable, Optional
5from errbot.backends.base import Message
6from errbot.backends.base import Person
7from errbot.backends.base import Room, RoomError
8from errbot.errBot import ErrBot
9from google.cloud import pubsub
10from oauth2client.service_account import ServiceAccountCredentials
11
12from markdownconverter import hangoutschat_markdown_converter
13
14log = logging.getLogger('errbot.backends.hangoutschat')
15
16
17class RoomsNotSupportedError(RoomError):
18 def __init__(self, message=None):
19 if message is None:
20 message = (
21 "Most Room operations are not supported in Google Hangouts Chat."
22 "While Rooms are a _concept_, the API is minimal and does not "
23 "expose this functionality to bots"
24 )
25 super().__init__(message)
26
27
28class GoogleHangoutsChatAPI:
29 """
30 Represents the Google Hangouts REST API
31 See: https://developers.google.com/hangouts/chat/reference/rest/
32 """
33 base_url = 'https://chat.googleapis.com/v1'
34 # Numbe of results to fetch at a time. Default is 100, Max is 1000
35 page_size = 500
36
37 def __init__(self, creds_file: str, scope: str = 'https://www.googleapis.com/auth/chat.bot'):
38 self.creds_file = creds_file
39 self.scope = scope
40
41 @property
42 def credentials(self):
43 return ServiceAccountCredentials.from_json_keyfile_name(self.creds_file,
44 scopes=[self.scope])
45
46 @property
47 def client(self):
48 return self.credentials.authorize(httplib2.Http())
49
50 def _request(self, uri: str, query_string: str = None, **kwargs) -> Optional[dict]:
51 request_args = {
52 'method': 'GET',
53 'headers': {'Content-Type': 'application/json; charset=UTF-8', }}
54 request_args.update(kwargs)
55 url = '{}/{}'.format(self.base_url, uri)
56 if query_string:
57 url += '?{}'.format(query_string)
58 result, content = self.client.request(
59 uri=url,
60 **request_args
61 )
62 if result['status'] == '200':
63 content_json = json.loads(content.decode('utf-8'))
64 return content_json
65 else:
66 log.error('status: {}, content: {}'.format(result['status'], content))
67
68 def _list(self, resource: str, return_attr: str, next_page_token: str = '') -> Iterable[dict]:
69 """
70 Gets a list of resources.
71
72 Args:
73 resource: name of resource to list
74 return_attr: name of attribute in the root of the response to get
75 resources from
76 next_page_token: the nextPageToken returned by the previous call
77
78 Yields:
79 dict: the next found resource
80 """
81
82 query_string = 'pageSize={}'.format(self.page_size)
83 if next_page_token:
84 query_string += '&pageToken={}'.format(next_page_token)
85 data = self._request(resource, query_string=query_string)
86 if data:
87 for itm in data[return_attr]:
88 yield itm
89 next_page_token = data.get('nextPageToken')
90 if next_page_token != '':
91 yield from self._list(resource, return_attr, next_page_token)
92
93 def get_spaces(self) -> Iterable[dict]:
94 return self._list('spaces', 'spaces')
95
96 def get_space(self, name: str) -> Optional[dict]:
97 return self._request('spaces/{}'.format(name.lstrip('spaces/')))
98
99 def get_members(self, space_name: str) -> Iterable[dict]:
100 return self._list('spaces/{}/members'.format(space_name.lstrip('spaces/')), 'memberships')
101
102 def get_member(self, space_name: str, name: str) -> Optional[dict]:
103 return self._request('spaces/{}/members/{}'.format(space_name.lstrip('spaces/'), name))
104
105 def create_message(self, space_name: str, body: dict, thread_key: str = None) -> Optional[dict]:
106 url = 'spaces/{}/messages'.format(space_name.lstrip('spaces/'))
107 if thread_key is None:
108 return self._request(url, body=json.dumps(body), method='POST')
109 else:
110 return self._request(url, body=json.dumps(body), method='POST',
111 query_string='threadKey={}'.format(thread_key))
112
113
114class HangoutsChatRoom(Room):
115 """
116 Represents a 'Space' in Google-Hangouts-Chat terminology
117 """
118 def __init__(self, space_id, chat_api):
119 super().__init__()
120 self.space_id = space_id
121 self.chat_api = chat_api
122 self._load()
123
124 def _load(self):
125 space = self.chat_api.get_space(self.space_id)
126 self.does_exist = bool(space)
127 self.display_name = space['displayName'] if self.does_exist else ''
128
129 def join(self, username=None, password=None):
130 raise RoomsNotSupportedError()
131
132 def create(self):
133 raise RoomsNotSupportedError()
134
135 def leave(self, reason=None):
136 raise RoomsNotSupportedError()
137
138 def destroy(self):
139 raise RoomsNotSupportedError()
140
141 @property
142 def joined(self):
143 raise RoomsNotSupportedError()
144
145 @property
146 def exists(self):
147 raise RoomsNotSupportedError()
148
149 @property
150 def topic(self):
151 raise RoomsNotSupportedError()
152
153 @property
154 def occupants(self):
155 memberships = self.chat_api.get_members(self.space_id)
156 occupants = []
157 for membership in memberships:
158 name = '{} ({} / {})'.format(membership['member']['displayName'],
159 membership['member']['name'],
160 membership['state'])
161 if membership['member']['type'] == 'BOT':
162 name += ' **BOT**'
163 occupants.append(HangoutsChatUser(name,
164 membership['member']['displayName'],
165 None,
166 membership['member']['type']))
167
168 return occupants
169
170 def invite(self, *args):
171 raise RoomsNotSupportedError()
172
173
174class HangoutsChatUser(Person):
175 def __init__(self, name, display_name, email, user_type):
176 super().__init__()
177 self.name = name
178 self.display_name = display_name
179 self.email = email
180 self.user_type = user_type
181
182 @property
183 def person(self):
184 return self.name
185
186 @property
187 def fullname(self):
188 return self.display_name
189
190 @property
191 def client(self):
192 return 'Hangouts Chat'
193
194 @property
195 def nick(self):
196 return self.display_name
197
198 @property
199 def aclattr(self):
200 return self.email
201
202
203class GoogleHangoutsChatBackend(ErrBot):
204 def __init__(self, config):
205 super().__init__(config)
206 identity = config.BOT_IDENTITY
207 self.at_name = config.BOT_PREFIX
208 self.creds_file = identity['GOOGLE_CREDS_FILE']
209 self.gce_project = identity['GOOGLE_CLOUD_ENGINE_PROJECT']
210 self.gce_topic = identity['GOOGLE_CLOUD_ENGINE_PUBSUB_TOPIC']
211 self.gce_subscription = identity['GOOGLE_CLOUD_ENGINE_PUBSUB_SUBSCRIPTION']
212 self.chat_api = GoogleHangoutsChatAPI(self.creds_file)
213 self.bot_identifier = HangoutsChatUser(None, self.at_name, None, None)
214
215 self.md = hangoutschat_markdown_converter()
216
217 def _subscribe_to_pubsub_topic(self, project, topic_name, subscription_name, callback):
218 subscriber = pubsub.SubscriberClient()
219 subscription_name = 'projects/{}/subscriptions/{}'.format(project, subscription_name)
220 log.info("Subscribed to {}".format(subscription_name))
221 return subscriber.subscribe(subscription_name, callback=callback)
222
223 def _handle_message(self, message):
224 try:
225 data = json.loads(message.data.decode('utf-8'))
226 except Exception:
227 log.warning('Receieved malformed message: {}'.format(message.data))
228 message.ack()
229 return
230
231 if not data.get('message'):
232 message.ack()
233 return
234 sender_blob = data['message']['sender']
235 sender = HangoutsChatUser(sender_blob['name'],
236 sender_blob['displayName'],
237 sender_blob['email'],
238 sender_blob['type'])
239 message_body = data['message']['text']
240 message.ack()
241 context = {
242 'space_id': data['space']['name'],
243 'thread_id': data['message']['thread']['name']
244 }
245 msg = Message(body=message_body.strip(), frm=sender, extras=context)
246 is_dm = data['message']['space']['type'] == 'DM'
247 if is_dm:
248 msg.to = self.bot_identifier
249 self.callback_message(msg)
250
251 def send_message(self, message):
252 super(GoogleHangoutsChatBackend, self).send_message(message)
253 log.info("Sending {}".format(message.body))
254 space_id = message.extras.get('space_id', None)
255 if not space_id:
256 log.info(message.body)
257 return
258 thread_id = message.extras.get('thread_id', None)
259 message_payload = {
260 'text': self.md.convert(message.body)
261 }
262
263 if thread_id:
264 message_payload['thread'] = {'name': thread_id}
265
266 self.chat_api.create_message(space_id, message_payload)
267
268 def send_card(self, cards, space_id, thread_id=None):
269 log.info("Sending card")
270 message_payload = {
271 'cards': cards
272 }
273 if thread_id:
274 message_payload['thread'] = {'name': thread_id}
275
276 self.chat_api.create_message(space_id, message_payload)
277
278 def serve_forever(self):
279 subscription = self._subscribe_to_pubsub_topic(self.gce_project,
280 self.gce_topic,
281 self.gce_subscription,
282 self._handle_message)
283 self.connect_callback()
284
285 try:
286 import time
287 while True:
288 time.sleep(10)
289 except KeyboardInterrupt:
290 log.info("Exiting")
291 finally:
292 subscription.close()
293 self.disconnect_callback()
294 self.shutdown()
295
296 def build_identifier(self, strrep):
297 return HangoutsChatUser(None, strrep, None, None)
298
299 def build_reply(self, msg, text=None, private=False, threaded=False):
300 response = Message(body=text, frm=msg.to, to=msg.frm, extras=msg.extras)
301 return response
302
303 def change_presence(self, status='online', message=''):
304 return None
305
306 @property
307 def mode(self):
308 return 'Google_Hangouts_Chat'
309
310 def query_room(self, room):
311 return HangoutsChatRoom(room, self.chat_api)
312
313 def rooms(self):
314 spaces = self.chat_api.get_spaces()
315 rooms = ['{} ({})'.format(space['displayName'], space['name'])
316 for space in list(spaces) if space['type'] == 'ROOM']
317
318 return rooms