First test implementation of video client
This commit is contained in:
		
							parent
							
								
									30f30d7669
								
							
						
					
					
						commit
						3cf7ffd6a7
					
				
					 1 changed files with 212 additions and 0 deletions
				
			
		
							
								
								
									
										212
									
								
								lib/videoClient.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										212
									
								
								lib/videoClient.ts
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,212 @@ | |||
| function handleErrorsJson(response) { | ||||
|     if (!response.ok) { | ||||
|         response.json().then(console.log); | ||||
|         throw Error(response.statusText); | ||||
|     } | ||||
|     return response.json(); | ||||
| } | ||||
| 
 | ||||
| function handleErrorsRaw(response) { | ||||
|     if (!response.ok) { | ||||
|         response.text().then(console.log); | ||||
|         throw Error(response.statusText); | ||||
|     } | ||||
|     return response.text(); | ||||
| } | ||||
| 
 | ||||
| const zoomAuth = (credential) => { | ||||
| 
 | ||||
|     const isExpired = (expiryDate) => expiryDate < +(new Date()); | ||||
|     const authHeader = 'Basic ' + Buffer.from(process.env.CLIENT_ID + ':' + process.env.CLIENT_SECRET).toString('base64'); | ||||
| 
 | ||||
|     const refreshAccessToken = (refreshToken) => fetch('https://zoom.us/oauth/token', { | ||||
|         method: 'POST', | ||||
|         headers: { | ||||
|             'Authorization': authHeader, | ||||
|             'Content-Type': 'application/x-www-form-urlencoded' | ||||
|         }, | ||||
|         body: new URLSearchParams({ | ||||
|             'refresh_token': refreshToken, | ||||
|             'grant_type': 'refresh_token', | ||||
|         }) | ||||
|     }) | ||||
|         .then(handleErrorsJson) | ||||
|         .then((responseBody) => { | ||||
|             credential.key.access_token = responseBody.access_token; | ||||
|             credential.key.expires_in = Math.round((+(new Date()) / 1000) + responseBody.expires_in); | ||||
|             return credential.key.access_token; | ||||
|         }) | ||||
| 
 | ||||
|     return { | ||||
|         getToken: () => !isExpired(credential.key.expires_in) ? Promise.resolve(credential.key.access_token) : refreshAccessToken(credential.key.refresh_token) | ||||
|     }; | ||||
| }; | ||||
| 
 | ||||
| interface Person { | ||||
|     name?: string, | ||||
|     email: string, | ||||
|     timeZone: string | ||||
| } | ||||
| 
 | ||||
| interface VideoMeeting { | ||||
|     title: string; | ||||
|     startTime: string; | ||||
|     endTime: string; | ||||
|     description?: string; | ||||
|     timezone: string; | ||||
|     organizer: Person; | ||||
|     attendees: Person[]; | ||||
| } | ||||
| 
 | ||||
| interface VideoApiAdapter { | ||||
|     createMeeting(meeting: VideoMeeting): Promise<any>; | ||||
| 
 | ||||
|     updateMeeting(uid: String, meeting: VideoMeeting); | ||||
| 
 | ||||
|     deleteMeeting(uid: String); | ||||
| 
 | ||||
|     getAvailability(dateFrom, dateTo): Promise<any>; | ||||
| } | ||||
| 
 | ||||
| const ZoomVideo = (credential): VideoApiAdapter => { | ||||
| 
 | ||||
|     const auth = zoomAuth(credential); | ||||
| 
 | ||||
|     const translateMeeting = (meeting: VideoMeeting) => { | ||||
|         // Documentation at: https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingcreate
 | ||||
|         return { | ||||
|             topic: meeting.title, | ||||
|             type: 2,    // Means that this is a scheduled meeting
 | ||||
|             start_time: meeting.startTime, | ||||
|             duration: 60,    //TODO calculate endTime - startTime (in minutes, int)
 | ||||
|             //schedule_for: "string",   TODO: Used when scheduling the meeting for someone else (needed?)
 | ||||
|             timezone: meeting.timezone, | ||||
|             //password: "string",       TODO: Should we use a password? Maybe generate a random one?
 | ||||
|             agenda: meeting.description, | ||||
|             settings: { | ||||
|                 host_video: true, | ||||
|                 participant_video: true, | ||||
|                 cn_meeting: false,  // TODO: true if host meeting in china
 | ||||
|                 in_meeting: false,  // TODO: true if host meeting in india
 | ||||
|                 join_before_host: true, | ||||
|                 mute_upon_entry: false, | ||||
|                 watermark: false, | ||||
|                 use_pmi: false, | ||||
|                 approval_type: 2, | ||||
|                 audio: "both", | ||||
|                 auto_recording: "none", | ||||
|                 enforce_login: false, | ||||
|                 registrants_email_notification: true | ||||
|             } | ||||
|         }; | ||||
|     }; | ||||
| 
 | ||||
|     return { | ||||
|         getAvailability: (dateFrom, dateTo) => { | ||||
|             /*const payload = { | ||||
|                 schedules: [credential.key.email], | ||||
|                 startTime: { | ||||
|                     dateTime: dateFrom, | ||||
|                     timeZone: 'UTC', | ||||
|                 }, | ||||
|                 endTime: { | ||||
|                     dateTime: dateTo, | ||||
|                     timeZone: 'UTC', | ||||
|                 }, | ||||
|                 availabilityViewInterval: 60 | ||||
|             }; | ||||
| 
 | ||||
|             return auth.getToken().then( | ||||
|                 (accessToken) => fetch('https://graph.microsoft.com/v1.0/me/calendar/getSchedule', { | ||||
|                     method: 'post', | ||||
|                     headers: { | ||||
|                         'Authorization': 'Bearer ' + accessToken, | ||||
|                         'Content-Type': 'application/json' | ||||
|                     }, | ||||
|                     body: JSON.stringify(payload) | ||||
|                 }) | ||||
|                     .then(handleErrorsJson) | ||||
|                     .then(responseBody => { | ||||
|                         return responseBody.value[0].scheduleItems.map((evt) => ({ | ||||
|                             start: evt.start.dateTime + 'Z', | ||||
|                             end: evt.end.dateTime + 'Z' | ||||
|                         })) | ||||
|                     }) | ||||
|             ).catch((err) => { | ||||
|                 console.log(err); | ||||
|             });*/ | ||||
|         }, | ||||
|         //TODO Also add the client user to the meeting after creation
 | ||||
|         createMeeting: (meeting: VideoMeeting) => auth.getToken().then(accessToken => fetch('https://zoom.us/users/me/meetings', { | ||||
|             method: 'POST', | ||||
|             headers: { | ||||
|                 'Authorization': 'Bearer ' + accessToken, | ||||
|                 'Content-Type': 'application/json', | ||||
|             }, | ||||
|             body: JSON.stringify(translateMeeting(meeting)) | ||||
|         }).then(handleErrorsJson)), | ||||
|         deleteMeeting: (uid: String) => auth.getToken().then(accessToken => fetch('https://zoom.us/meetings/' + uid, { | ||||
|             method: 'DELETE', | ||||
|             headers: { | ||||
|                 'Authorization': 'Bearer ' + accessToken | ||||
|             } | ||||
|         }).then(handleErrorsRaw)), | ||||
|         updateMeeting: (uid: String, meeting: VideoMeeting) => auth.getToken().then(accessToken => fetch('https://zoom.us/meetings/' + uid, { | ||||
|             method: 'PATCH', | ||||
|             headers: { | ||||
|                 'Authorization': 'Bearer ' + accessToken, | ||||
|                 'Content-Type': 'application/json' | ||||
|             }, | ||||
|             body: JSON.stringify(translateMeeting(meeting)) | ||||
|         }).then(handleErrorsRaw)), | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| // factory
 | ||||
| const videoIntegrations = (withCredentials): VideoApiAdapter[] => withCredentials.map((cred) => { | ||||
|     switch (cred.type) { | ||||
|         case 'zoom': | ||||
|             return ZoomVideo(cred); | ||||
|         default: | ||||
|             return; // unknown credential, could be legacy? In any case, ignore
 | ||||
|     } | ||||
| }).filter(Boolean); | ||||
| 
 | ||||
| 
 | ||||
| const getBusyTimes = (withCredentials, dateFrom, dateTo) => Promise.all( | ||||
|   videoIntegrations(withCredentials).map(c => c.getAvailability(dateFrom, dateTo)) | ||||
| ).then( | ||||
|     (results) => results.reduce((acc, availability) => acc.concat(availability), []) | ||||
| ); | ||||
| 
 | ||||
| const createMeeting = (credential, meeting: VideoMeeting): Promise<any> => { | ||||
| 
 | ||||
|     //TODO Implement email template
 | ||||
|     /*createNewMeetingEmail( | ||||
|       meeting, | ||||
|     );*/ | ||||
| 
 | ||||
|     if (credential) { | ||||
|         return videoIntegrations([credential])[0].createMeeting(meeting); | ||||
|     } | ||||
| 
 | ||||
|     return Promise.resolve({}); | ||||
| }; | ||||
| 
 | ||||
| const updateMeeting = (credential, uid: String, meeting: VideoMeeting): Promise<any> => { | ||||
|     if (credential) { | ||||
|         return videoIntegrations([credential])[0].updateMeeting(uid, meeting); | ||||
|     } | ||||
| 
 | ||||
|     return Promise.resolve({}); | ||||
| }; | ||||
| 
 | ||||
| const deleteMeeting = (credential, uid: String): Promise<any> => { | ||||
|     if (credential) { | ||||
|         return videoIntegrations([credential])[0].deleteMeeting(uid); | ||||
|     } | ||||
| 
 | ||||
|     return Promise.resolve({}); | ||||
| }; | ||||
| 
 | ||||
| export {getBusyTimes, createMeeting, updateMeeting, deleteMeeting, VideoMeeting}; | ||||
		Loading…
	
		Reference in a new issue
	
	 nicolas
						nicolas