· 6 years ago · Apr 14, 2020, 06:40 AM
1#requests
2import requests
3import json
4
5#google stuff
6import datetime
7from googleapiclient.discovery import build
8from googleapiclient.errors import HttpError
9from google.oauth2 import service_account
10
11
12#errors
13import logging
14from logging.handlers import RotatingFileHandler
15import sys
16
17connector_id = "gsuitev3connector"
18
19logger = logging.getLogger(connector_id)
20hdlr = RotatingFileHandler('./logs/' + connector_id + '.log', maxBytes=10485760, backupCount=10)
21formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
22hdlr.setFormatter(formatter)
23logger.addHandler(hdlr)
24
25
26def error_handling():
27 return 'Error: {}. {}, line: {}'.format(sys.exc_info()[0], sys.exc_info()[1], sys.exc_info()[2].tb_lineno)
28
29
30
31
32#placeholder manifest until a manifest can be sent to this script from core
33manifest = {
34 "tenant_id": "reciprocity_ppd",
35 "log_level": "info",
36 "recordtype": "Events",
37 "job_type": "read",
38 "core_config": {
39 "core_api_url": "https://reciprocity.saasyan.com.au/api/v1.0/connector/records"
40 },
41 "token": "e5ba6a6e-e287-4d59-94d0-5751a2186dfb",
42 "job_id": 3811,
43 "records": [],
44 "end_of_list": 1,
45 "job_status": "success",
46 "connector_config": {
47 "client_id": {
48 "type": "string",
49 "value": "111232198740383522653",
50 "required": "true",
51 "description": "Client ID"
52 },
53 "project_id": {
54 "type": "string",
55 "value": "gsuitev3connector",
56 "required": "true",
57 "description": "Project ID"
58 },
59 "private_key": {
60 "type": "string",
61 "value": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCehCXlcNODbNtm\nRxkOOJ+NmjwwSaf8olaqnGcitecn9rVJP+uzjmwmmKoyNLw2SMT9JVWXamoM9YGs\nwsgUk+zM+H4ItL2iJBECLyZNF0mmEY2uqvZJELgQhttj2y4lUrwZT/eo1VLGPz6f\nJz9ZC49+/IDvghyddtTsCiBqkgEn1DtTHmSNcz1CZnicbRwBwA53GqGOGpY/Beeu\n2eMYllU0Bb/Dd5ketQeRUGwCgK4C4euLPyX4FeGVn5h7StIh1HMOSPQSxFTJdxSw\nDop8qEJcgm75ckzPpbMzBYL2QaPK/brVFAPKOXFQJae7LvB98Hi+phQ/8DeF1zVp\n48oHykN3AgMBAAECggEAAox+TbjQfBCA6iumC+627LlKKD5jZKekBuJDJH+8t1St\nFnHDyObhzIX3AUiMPGpTklIDXBYjSC2d9fGfZRVPYP4fqKwtiU1xYXVjSn58CDuP\nIlEwFX2QqeGWkQwHZCTo3OvGeQDxjDoOVJqSuqGWSfo0hPq00xUgSf7hd3Gnx4uz\nJHuYX+nLoOMgfktXAEwEktVL2D0jjG6a2Y/55uChmV+nZG+h8nPsAhWMsep1IAnb\n7iiovU2BRdyjemxLr/Z3DZ2SX1foOq3IfsbvoDBr50QtL5jmAaovN4P9nUbCLQmW\n2rQhT/B1SDtXsBtjPPfVv5kegVxCDOMBIi5aLiqi4QKBgQDPZTlss8soQTQhS0bh\nq9N55ahHw0wYpqC3tziaBiESc02BgTdBNeTvk574ALqCBxSAVK0Aks0teeYpSoY3\nfN8hyGp/R8ET0kwcJzQtghcFF55wymnUtH4ijT2Nbeq8yNxK6iFvnGxTjiI51eI2\nkZ8A69K3I4ti6zbomLzYlON+hQKBgQDDqmUjRCEDZsaupPnAvX2ywV0T4sDUdFl8\nJ4fTjcsyt8cQrEzUN7e9/tOLdWa+AJV/Sp5KYpROOq383GOsSfSVtQkbmjIb83CY\n2AayDofSxvoEZJ7OuWTk/fhhMjIb9r4hPlULeGlFG5X3Qs+/IJXo5jYvs1B1hu9i\nVtZqrrwwywKBgQCKmEar0ert18USis+vd7g3ObYfXu+3eYLlwtdcdsQbJFqjNMrM\nFUwMnkYZzcLVqg2VRQDn5TKgNVoONfNDmXszhE9HB5iLunmKRtijoM/pY0NKdLoM\nnyG7zU9Y+B9qUYCFv7jgcVQi8lUE+V3WwbHUV8PVBzfavv1OquNoiPcOUQKBgDm2\n0hxD5UT+lM5A5VZfzmritFTARsakBx0i/+J76ZrtMG+Pkx3pzW3ic32oRIT44R/p\n36TrTPmCP33deB0Ct31NKAGXSv6JoQXEer0coh2fCXIwR9OMReFxtEjfKH6tGknt\n++HlQ4/Z4jNL5sFmmDFZblRFnQ79nTCxko9nOSVfAoGAasCGuJdgB0Td6WRBQ5AP\niIpRH3iuKkMANPihap0yOHCne0xpfMmnQ5nEP5gzN8XYMvk2WG6zp/nSVokPvGrZ\nyj/PaFhI346lAGn1lowAipCTdpQg5sMjrUBcPuUSLRWQgwEfMbY3HokgbUQzYcp0\nkV1shtVq1v8oV3oXJQkkLrs=\n-----END PRIVATE KEY-----\n",
62 "required": "true",
63 "description": "Private Key"
64 },
65 "client_email": {
66 "type": "string",
67 "value": "service@gsuitev3connector.iam.gserviceaccount.com",
68 "required": "true",
69 "description": "Client Email"
70 },
71 "private_key_id": {
72 "type": "integer",
73 "value": "c68bdcc64d07a7fc82121fb57a1bc27d620364de",
74 "required": "true",
75 "description": "Project Key ID"
76 }
77 },
78 "job_parameters": {
79 "field_mapping": {
80 "kind": "kind",
81 "id": "id",
82 "status": "status",
83 "summary": "summary",
84 "description": "description",
85 "location": "location",
86 "organizer": "organizer",
87 "start": "start",
88 "end": "end"
89 },
90 "time_zone": "Australia/Sydney",
91 },
92 "connector_id": "gsuite_v3_api_native_generic"
93}
94
95
96#a config object has to be formatted differently to how it is formatted in the manifest for google authentication to work
97config = {}
98
99#firstly all dynamic fields are copied from manifest object to the new config object
100config["client_id"] = manifest['connector_config']['client_id']['value']
101config["project_id"] = manifest['connector_config']['project_id']['value']
102config["private_key"] = manifest['connector_config']['private_key']['value']
103config["client_email"] = manifest['connector_config']['client_email']['value']
104config["private_key_id"] = manifest['connector_config']['private_key_id']['value']
105
106#then all static fields are hardcoded into the config object
107config['auth_uri'] = "https://accounts.google.com/o/oauth2/auth"
108config['token_uri'] = "https://oauth2.googleapis.com/token"
109config['auth_provider_x509_cert_url'] = "https://www.googleapis.com/oauth2/v1/certs"
110config['client_x509_cert_url'] = "https://www.googleapis.com/robot/v1/metadata/x509/service%40gsuitev3connector.iam.gserviceaccount.com"
111config['type'] = "service_account"
112
113
114#levels of access needed - currently /calendar gives read + write + manage access
115SCOPES = ['https://www.googleapis.com/auth/calendar']
116
117
118#creating credentials object
119creds = service_account.Credentials.from_service_account_info(
120 config, scopes=SCOPES)
121
122
123fieldmapping = manifest['job_parameters']['field_mapping']
124
125
126
127
128def main(manifest, records):
129
130 #enumerate this for each user in the tenants db
131 try:
132 #Building api service - authentication occurs here
133 service = build('calendar', 'v3', credentials=creds)
134
135 calendarId = 'greg@iteralis.com.au'
136 job_status = None
137
138 #logging setup
139 log_level = manifest["log_level"]
140 if log_level == "info" or log_level == "Info" or log_level == "INFO":
141 logger.setLevel(logging.INFO)
142 elif log_level == "warning" or log_level == "Warning" or log_level == "WARNING" or log_level == "WARN" \
143 or log_level == "warn" or log_level == "Warn":
144 logger.setLevel(logging.WARNING)
145 else:
146 logger.setLevel(logging.ERROR)
147
148
149 #start of job logic
150 if manifest['recordtype'] == 'Events':
151 if manifest['job_type'] == 'read':
152
153 #getting all event data from now
154 now = datetime.datetime.utcnow().isoformat() + 'Z' # 'Z' indicates UTC time
155 events_result = service.events().list(calendarId=calendarId, timeMin=now,
156 singleEvents=True,
157 orderBy='startTime').execute()
158
159 #getting events out of events result object
160 events = events_result.get('items', [])
161
162 #formatting event data for the manifest
163 records = []
164 for event in events:
165 e = {}
166 for k in fieldmapping.keys():
167 if k == "organizer":
168 e[fieldmapping[k]] = {
169 "email": event['organizer']['email'],
170 "self": "true"
171 }
172 elif k == "start":
173 e[fieldmapping[k]] = {
174 "dateTime": event['start']['dateTime'],
175 "timeZone": manifest['job_parameters']['time_zone']
176 }
177 elif k == "end":
178 e[fieldmapping[k]] = {
179 "dateTime": event['end']['dateTime'],
180 "timeZone": manifest['job_parameters']['time_zone']
181 }
182 else:
183 e[fieldmapping[k]] = event[k]
184
185 record = {"data": e, "id": str(event[fieldmapping['id']])}
186 records.append(record)
187 manifest['records'] = records
188
189 #sending manifest back to core
190 response = requests.request("POST", manifest["core_config"]["core_api_url"],
191 headers={
192 "Content-Type": "application/json",
193 "X-Secret": manifest["token"],
194 "job_id": str(manifest["job_id"]),
195 "tenant_id": manifest["tenant_id"]
196 },
197 data=json.dumps(manifest)
198 )
199 #logging to core+logfile
200 logger.info("Reading calendar events" + str(json.dumps(manifest["records"], indent=3)))
201 return json.dumps(events, indent=2)
202
203 elif manifest['job_type'] == 'write':
204
205
206 job_status = 'success'
207 updated_records = []
208
209 #change records to manifest["records"] when in production
210 for record in records:
211 if record['action'] == 'delete':
212 #getting id so we can use it to delete
213 eventID = record['data']['id']
214
215 error = None
216
217 #trying to delete using the API
218 try:
219 res = service.events().delete(calendarId=calendarId, eventId=eventID).execute()
220
221 #if delete comes back with an error then log it
222 except HttpError as err:
223 if err.resp.status >= 400:
224 error = err.resp.status
225 logger.error(err)
226
227 #setting the error message on the record and then appending record to updated_records
228 record["error"] = error
229 updated_records.append(record)
230
231
232 elif record['action'] == 'update':
233 #getting the body from the record, also getting the eventID from the record as well (this record comes in the manifest from the core)
234 body = {}
235 for k in fieldmapping.keys():
236 body[k] = record['data'][fieldmapping[k]]
237
238 eventID = record['data'][fieldmapping['id']]
239
240
241 error = None
242
243 #trying to update with the API
244 try:
245 res = service.events().update(calendarId=calendarId, eventId=eventID, body=body).execute()
246
247 #if an error is caught then log the error
248 except HttpError as err:
249 if err.resp.status >= 400:
250 error = err.resp.status
251 logger.error(err)
252
253 #setting the error message on the record and then appending record to updated_records
254 record["error"] = error
255 updated_records.append(record)
256
257
258 elif record['action'] == 'create':
259 #getting the body from record
260 body = {}
261 for k in fieldmapping.keys():
262 if k == 'id':
263 continue
264 body[k] = record['data'][fieldmapping[k]]
265
266 error = None
267
268
269 #trying to create the event with the API
270 try:
271 res = service.events().insert(calendarId=calendarId, body=body).execute()
272
273 #if creation fails then log the error
274 except HttpError as err:
275 if err.resp.status >= 400:
276 error = err.resp.status
277 logger.error(err)
278
279 #setting the error message on the record and then appending record to updated_records
280 record["error"] = error
281 updated_records.append(record)
282
283 #logging that records have been send to core
284 logger.info("records sent to core: " + json.dumps(updated_records, indent=4, sort_keys=True))
285
286 # Send response back to core.
287 response = requests.request("POST", manifest["core_config"]["core_api_url"],
288 headers={
289 "Content-Type": "application/json",
290 "X-Secret": manifest["token"],
291 "job_id": str(manifest["job_id"]),
292 "tenant_id": manifest["tenant_id"]
293 },
294 data=json.dumps({
295 "job_status": job_status,
296 "end_of_list": 1,
297 "log_level": manifest["log_level"],
298 "job_type": manifest["job_type"],
299 "connector_id": manifest["connector_id"],
300 "recordtype": manifest["recordtype"],
301 "records": updated_records
302 })
303 )
304
305 #either a success or error depending on the status code recieved from core
306 if response.status_code < 300:
307 logger.info("core_api_status_code: " + str(response.status_code))
308 return {"success": {"core_api_status_code": str(response.status_code)}}
309 else:
310 logger.error("core_api_status_code: " + str(response.status_code))
311 return {"error": {"core_api_status_code": str(response.status_code)}}
312
313
314
315
316 else:
317 logger.error("Error: BAD_JOB_TYPE - job type must be 'read' or 'write'")
318 return {"error": {"connector": "invalid job type"}}
319
320
321
322
323 else:
324 logger.error("Error: BAD_RECORD_TYPE - record type must be 'Events'")
325 return {"error": {"connector": "invalid record type"}}
326
327
328 except:
329 logger.error("exception: " + str(error_handling()))
330 return {"error": {"exception: ": str(error_handling())}}
331
332
333
334
335
336
337#placeholder records object to test CRUD functions
338records = [{
339 "data": {
340 "kind": "calendar#event",
341 "id": "70bun2miab7bf1vjufrfm3r8fc",
342 "status": "confirmed",
343 "summary": "hello kev",
344 "description": "A chance to hear more about Google's developer products.",
345 "location": "800 Howard St., San Francisco, CA 94103",
346 "organizer": {
347 "email": "greg@iteralis.com.au",
348 "self": "true"
349 },
350 "start": {
351 "dateTime": "2020-04-15T18:00:00+10:00",
352 "timeZone": "America/Los_Angeles"
353 },
354 "end": {
355 "dateTime": "2020-04-15T18:00:00+10:00",
356 "timeZone": "America/Los_Angeles"
357 }
358 },
359 "action": "create"
360}]
361
362
363print(main(manifest, records))