-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #53 from Xitija/main2
Zoom Attendance integration
- Loading branch information
Showing
9 changed files
with
444 additions
and
48 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import { Body, Controller, Post, Res, Req, UseFilters } from '@nestjs/common'; | ||
import { Response, Request } from 'express'; | ||
import { ApiBody, ApiQuery, ApiTags } from '@nestjs/swagger'; | ||
import { AttendanceService } from './attendance.service'; | ||
import { AllExceptionsFilter } from 'src/common/filters/exception.filter'; | ||
import { MarkZoomAttendanceDto } from './dto/MarkZoomAttendance.dto'; | ||
import { checkValidUserId } from 'src/common/utils/functions.util'; | ||
import { API_ID, ERROR_MESSAGES } from 'src/common/utils/constants.util'; | ||
|
||
@Controller('attendance/v1') | ||
@ApiTags('Event-Attendance') | ||
export class EventAttendance { | ||
constructor(private readonly attendanceService: AttendanceService) {} | ||
|
||
@UseFilters(new AllExceptionsFilter(API_ID.MARK_ZOOM_ATTENDANCE)) | ||
@Post('/markeventattendance') | ||
@ApiBody({ type: MarkZoomAttendanceDto }) | ||
@ApiQuery({ | ||
name: 'userId', | ||
required: true, | ||
description: ERROR_MESSAGES.USERID_REQUIRED, | ||
example: '123e4567-e89b-12d3-a456-426614174000', | ||
}) | ||
async markEventAttendance( | ||
@Body() markZoomAttendanceDto: MarkZoomAttendanceDto, | ||
@Res() response: Response, | ||
@Req() request: Request, | ||
): Promise<Response> { | ||
const userId: string = checkValidUserId(request.query?.userId); | ||
return this.attendanceService.markAttendanceForZoomMeetingParticipants( | ||
markZoomAttendanceDto, | ||
userId, | ||
response, | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import { Module } from '@nestjs/common'; | ||
import { EventAttendance } from './attendance.controller'; | ||
import { AttendanceService } from './attendance.service'; | ||
import { HttpModule } from '@nestjs/axios'; | ||
import { ConfigService } from '@nestjs/config'; | ||
|
||
@Module({ | ||
imports: [HttpModule], | ||
controllers: [EventAttendance], | ||
providers: [AttendanceService, ConfigService], | ||
}) | ||
export class AttendanceModule {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,256 @@ | ||
import { HttpService } from '@nestjs/axios'; | ||
import { | ||
BadRequestException, | ||
HttpStatus, | ||
Injectable, | ||
InternalServerErrorException, | ||
OnModuleInit, | ||
} from '@nestjs/common'; | ||
import { ConfigService } from '@nestjs/config'; | ||
import { AxiosResponse } from 'axios'; | ||
import { Response } from 'express'; | ||
import APIResponse from 'src/common/utils/response'; | ||
import { MarkZoomAttendanceDto } from './dto/MarkZoomAttendance.dto'; | ||
import { API_ID } from 'src/common/utils/constants.util'; | ||
|
||
@Injectable() | ||
export class AttendanceService implements OnModuleInit { | ||
// utilize apis of the attendance service to mark event attendance | ||
|
||
private readonly userServiceUrl: string; | ||
private readonly attendanceServiceUrl: string; | ||
private readonly accountId: string; | ||
private readonly username: string; | ||
private readonly password: string; | ||
private readonly authUrl: string; | ||
private readonly zoomPastMeetings: string; | ||
|
||
constructor( | ||
private readonly httpService: HttpService, | ||
private readonly configService: ConfigService, | ||
) { | ||
this.userServiceUrl = this.configService.get('USER_SERVICE'); | ||
this.attendanceServiceUrl = this.configService.get('ATTENDANCE_SERVICE'); | ||
this.accountId = this.configService.get('ZOOM_ACCOUNT_ID'); | ||
this.username = this.configService.get('ZOOM_USERNAME'); | ||
this.password = this.configService.get('ZOOM_PASSWORD'); | ||
this.authUrl = this.configService.get('ZOOM_AUTH_URL'); | ||
this.zoomPastMeetings = this.configService.get('ZOOM_PAST_MEETINGS'); | ||
} | ||
|
||
onModuleInit() { | ||
if ( | ||
!this.userServiceUrl.trim().length || | ||
!this.attendanceServiceUrl.trim().length || | ||
!this.accountId.trim().length || | ||
!this.username.trim().length || | ||
!this.password.trim().length || | ||
!this.authUrl.trim().length || | ||
!this.zoomPastMeetings.trim().length | ||
) { | ||
throw new InternalServerErrorException('Environment variables missing!'); | ||
} | ||
} | ||
|
||
async markAttendanceForZoomMeetingParticipants( | ||
markZoomAttendanceDto: MarkZoomAttendanceDto, | ||
userId: string, | ||
response: Response, | ||
) { | ||
const apiId = API_ID.MARK_ZOOM_ATTENDANCE; | ||
|
||
const participantEmails = await this.getZoomMeetingParticipantsEmail( | ||
markZoomAttendanceDto.zoomMeetingId, | ||
); | ||
|
||
// get userids from email list in user service | ||
|
||
const userList = await this.getUserIdList(participantEmails); | ||
|
||
// mark attendance for each user | ||
const res = await this.markUsersAttendance( | ||
userList, | ||
markZoomAttendanceDto, | ||
userId, | ||
); | ||
|
||
return response | ||
.status(HttpStatus.CREATED) | ||
.json(APIResponse.success(apiId, res, 'Created')); | ||
} | ||
|
||
async getUserIdList(emailList: string[]): Promise<string[]> { | ||
try { | ||
const userListResponse = await this.httpService.axiosRef.post( | ||
`${this.userServiceUrl}/user/v1/list`, | ||
{ | ||
limit: emailList.length, | ||
offset: 0, | ||
filters: { | ||
email: emailList, | ||
}, | ||
}, | ||
{ | ||
headers: { | ||
Accept: 'application/json', | ||
'Content-Type': 'application/json', | ||
}, | ||
}, | ||
); | ||
|
||
const userDetails = userListResponse.data.result.getUserDetails; | ||
|
||
if (!userDetails.length) { | ||
throw new BadRequestException('No users found'); | ||
} | ||
|
||
return userDetails.map(({ userId }) => userId); | ||
} catch (e) { | ||
if (e.status === 404) { | ||
throw new BadRequestException('Service not found'); | ||
} | ||
throw e; | ||
} | ||
} | ||
|
||
async markUsersAttendance( | ||
userIds: string[], | ||
markZoomAttendanceDto: MarkZoomAttendanceDto, | ||
loggedInUserId: string, | ||
): Promise<any> { | ||
// mark attendance for each user | ||
try { | ||
const userAttendance = userIds.map((userId) => ({ | ||
userId, | ||
attendance: 'present', | ||
})); | ||
|
||
const attendanceMarkResponse = await this.httpService.axiosRef.post( | ||
`${this.attendanceServiceUrl}/api/v1/attendance/bulkAttendance`, | ||
{ | ||
attendanceDate: markZoomAttendanceDto.attendanceDate, | ||
contextId: markZoomAttendanceDto.eventId, | ||
scope: markZoomAttendanceDto.scope, | ||
context: 'event', | ||
userAttendance, | ||
}, | ||
{ | ||
headers: { | ||
Accept: 'application/json', | ||
tenantid: markZoomAttendanceDto.tenantId, | ||
userId: loggedInUserId, | ||
}, | ||
}, | ||
); | ||
|
||
return attendanceMarkResponse.data; | ||
} catch (e) { | ||
if (e.status === 404) { | ||
throw new BadRequestException( | ||
`Service not found ${e?.response?.data?.message}`, | ||
); | ||
} else if (e.status === 400) { | ||
throw new BadRequestException( | ||
`Bad request ${e?.response?.data?.message}`, | ||
); | ||
} | ||
throw e; | ||
} | ||
} | ||
|
||
async getZoomMeetingParticipantsEmail( | ||
zoomMeetingId: string, | ||
): Promise<string[]> { | ||
try { | ||
const token = await this.getZoomToken(); | ||
|
||
const userList = await this.getZoomParticipantList( | ||
token, | ||
[], | ||
zoomMeetingId, | ||
); | ||
|
||
const emailIds = userList | ||
.filter(({ user_email, status }) => { | ||
if (status === 'in_meeting') return user_email; | ||
}) | ||
.map(({ user_email }) => user_email); | ||
|
||
if (!emailIds.length) { | ||
throw new BadRequestException('No participants found for meeting'); | ||
} | ||
|
||
return emailIds; | ||
} catch (e) { | ||
if (e.status === 404) { | ||
throw new BadRequestException('Meeting not found'); | ||
} | ||
throw e; | ||
} | ||
} | ||
|
||
async getZoomToken() { | ||
try { | ||
const auth = | ||
'Basic ' + | ||
Buffer.from(this.username + ':' + this.password).toString('base64'); | ||
|
||
const tokenResponse: AxiosResponse = await this.httpService.axiosRef.post( | ||
`${this.authUrl}?grant_type=account_credentials&account_id=${this.accountId}`, | ||
{}, | ||
{ | ||
headers: { | ||
Accept: 'application/json', | ||
Authorization: auth, | ||
}, | ||
}, | ||
); | ||
|
||
return tokenResponse.data.access_token; | ||
} catch (e) { | ||
if (e.status === 404) { | ||
throw new BadRequestException('Service not found'); | ||
} | ||
throw e; | ||
} | ||
} | ||
|
||
async getZoomParticipantList( | ||
token: string, | ||
userArray: any[], | ||
meetId: string, | ||
url = '', | ||
) { | ||
const headers = { | ||
headers: { | ||
'Content-Type': 'application/json', | ||
Authorization: 'Bearer ' + token, | ||
}, | ||
}; | ||
|
||
let manualPageSize = 100; | ||
const finalUrl = | ||
`${this.zoomPastMeetings}/${meetId}/participants?page_size=${manualPageSize}` + | ||
url; | ||
|
||
return await this.httpService.axiosRef | ||
.get(finalUrl, headers) | ||
.then((response) => { | ||
const retrievedUsersArray = userArray.concat( | ||
response.data.participants, | ||
); | ||
if (response.data.next_page_token) { | ||
let nextPath = `&next_page_token=${response.data.next_page_token}`; | ||
|
||
return this.getZoomParticipantList( | ||
token, | ||
retrievedUsersArray, | ||
meetId, | ||
nextPath, | ||
); | ||
} else { | ||
return retrievedUsersArray; | ||
} | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import { ApiProperty } from '@nestjs/swagger'; | ||
import { IsNotEmpty, IsString, IsUUID, Matches } from 'class-validator'; | ||
|
||
export class MarkZoomAttendanceDto { | ||
@ApiProperty({ | ||
type: String, | ||
description: 'Meeting ID', | ||
example: '1234567890', | ||
}) | ||
@IsString() | ||
@IsNotEmpty() | ||
zoomMeetingId: string; | ||
|
||
@ApiProperty({ | ||
type: String, | ||
description: 'Event ID', | ||
example: '123e4567-e89b-12d3-a456-426614174000', | ||
}) | ||
@IsUUID() | ||
@IsNotEmpty() | ||
eventId: string; | ||
|
||
@ApiProperty({ | ||
type: String, | ||
description: 'The date of the attendance in format yyyy-mm-dd', | ||
}) | ||
@IsNotEmpty() | ||
@Matches(/^\d{4}-\d{2}-\d{2}$/, { | ||
message: 'Please provide a valid date in the format yyyy-mm-dd', | ||
}) | ||
attendanceDate: string; | ||
|
||
@ApiProperty({ | ||
type: String, | ||
description: 'Scope of the attendance', | ||
example: 'self / student', | ||
}) | ||
@IsString() | ||
@IsNotEmpty() | ||
scope: string; | ||
|
||
@ApiProperty({ | ||
type: String, | ||
description: 'Tenant ID', | ||
example: '123e4567-e89b-12d3-a456-426614174000', | ||
}) | ||
@IsUUID() | ||
@IsNotEmpty() | ||
tenantId: string; | ||
} |
Oops, something went wrong.