From 8cda64df81c53eb97915f38ee4a610d560266ede Mon Sep 17 00:00:00 2001 From: Kshitija Kadam <65657373+Xitija@users.noreply.github.com> Date: Fri, 3 Jan 2025 15:15:26 +0530 Subject: [PATCH 01/12] chore : added log for debugging --- src/common/filters/exception.filter.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/common/filters/exception.filter.ts b/src/common/filters/exception.filter.ts index c4699d1..49f80b0 100644 --- a/src/common/filters/exception.filter.ts +++ b/src/common/filters/exception.filter.ts @@ -23,6 +23,8 @@ export class AllExceptionsFilter implements ExceptionFilter { const response = ctx.getResponse(); const request = ctx.getRequest(); + console.log(exception, 'Filter Exception'); + const status = exception instanceof HttpException ? exception.getStatus() : 500; From 144091e934fe33ceb0bcd76f8f000e9e1af18bf2 Mon Sep 17 00:00:00 2001 From: Kshitija Kadam <65657373+Xitija@users.noreply.github.com> Date: Fri, 3 Jan 2025 19:30:24 +0530 Subject: [PATCH 02/12] feat : added metadata to user attendance --- src/modules/attendance/attendance.service.ts | 86 +++++++++++++++----- 1 file changed, 66 insertions(+), 20 deletions(-) diff --git a/src/modules/attendance/attendance.service.ts b/src/modules/attendance/attendance.service.ts index 095c400..1a0994f 100644 --- a/src/modules/attendance/attendance.service.ts +++ b/src/modules/attendance/attendance.service.ts @@ -63,13 +63,22 @@ export class AttendanceService implements OnModuleInit { markZoomAttendanceDto.zoomMeetingId, ); - // get userids from email list in user service + // get userIds from email list in user service - const userList = await this.getUserIdList(participantEmails); + const userList = await this.getUserIdList(participantEmails.emailIds); + + const userDetailList = []; + const userMap = new Map(userList.map((user) => [user.email, user])); + participantEmails.inMeetingUserDetails.forEach((element) => { + const ele = userMap.get(element.user_email); + if (ele) { + userDetailList.push({ ...ele, ...element }); + } + }); // mark attendance for each user const res = await this.markUsersAttendance( - userList, + userDetailList, markZoomAttendanceDto, userId, ); @@ -79,7 +88,22 @@ export class AttendanceService implements OnModuleInit { .json(APIResponse.success(apiId, res, 'Created')); } - async getUserIdList(emailList: string[]): Promise { + async getUserIdList(emailList: string[]): Promise< + { + userId: string; + username: string; + email: string; + name: string; + role: string; + mobile: string; + createdBy: string; + updatedBy: string; + createdAt: string; + updatedAt: string; + status: string; + total_count: string; + }[] + > { try { const userListResponse = await this.httpService.axiosRef.post( `${this.userServiceUrl}/user/v1/list`, @@ -101,10 +125,10 @@ export class AttendanceService implements OnModuleInit { const userDetails = userListResponse.data.result.getUserDetails; if (!userDetails.length) { - throw new BadRequestException('No users found'); + throw new BadRequestException('No users found in user service'); } - return userDetails.map(({ userId }) => userId); + return userDetails; } catch (e) { if (e.status === 404) { throw new BadRequestException('Service not found'); @@ -114,19 +138,26 @@ export class AttendanceService implements OnModuleInit { } async markUsersAttendance( - userIds: string[], + userDetails: any[], markZoomAttendanceDto: MarkZoomAttendanceDto, loggedInUserId: string, ): Promise { // mark attendance for each user try { - const userAttendance = userIds.map((userId) => ({ - userId, - attendance: 'present', - })); + const userAttendance = userDetails.map( + ({ userId, duration, join_time, leave_time }) => ({ + userId, + attendance: 'present', + metaData: { + duration, + joinTime: join_time, + leaveTime: leave_time, + }, + }), + ); const attendanceMarkResponse = await this.httpService.axiosRef.post( - `${this.attendanceServiceUrl}/api/v1/attendance/bulkAttendance`, + `${this.attendanceServiceUrl}/api/v1/attendance/bulkAttendance?userId=${loggedInUserId}`, { attendanceDate: markZoomAttendanceDto.attendanceDate, contextId: markZoomAttendanceDto.eventId, @@ -160,7 +191,7 @@ export class AttendanceService implements OnModuleInit { async getZoomMeetingParticipantsEmail( zoomMeetingId: string, - ): Promise { + ): Promise<{ emailIds: string[]; inMeetingUserDetails: any[] }> { try { const token = await this.getZoomToken(); @@ -170,17 +201,17 @@ export class AttendanceService implements OnModuleInit { zoomMeetingId, ); - const emailIds = userList - .filter(({ user_email, status }) => { - if (status === 'in_meeting') return user_email; - }) - .map(({ user_email }) => user_email); + const inMeetingUserDetails = userList.filter(({ user_email, status }) => { + if (status === 'in_meeting') return user_email; + }); + + const emailIds = inMeetingUserDetails.map(({ user_email }) => user_email); if (!emailIds.length) { throw new BadRequestException('No participants found for meeting'); } - return emailIds; + return { emailIds, inMeetingUserDetails }; } catch (e) { if (e.status === 404) { throw new BadRequestException('Meeting not found'); @@ -220,7 +251,22 @@ export class AttendanceService implements OnModuleInit { userArray: any[], meetId: string, url = '', - ) { + ): Promise< + { + id: string; + user_id: string; + name: string; + user_email: string; + join_time: string; + leave_time: string; + duration: number; + registrant_id: string; + failover: boolean; + status: string; + groupId: string; + internal_user: boolean; + }[] + > { const headers = { headers: { 'Content-Type': 'application/json', From 1df57b2c4b03a49f0ec53af312c1ac3dc7138e02 Mon Sep 17 00:00:00 2001 From: Kshitija Kadam <65657373+Xitija@users.noreply.github.com> Date: Mon, 6 Jan 2025 21:51:31 +0530 Subject: [PATCH 03/12] chore : removed console log --- src/common/filters/exception.filter.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/common/filters/exception.filter.ts b/src/common/filters/exception.filter.ts index 49f80b0..c4699d1 100644 --- a/src/common/filters/exception.filter.ts +++ b/src/common/filters/exception.filter.ts @@ -23,8 +23,6 @@ export class AllExceptionsFilter implements ExceptionFilter { const response = ctx.getResponse(); const request = ctx.getRequest(); - console.log(exception, 'Filter Exception'); - const status = exception instanceof HttpException ? exception.getStatus() : 500; From 14597951c7e4897d9b2fd7c2e41d1c026c00612c Mon Sep 17 00:00:00 2001 From: Kshitija Kadam <65657373+Xitija@users.noreply.github.com> Date: Tue, 7 Jan 2025 15:33:59 +0530 Subject: [PATCH 04/12] chore : added language constants --- src/common/utils/constants.util.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/common/utils/constants.util.ts b/src/common/utils/constants.util.ts index 5d560de..8938503 100644 --- a/src/common/utils/constants.util.ts +++ b/src/common/utils/constants.util.ts @@ -90,6 +90,11 @@ export const ERROR_MESSAGES = { USERID_INVALID: 'Invalid UserId', USERID_REQUIRED: 'UserId Required', PROVIDE_ONE_USERID_IN_QUERY: 'Please provide userId in query params', + ENVIRONMENT_VARIABLES_MISSING: 'Environment variables missing!', + USERS_NOT_FOUND_IN_SERVICE: 'Users not found in user service', + SERVICE_NOT_FOUND: 'Service not found', + NO_PARTICIPANTS_FOUND: 'No participants found for the meeting', + MEETING_NOT_FOUND: 'Meeting not found', API_REQ_FAILURE: (url: string) => `Error occurred on API Request: ${url}`, DB_QUERY_FAILURE: (url: string) => `Database Query Failed on API: ${url}`, API_FAILURE: (url: string) => `API Failure: ${url}`, @@ -111,6 +116,7 @@ export const SUCCESS_MESSAGES = { EVENT_CREATED_LOG: (url: string) => `Event created with ID: ${url}`, EVENTS_FETCHED_LOG: 'Successfully fetched events', EVENT_UPDATED_LOG: 'Successfully updated events', + ATTENDANCE_MARKED_FOR_ZOOM_MEETING: 'Attendance marked for zoom meeting', }; export const API_ID = { @@ -129,5 +135,5 @@ export const API_ID = { CREATE_EVENT_ATTENDEE_HISTORY_ITEM: 'api.event.attendee.history.item.create', UPDATE_EVENT_ATTENDEE_HISTORY_ITEM: 'api.event.attendee.history.item.update', DELETE_EVENT_ATTENDEE_HISTORY_ITEM: 'api.event.attendee.history.item.delete', - MARK_ZOOM_ATTENDANCE: 'mark.zoom.event.attendance', + MARK_EVENT_ATTENDANCE: 'api.event.mark.attendance', }; From 558ceec6125f2b8c58364cddc3e1ff175ed89fea Mon Sep 17 00:00:00 2001 From: Kshitija Kadam <65657373+Xitija@users.noreply.github.com> Date: Tue, 7 Jan 2025 15:39:30 +0530 Subject: [PATCH 05/12] refactor : added adapter and seperated zoom module --- src/common/utils/types.ts | 15 ++ .../attendance/attendance.controller.ts | 2 +- src/modules/attendance/attendance.module.ts | 9 +- src/modules/attendance/attendance.service.ts | 138 ++++-------------- .../onlineMeeting.adapter.ts | 29 ++++ .../onlineMeeting.locator.ts | 9 ++ .../zoom/zoom.adapter.ts | 106 ++++++++++++++ 7 files changed, 195 insertions(+), 113 deletions(-) create mode 100644 src/online-meeting-adapters/onlineMeeting.adapter.ts create mode 100644 src/online-meeting-adapters/onlineMeeting.locator.ts create mode 100644 src/online-meeting-adapters/zoom/zoom.adapter.ts diff --git a/src/common/utils/types.ts b/src/common/utils/types.ts index 7f3ba8c..6228030 100644 --- a/src/common/utils/types.ts +++ b/src/common/utils/types.ts @@ -77,3 +77,18 @@ export type RecurrencePattern = { value: string; }; }; + +export type ZoomParticipant = { + id: string; + user_id: string; + name: string; + user_email: string; + join_time: string; + leave_time: string; + duration: number; + registrant_id: string; + failover: boolean; + status: string; + groupId: string; + internal_user: boolean; +}; diff --git a/src/modules/attendance/attendance.controller.ts b/src/modules/attendance/attendance.controller.ts index 5eb1718..83240c2 100644 --- a/src/modules/attendance/attendance.controller.ts +++ b/src/modules/attendance/attendance.controller.ts @@ -12,7 +12,7 @@ import { API_ID, ERROR_MESSAGES } from 'src/common/utils/constants.util'; export class EventAttendance { constructor(private readonly attendanceService: AttendanceService) {} - @UseFilters(new AllExceptionsFilter(API_ID.MARK_ZOOM_ATTENDANCE)) + @UseFilters(new AllExceptionsFilter(API_ID.MARK_EVENT_ATTENDANCE)) @Post('/markeventattendance') @ApiBody({ type: MarkZoomAttendanceDto }) @ApiQuery({ diff --git a/src/modules/attendance/attendance.module.ts b/src/modules/attendance/attendance.module.ts index 9a506f6..ca93af1 100644 --- a/src/modules/attendance/attendance.module.ts +++ b/src/modules/attendance/attendance.module.ts @@ -3,10 +3,17 @@ import { EventAttendance } from './attendance.controller'; import { AttendanceService } from './attendance.service'; import { HttpModule } from '@nestjs/axios'; import { ConfigService } from '@nestjs/config'; +import { OnlineMeetingAdapter } from 'src/online-meeting-adapters/onlineMeeting.adapter'; +import { ZoomService } from 'src/online-meeting-adapters/zoom/zoom.adapter'; @Module({ imports: [HttpModule], controllers: [EventAttendance], - providers: [AttendanceService, ConfigService], + providers: [ + AttendanceService, + ConfigService, + OnlineMeetingAdapter, + ZoomService, + ], }) export class AttendanceModule {} diff --git a/src/modules/attendance/attendance.service.ts b/src/modules/attendance/attendance.service.ts index 1a0994f..d764ffb 100644 --- a/src/modules/attendance/attendance.service.ts +++ b/src/modules/attendance/attendance.service.ts @@ -7,11 +7,15 @@ import { 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'; +import { + API_ID, + ERROR_MESSAGES, + SUCCESS_MESSAGES, +} from 'src/common/utils/constants.util'; +import { OnlineMeetingAdapter } from 'src/online-meeting-adapters/onlineMeeting.adapter'; @Injectable() export class AttendanceService implements OnModuleInit { @@ -19,36 +23,24 @@ export class AttendanceService implements OnModuleInit { 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, + private readonly onlineMeetingAdapter: OnlineMeetingAdapter, ) { 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 + !this.attendanceServiceUrl.trim().length ) { - throw new InternalServerErrorException('Environment variables missing!'); + throw new InternalServerErrorException( + ERROR_MESSAGES.ENVIRONMENT_VARIABLES_MISSING, + ); } } @@ -57,7 +49,7 @@ export class AttendanceService implements OnModuleInit { userId: string, response: Response, ) { - const apiId = API_ID.MARK_ZOOM_ATTENDANCE; + const apiId = API_ID.MARK_EVENT_ATTENDANCE; const participantEmails = await this.getZoomMeetingParticipantsEmail( markZoomAttendanceDto.zoomMeetingId, @@ -85,7 +77,13 @@ export class AttendanceService implements OnModuleInit { return response .status(HttpStatus.CREATED) - .json(APIResponse.success(apiId, res, 'Created')); + .json( + APIResponse.success( + apiId, + res, + SUCCESS_MESSAGES.ATTENDANCE_MARKED_FOR_ZOOM_MEETING, + ), + ); } async getUserIdList(emailList: string[]): Promise< @@ -125,13 +123,13 @@ export class AttendanceService implements OnModuleInit { const userDetails = userListResponse.data.result.getUserDetails; if (!userDetails.length) { - throw new BadRequestException('No users found in user service'); + throw new BadRequestException(); } return userDetails; } catch (e) { if (e.status === 404) { - throw new BadRequestException('Service not found'); + throw new BadRequestException(ERROR_MESSAGES.SERVICE_NOT_FOUND); } throw e; } @@ -193,13 +191,11 @@ export class AttendanceService implements OnModuleInit { zoomMeetingId: string, ): Promise<{ emailIds: string[]; inMeetingUserDetails: any[] }> { try { - const token = await this.getZoomToken(); + const token = await this.onlineMeetingAdapter.getAdapter().getToken(); - const userList = await this.getZoomParticipantList( - token, - [], - zoomMeetingId, - ); + const userList = await this.onlineMeetingAdapter + .getAdapter() + .getMeetingParticipantList(token, [], zoomMeetingId, ''); const inMeetingUserDetails = userList.filter(({ user_email, status }) => { if (status === 'in_meeting') return user_email; @@ -208,95 +204,15 @@ export class AttendanceService implements OnModuleInit { const emailIds = inMeetingUserDetails.map(({ user_email }) => user_email); if (!emailIds.length) { - throw new BadRequestException('No participants found for meeting'); + throw new BadRequestException(ERROR_MESSAGES.NO_PARTICIPANTS_FOUND); } return { emailIds, inMeetingUserDetails }; } 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 new BadRequestException(ERROR_MESSAGES.MEETING_NOT_FOUND); } throw e; } } - - async getZoomParticipantList( - token: string, - userArray: any[], - meetId: string, - url = '', - ): Promise< - { - id: string; - user_id: string; - name: string; - user_email: string; - join_time: string; - leave_time: string; - duration: number; - registrant_id: string; - failover: boolean; - status: string; - groupId: string; - internal_user: boolean; - }[] - > { - 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; - } - }); - } } diff --git a/src/online-meeting-adapters/onlineMeeting.adapter.ts b/src/online-meeting-adapters/onlineMeeting.adapter.ts new file mode 100644 index 0000000..d73a60d --- /dev/null +++ b/src/online-meeting-adapters/onlineMeeting.adapter.ts @@ -0,0 +1,29 @@ +import { Injectable } from '@nestjs/common'; +import { IOnlineMeetingLocator } from './onlineMeeting.locator'; +import { ZoomService } from './zoom/zoom.adapter'; +import { ConfigService } from '@nestjs/config'; + +@Injectable() +export class OnlineMeetingAdapter { + private adapterMap: Record; + + constructor( + private zoomProvider: ZoomService, + private readonly configService: ConfigService, + ) { + this.adapterMap = { + zoom: this.zoomProvider, + }; + } + + getAdapter(): IOnlineMeetingLocator { + const source = this.configService.get('ONLINE_MEETING_ADAPTER'); // Default to 'postgres' if undefined + const adapter = this.adapterMap[source]; + + if (!adapter) { + throw new Error(`Invalid adapter source: ${source}`); + } + + return adapter; + } +} diff --git a/src/online-meeting-adapters/onlineMeeting.locator.ts b/src/online-meeting-adapters/onlineMeeting.locator.ts new file mode 100644 index 0000000..2cb7c45 --- /dev/null +++ b/src/online-meeting-adapters/onlineMeeting.locator.ts @@ -0,0 +1,9 @@ +export interface IOnlineMeetingLocator { + getToken: () => Promise; + getMeetingParticipantList: ( + token: string, + userArray: any[], + meetingId: string, + url: string, + ) => Promise; +} diff --git a/src/online-meeting-adapters/zoom/zoom.adapter.ts b/src/online-meeting-adapters/zoom/zoom.adapter.ts new file mode 100644 index 0000000..6be313e --- /dev/null +++ b/src/online-meeting-adapters/zoom/zoom.adapter.ts @@ -0,0 +1,106 @@ +import { BadRequestException, Injectable } from '@nestjs/common'; +import { IOnlineMeetingLocator } from '../onlineMeeting.locator'; +import { ERROR_MESSAGES } from 'src/common/utils/constants.util'; +import { AxiosResponse } from 'axios'; +import { HttpService } from '@nestjs/axios'; +import { ConfigService } from '@nestjs/config'; +import { ZoomParticipant } from 'src/common/utils/types'; + +@Injectable() +export class ZoomService implements IOnlineMeetingLocator { + private readonly accountId: string; + private readonly username: string; + private readonly password: string; + private readonly authUrl: string; + private readonly zoomPastMeetings: string; + + constructor( + private httpService: HttpService, + private readonly configService: ConfigService, + ) { + 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.accountId.trim().length || + !this.username.trim().length || + !this.password.trim().length || + !this.authUrl.trim().length || + !this.zoomPastMeetings.trim().length + ) { + throw new BadRequestException( + ERROR_MESSAGES.ENVIRONMENT_VARIABLES_MISSING, + ); + } + } + + async getToken(): Promise { + 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(ERROR_MESSAGES.SERVICE_NOT_FOUND); + } + throw e; + } + } + + async getMeetingParticipantList( + token: string, + userArray: any[], + meetingId: string, + url: string = '', + ): Promise { + const headers = { + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer ' + token, + }, + }; + + let manualPageSize = 100; + const finalUrl = + `${this.zoomPastMeetings}/${meetingId}/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.getMeetingParticipantList( + token, + retrievedUsersArray, + meetingId, + nextPath, + ); + } else { + return retrievedUsersArray; + } + }); + } +} From c27b67ba4b210c3898297af125c17c88b0b12a69 Mon Sep 17 00:00:00 2001 From: Kshitija Kadam <65657373+Xitija@users.noreply.github.com> Date: Tue, 7 Jan 2025 15:44:37 +0530 Subject: [PATCH 06/12] chore: added sonarcloud suggestions --- src/online-meeting-adapters/onlineMeeting.adapter.ts | 5 +++-- src/online-meeting-adapters/zoom/zoom.adapter.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/online-meeting-adapters/onlineMeeting.adapter.ts b/src/online-meeting-adapters/onlineMeeting.adapter.ts index d73a60d..474a90c 100644 --- a/src/online-meeting-adapters/onlineMeeting.adapter.ts +++ b/src/online-meeting-adapters/onlineMeeting.adapter.ts @@ -5,14 +5,15 @@ import { ConfigService } from '@nestjs/config'; @Injectable() export class OnlineMeetingAdapter { - private adapterMap: Record; + private readonly adapterMap: Record; constructor( - private zoomProvider: ZoomService, + private readonly zoomProvider: ZoomService, private readonly configService: ConfigService, ) { this.adapterMap = { zoom: this.zoomProvider, + // Add more adapters here }; } diff --git a/src/online-meeting-adapters/zoom/zoom.adapter.ts b/src/online-meeting-adapters/zoom/zoom.adapter.ts index 6be313e..9a24954 100644 --- a/src/online-meeting-adapters/zoom/zoom.adapter.ts +++ b/src/online-meeting-adapters/zoom/zoom.adapter.ts @@ -15,7 +15,7 @@ export class ZoomService implements IOnlineMeetingLocator { private readonly zoomPastMeetings: string; constructor( - private httpService: HttpService, + private readonly httpService: HttpService, private readonly configService: ConfigService, ) { this.accountId = this.configService.get('ZOOM_ACCOUNT_ID'); From ac686ea4d8985f2761776a4ffc8e8d9a4903ea6f Mon Sep 17 00:00:00 2001 From: Kshitija Kadam <65657373+Xitija@users.noreply.github.com> Date: Tue, 7 Jan 2025 16:13:08 +0530 Subject: [PATCH 07/12] chore: resolved code rabbit suggestions --- src/common/utils/constants.util.ts | 3 +- src/common/utils/types.ts | 17 +++++++ src/modules/attendance/attendance.service.ts | 23 +++++---- .../onlineMeeting.adapter.ts | 6 ++- .../zoom/zoom.adapter.ts | 48 ++++++++++--------- 5 files changed, 62 insertions(+), 35 deletions(-) diff --git a/src/common/utils/constants.util.ts b/src/common/utils/constants.util.ts index 8938503..1686581 100644 --- a/src/common/utils/constants.util.ts +++ b/src/common/utils/constants.util.ts @@ -95,6 +95,7 @@ export const ERROR_MESSAGES = { SERVICE_NOT_FOUND: 'Service not found', NO_PARTICIPANTS_FOUND: 'No participants found for the meeting', MEETING_NOT_FOUND: 'Meeting not found', + NO_USERS_FOUND: 'No users found in system', API_REQ_FAILURE: (url: string) => `Error occurred on API Request: ${url}`, DB_QUERY_FAILURE: (url: string) => `Database Query Failed on API: ${url}`, API_FAILURE: (url: string) => `API Failure: ${url}`, @@ -116,7 +117,7 @@ export const SUCCESS_MESSAGES = { EVENT_CREATED_LOG: (url: string) => `Event created with ID: ${url}`, EVENTS_FETCHED_LOG: 'Successfully fetched events', EVENT_UPDATED_LOG: 'Successfully updated events', - ATTENDANCE_MARKED_FOR_ZOOM_MEETING: 'Attendance marked for zoom meeting', + ATTENDANCE_MARKED_FOR_MEETING: 'Attendance marked for meeting', }; export const API_ID = { diff --git a/src/common/utils/types.ts b/src/common/utils/types.ts index 6228030..6598c06 100644 --- a/src/common/utils/types.ts +++ b/src/common/utils/types.ts @@ -92,3 +92,20 @@ export type ZoomParticipant = { groupId: string; internal_user: boolean; }; + +export interface AttendanceRecord { + userId: string; + attendance: 'present' | 'absent'; + metaData: { + duration: number; + joinTime: string; + leaveTime: string; + }; +} + +export interface InMeetingUserDetails { + user_email: string; + duration: number; + join_time: string; + leave_time: string; +} diff --git a/src/modules/attendance/attendance.service.ts b/src/modules/attendance/attendance.service.ts index d764ffb..0790e4c 100644 --- a/src/modules/attendance/attendance.service.ts +++ b/src/modules/attendance/attendance.service.ts @@ -16,6 +16,7 @@ import { SUCCESS_MESSAGES, } from 'src/common/utils/constants.util'; import { OnlineMeetingAdapter } from 'src/online-meeting-adapters/onlineMeeting.adapter'; +import { AttendanceRecord, InMeetingUserDetails } from 'src/common/utils/types'; @Injectable() export class AttendanceService implements OnModuleInit { @@ -39,7 +40,7 @@ export class AttendanceService implements OnModuleInit { !this.attendanceServiceUrl.trim().length ) { throw new InternalServerErrorException( - ERROR_MESSAGES.ENVIRONMENT_VARIABLES_MISSING, + `${ERROR_MESSAGES.ENVIRONMENT_VARIABLES_MISSING}: USER_SERVICE, ATTENDANCE_SERVICE`, ); } } @@ -61,12 +62,14 @@ export class AttendanceService implements OnModuleInit { const userDetailList = []; const userMap = new Map(userList.map((user) => [user.email, user])); - participantEmails.inMeetingUserDetails.forEach((element) => { - const ele = userMap.get(element.user_email); - if (ele) { - userDetailList.push({ ...ele, ...element }); - } - }); + participantEmails.inMeetingUserDetails.forEach( + (element: InMeetingUserDetails) => { + const ele = userMap.get(element.user_email); + if (ele) { + userDetailList.push({ ...ele, ...element }); + } + }, + ); // mark attendance for each user const res = await this.markUsersAttendance( @@ -81,7 +84,7 @@ export class AttendanceService implements OnModuleInit { APIResponse.success( apiId, res, - SUCCESS_MESSAGES.ATTENDANCE_MARKED_FOR_ZOOM_MEETING, + SUCCESS_MESSAGES.ATTENDANCE_MARKED_FOR_MEETING, ), ); } @@ -123,7 +126,7 @@ export class AttendanceService implements OnModuleInit { const userDetails = userListResponse.data.result.getUserDetails; if (!userDetails.length) { - throw new BadRequestException(); + throw new BadRequestException(ERROR_MESSAGES.NO_USERS_FOUND); } return userDetails; @@ -142,7 +145,7 @@ export class AttendanceService implements OnModuleInit { ): Promise { // mark attendance for each user try { - const userAttendance = userDetails.map( + const userAttendance: AttendanceRecord[] = userDetails.map( ({ userId, duration, join_time, leave_time }) => ({ userId, attendance: 'present', diff --git a/src/online-meeting-adapters/onlineMeeting.adapter.ts b/src/online-meeting-adapters/onlineMeeting.adapter.ts index 474a90c..c2de990 100644 --- a/src/online-meeting-adapters/onlineMeeting.adapter.ts +++ b/src/online-meeting-adapters/onlineMeeting.adapter.ts @@ -18,11 +18,13 @@ export class OnlineMeetingAdapter { } getAdapter(): IOnlineMeetingLocator { - const source = this.configService.get('ONLINE_MEETING_ADAPTER'); // Default to 'postgres' if undefined + const source = this.configService.get('ONLINE_MEETING_ADAPTER'); const adapter = this.adapterMap[source]; if (!adapter) { - throw new Error(`Invalid adapter source: ${source}`); + throw new Error( + `Invalid online meeting adapter: '${source}'. Supported adapters: ${Object.keys(this.adapterMap).join(', ')}`, + ); } return adapter; diff --git a/src/online-meeting-adapters/zoom/zoom.adapter.ts b/src/online-meeting-adapters/zoom/zoom.adapter.ts index 9a24954..fb8528a 100644 --- a/src/online-meeting-adapters/zoom/zoom.adapter.ts +++ b/src/online-meeting-adapters/zoom/zoom.adapter.ts @@ -1,4 +1,8 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { + BadRequestException, + Injectable, + InternalServerErrorException, +} from '@nestjs/common'; import { IOnlineMeetingLocator } from '../onlineMeeting.locator'; import { ERROR_MESSAGES } from 'src/common/utils/constants.util'; import { AxiosResponse } from 'axios'; @@ -33,7 +37,7 @@ export class ZoomService implements IOnlineMeetingLocator { !this.authUrl.trim().length || !this.zoomPastMeetings.trim().length ) { - throw new BadRequestException( + throw new InternalServerErrorException( ERROR_MESSAGES.ENVIRONMENT_VARIABLES_MISSING, ); } @@ -58,7 +62,7 @@ export class ZoomService implements IOnlineMeetingLocator { return tokenResponse.data.access_token; } catch (e) { - if (e.status === 404) { + if (e.response && e.response.status === 404) { throw new BadRequestException(ERROR_MESSAGES.SERVICE_NOT_FOUND); } throw e; @@ -67,7 +71,7 @@ export class ZoomService implements IOnlineMeetingLocator { async getMeetingParticipantList( token: string, - userArray: any[], + userArray: ZoomParticipant[], meetingId: string, url: string = '', ): Promise { @@ -83,24 +87,24 @@ export class ZoomService implements IOnlineMeetingLocator { `${this.zoomPastMeetings}/${meetingId}/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}`; + try { + const response = await this.httpService.axiosRef.get(finalUrl, headers); - return this.getMeetingParticipantList( - token, - retrievedUsersArray, - meetingId, - nextPath, - ); - } else { - return retrievedUsersArray; - } - }); + const retrievedUsersArray = userArray.concat(response.data.participants); + if (response.data.next_page_token) { + let nextPath = `&next_page_token=${response.data.next_page_token}`; + + return await this.getMeetingParticipantList( + token, + retrievedUsersArray, + meetingId, + nextPath, + ); + } else { + return retrievedUsersArray; + } + } catch (e) { + throw e; + } } } From 09c8ada50eb167e7206e107952045388f9487c15 Mon Sep 17 00:00:00 2001 From: Kshitija Kadam <65657373+Xitija@users.noreply.github.com> Date: Tue, 7 Jan 2025 16:18:19 +0530 Subject: [PATCH 08/12] chore : minor changes --- .../zoom/zoom.adapter.ts | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/src/online-meeting-adapters/zoom/zoom.adapter.ts b/src/online-meeting-adapters/zoom/zoom.adapter.ts index fb8528a..92339b5 100644 --- a/src/online-meeting-adapters/zoom/zoom.adapter.ts +++ b/src/online-meeting-adapters/zoom/zoom.adapter.ts @@ -87,24 +87,20 @@ export class ZoomService implements IOnlineMeetingLocator { `${this.zoomPastMeetings}/${meetingId}/participants?page_size=${manualPageSize}` + url; - try { - const response = await this.httpService.axiosRef.get(finalUrl, headers); + const response = await this.httpService.axiosRef.get(finalUrl, headers); - const retrievedUsersArray = userArray.concat(response.data.participants); - if (response.data.next_page_token) { - let nextPath = `&next_page_token=${response.data.next_page_token}`; + const retrievedUsersArray = userArray.concat(response.data.participants); + if (response.data.next_page_token) { + let nextPath = `&next_page_token=${response.data.next_page_token}`; - return await this.getMeetingParticipantList( - token, - retrievedUsersArray, - meetingId, - nextPath, - ); - } else { - return retrievedUsersArray; - } - } catch (e) { - throw e; + return await this.getMeetingParticipantList( + token, + retrievedUsersArray, + meetingId, + nextPath, + ); + } else { + return retrievedUsersArray; } } } From 22a3f103b1bc5d41702ca5fc44053ef9d626dc0a Mon Sep 17 00:00:00 2001 From: Kshitija Kadam <65657373+Xitija@users.noreply.github.com> Date: Mon, 13 Jan 2025 15:26:25 +0530 Subject: [PATCH 09/12] refactor : added modular code for zoom attendance --- src/common/utils/constants.util.ts | 1 + src/common/utils/types.ts | 26 +++- .../attendance/attendance.controller.ts | 20 ++- src/modules/attendance/attendance.module.ts | 4 +- src/modules/attendance/attendance.service.ts | 124 ++++++------------ ...ttendance.dto.ts => MarkAttendance.dto.ts} | 6 +- .../onlineMeeting.locator.ts | 11 ++ .../zoom/zoom.adapter.ts | 73 ++++++++++- 8 files changed, 169 insertions(+), 96 deletions(-) rename src/modules/attendance/dto/{MarkZoomAttendance.dto.ts => MarkAttendance.dto.ts} (91%) diff --git a/src/common/utils/constants.util.ts b/src/common/utils/constants.util.ts index 1686581..68391aa 100644 --- a/src/common/utils/constants.util.ts +++ b/src/common/utils/constants.util.ts @@ -96,6 +96,7 @@ export const ERROR_MESSAGES = { NO_PARTICIPANTS_FOUND: 'No participants found for the meeting', MEETING_NOT_FOUND: 'Meeting not found', NO_USERS_FOUND: 'No users found in system', + EVENT_DOES_NOT_EXIST: 'Event does not exist', API_REQ_FAILURE: (url: string) => `Error occurred on API Request: ${url}`, DB_QUERY_FAILURE: (url: string) => `Database Query Failed on API: ${url}`, API_FAILURE: (url: string) => `API Failure: ${url}`, diff --git a/src/common/utils/types.ts b/src/common/utils/types.ts index 6598c06..1f8df4b 100644 --- a/src/common/utils/types.ts +++ b/src/common/utils/types.ts @@ -93,6 +93,30 @@ export type ZoomParticipant = { internal_user: boolean; }; +export type GMeetParticipant = { + name: string; + participant: string; + startTime: string; + endTime: string; + duration: string; + metadata: any; +}; + +export type UserDetails = { + userId: string; + username: string; + email: string; + name: string; + role: string; + mobile: string; + createdBy: string; + updatedBy: string; + createdAt: string; + updatedAt: string; + status: string; + total_count: string; +}; + export interface AttendanceRecord { userId: string; attendance: 'present' | 'absent'; @@ -103,7 +127,7 @@ export interface AttendanceRecord { }; } -export interface InMeetingUserDetails { +export interface InZoomMeetingUserDetails { user_email: string; duration: number; join_time: string; diff --git a/src/modules/attendance/attendance.controller.ts b/src/modules/attendance/attendance.controller.ts index 83240c2..55473d1 100644 --- a/src/modules/attendance/attendance.controller.ts +++ b/src/modules/attendance/attendance.controller.ts @@ -1,9 +1,18 @@ -import { Body, Controller, Post, Res, Req, UseFilters } from '@nestjs/common'; +import { + Body, + Controller, + Post, + Res, + Req, + UseFilters, + UsePipes, + ValidationPipe, +} 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 { MarkMeetingAttendanceDto } from './dto/MarkAttendance.dto'; import { checkValidUserId } from 'src/common/utils/functions.util'; import { API_ID, ERROR_MESSAGES } from 'src/common/utils/constants.util'; @@ -14,20 +23,21 @@ export class EventAttendance { @UseFilters(new AllExceptionsFilter(API_ID.MARK_EVENT_ATTENDANCE)) @Post('/markeventattendance') - @ApiBody({ type: MarkZoomAttendanceDto }) + @ApiBody({ type: MarkMeetingAttendanceDto }) @ApiQuery({ name: 'userId', required: true, description: ERROR_MESSAGES.USERID_REQUIRED, example: '123e4567-e89b-12d3-a456-426614174000', }) + @UsePipes(new ValidationPipe({ transform: true })) async markEventAttendance( - @Body() markZoomAttendanceDto: MarkZoomAttendanceDto, + @Body() markZoomAttendanceDto: MarkMeetingAttendanceDto, @Res() response: Response, @Req() request: Request, ): Promise { const userId: string = checkValidUserId(request.query?.userId); - return this.attendanceService.markAttendanceForZoomMeetingParticipants( + return this.attendanceService.markAttendanceForMeetingParticipants( markZoomAttendanceDto, userId, response, diff --git a/src/modules/attendance/attendance.module.ts b/src/modules/attendance/attendance.module.ts index ca93af1..34498d9 100644 --- a/src/modules/attendance/attendance.module.ts +++ b/src/modules/attendance/attendance.module.ts @@ -5,9 +5,11 @@ import { HttpModule } from '@nestjs/axios'; import { ConfigService } from '@nestjs/config'; import { OnlineMeetingAdapter } from 'src/online-meeting-adapters/onlineMeeting.adapter'; import { ZoomService } from 'src/online-meeting-adapters/zoom/zoom.adapter'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { EventRepetition } from '../event/entities/eventRepetition.entity'; @Module({ - imports: [HttpModule], + imports: [HttpModule, TypeOrmModule.forFeature([EventRepetition])], controllers: [EventAttendance], providers: [ AttendanceService, diff --git a/src/modules/attendance/attendance.service.ts b/src/modules/attendance/attendance.service.ts index 0790e4c..423cafc 100644 --- a/src/modules/attendance/attendance.service.ts +++ b/src/modules/attendance/attendance.service.ts @@ -9,14 +9,17 @@ import { import { ConfigService } from '@nestjs/config'; import { Response } from 'express'; import APIResponse from 'src/common/utils/response'; -import { MarkZoomAttendanceDto } from './dto/MarkZoomAttendance.dto'; +import { MarkMeetingAttendanceDto } from './dto/MarkAttendance.dto'; import { API_ID, ERROR_MESSAGES, SUCCESS_MESSAGES, } from 'src/common/utils/constants.util'; import { OnlineMeetingAdapter } from 'src/online-meeting-adapters/onlineMeeting.adapter'; -import { AttendanceRecord, InMeetingUserDetails } from 'src/common/utils/types'; +import { AttendanceRecord, UserDetails } from 'src/common/utils/types'; +import { EventRepetition } from '../event/entities/eventRepetition.entity'; +import { Not, Repository } from 'typeorm'; +import { InjectRepository } from '@nestjs/typeorm'; @Injectable() export class AttendanceService implements OnModuleInit { @@ -26,6 +29,8 @@ export class AttendanceService implements OnModuleInit { private readonly attendanceServiceUrl: string; constructor( + @InjectRepository(EventRepetition) + private readonly eventRepetitionRepository: Repository, private readonly httpService: HttpService, private readonly configService: ConfigService, private readonly onlineMeetingAdapter: OnlineMeetingAdapter, @@ -45,36 +50,47 @@ export class AttendanceService implements OnModuleInit { } } - async markAttendanceForZoomMeetingParticipants( - markZoomAttendanceDto: MarkZoomAttendanceDto, + async markAttendanceForMeetingParticipants( + markMeetingAttendanceDto: MarkMeetingAttendanceDto, userId: string, response: Response, ) { const apiId = API_ID.MARK_EVENT_ATTENDANCE; - const participantEmails = await this.getZoomMeetingParticipantsEmail( - markZoomAttendanceDto.zoomMeetingId, - ); + // check event exists + const eventRepetition = await this.eventRepetitionRepository.findOne({ + where: { + eventRepetitionId: markMeetingAttendanceDto.eventRepetitionId, + eventDetail: { + status: Not('archived'), + }, + }, + }); - // get userIds from email list in user service + if (!eventRepetition) { + throw new BadRequestException(ERROR_MESSAGES.EVENT_DOES_NOT_EXIST); + } - const userList = await this.getUserIdList(participantEmails.emailIds); + const participantEmails = await this.onlineMeetingAdapter + .getAdapter() + .getMeetingParticipantsEmail(markMeetingAttendanceDto.meetingId); - const userDetailList = []; - const userMap = new Map(userList.map((user) => [user.email, user])); - participantEmails.inMeetingUserDetails.forEach( - (element: InMeetingUserDetails) => { - const ele = userMap.get(element.user_email); - if (ele) { - userDetailList.push({ ...ele, ...element }); - } - }, + // get userIds from email list in user service + const userList: UserDetails[] = await this.getUserIdList( + participantEmails.emailIds, ); + const userDetailList = this.onlineMeetingAdapter + .getAdapter() + .getParticipantAttendance( + userList, + participantEmails.inMeetingUserDetails, + ); + // mark attendance for each user const res = await this.markUsersAttendance( userDetailList, - markZoomAttendanceDto, + markMeetingAttendanceDto, userId, ); @@ -89,22 +105,7 @@ export class AttendanceService implements OnModuleInit { ); } - async getUserIdList(emailList: string[]): Promise< - { - userId: string; - username: string; - email: string; - name: string; - role: string; - mobile: string; - createdBy: string; - updatedBy: string; - createdAt: string; - updatedAt: string; - status: string; - total_count: string; - }[] - > { + async getUserIdList(emailList: string[]): Promise { try { const userListResponse = await this.httpService.axiosRef.post( `${this.userServiceUrl}/user/v1/list`, @@ -139,37 +140,25 @@ export class AttendanceService implements OnModuleInit { } async markUsersAttendance( - userDetails: any[], - markZoomAttendanceDto: MarkZoomAttendanceDto, + userAttendance: AttendanceRecord[], + markMeetingAttendanceDto: MarkMeetingAttendanceDto, loggedInUserId: string, ): Promise { // mark attendance for each user try { - const userAttendance: AttendanceRecord[] = userDetails.map( - ({ userId, duration, join_time, leave_time }) => ({ - userId, - attendance: 'present', - metaData: { - duration, - joinTime: join_time, - leaveTime: leave_time, - }, - }), - ); - const attendanceMarkResponse = await this.httpService.axiosRef.post( `${this.attendanceServiceUrl}/api/v1/attendance/bulkAttendance?userId=${loggedInUserId}`, { - attendanceDate: markZoomAttendanceDto.attendanceDate, - contextId: markZoomAttendanceDto.eventId, - scope: markZoomAttendanceDto.scope, + attendanceDate: markMeetingAttendanceDto.attendanceDate, + contextId: markMeetingAttendanceDto.eventRepetitionId, + scope: markMeetingAttendanceDto.scope, context: 'event', userAttendance, }, { headers: { Accept: 'application/json', - tenantid: markZoomAttendanceDto.tenantId, + tenantid: markMeetingAttendanceDto.tenantId, userId: loggedInUserId, }, }, @@ -189,33 +178,4 @@ export class AttendanceService implements OnModuleInit { throw e; } } - - async getZoomMeetingParticipantsEmail( - zoomMeetingId: string, - ): Promise<{ emailIds: string[]; inMeetingUserDetails: any[] }> { - try { - const token = await this.onlineMeetingAdapter.getAdapter().getToken(); - - const userList = await this.onlineMeetingAdapter - .getAdapter() - .getMeetingParticipantList(token, [], zoomMeetingId, ''); - - const inMeetingUserDetails = userList.filter(({ user_email, status }) => { - if (status === 'in_meeting') return user_email; - }); - - const emailIds = inMeetingUserDetails.map(({ user_email }) => user_email); - - if (!emailIds.length) { - throw new BadRequestException(ERROR_MESSAGES.NO_PARTICIPANTS_FOUND); - } - - return { emailIds, inMeetingUserDetails }; - } catch (e) { - if (e.status === 404) { - throw new BadRequestException(ERROR_MESSAGES.MEETING_NOT_FOUND); - } - throw e; - } - } } diff --git a/src/modules/attendance/dto/MarkZoomAttendance.dto.ts b/src/modules/attendance/dto/MarkAttendance.dto.ts similarity index 91% rename from src/modules/attendance/dto/MarkZoomAttendance.dto.ts rename to src/modules/attendance/dto/MarkAttendance.dto.ts index aa19c71..de335ab 100644 --- a/src/modules/attendance/dto/MarkZoomAttendance.dto.ts +++ b/src/modules/attendance/dto/MarkAttendance.dto.ts @@ -1,7 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsNotEmpty, IsString, IsUUID, Matches } from 'class-validator'; -export class MarkZoomAttendanceDto { +export class MarkMeetingAttendanceDto { @ApiProperty({ type: String, description: 'Meeting ID', @@ -9,7 +9,7 @@ export class MarkZoomAttendanceDto { }) @IsString() @IsNotEmpty() - zoomMeetingId: string; + meetingId: string; @ApiProperty({ type: String, @@ -18,7 +18,7 @@ export class MarkZoomAttendanceDto { }) @IsUUID() @IsNotEmpty() - eventId: string; + eventRepetitionId: string; @ApiProperty({ type: String, diff --git a/src/online-meeting-adapters/onlineMeeting.locator.ts b/src/online-meeting-adapters/onlineMeeting.locator.ts index 2cb7c45..1cee8bc 100644 --- a/src/online-meeting-adapters/onlineMeeting.locator.ts +++ b/src/online-meeting-adapters/onlineMeeting.locator.ts @@ -1,3 +1,9 @@ +import { + AttendanceRecord, + InZoomMeetingUserDetails, + UserDetails, +} from 'src/common/utils/types'; + export interface IOnlineMeetingLocator { getToken: () => Promise; getMeetingParticipantList: ( @@ -6,4 +12,9 @@ export interface IOnlineMeetingLocator { meetingId: string, url: string, ) => Promise; + getParticipantAttendance: ( + userList: UserDetails[], + meetingParticipantDetails: InZoomMeetingUserDetails[], + ) => AttendanceRecord[]; + getMeetingParticipantsEmail: (meetingId: string) => Promise; } diff --git a/src/online-meeting-adapters/zoom/zoom.adapter.ts b/src/online-meeting-adapters/zoom/zoom.adapter.ts index 92339b5..e30b70c 100644 --- a/src/online-meeting-adapters/zoom/zoom.adapter.ts +++ b/src/online-meeting-adapters/zoom/zoom.adapter.ts @@ -8,7 +8,12 @@ import { ERROR_MESSAGES } from 'src/common/utils/constants.util'; import { AxiosResponse } from 'axios'; import { HttpService } from '@nestjs/axios'; import { ConfigService } from '@nestjs/config'; -import { ZoomParticipant } from 'src/common/utils/types'; +import { + AttendanceRecord, + InZoomMeetingUserDetails, + UserDetails, + ZoomParticipant, +} from 'src/common/utils/types'; @Injectable() export class ZoomService implements IOnlineMeetingLocator { @@ -72,7 +77,7 @@ export class ZoomService implements IOnlineMeetingLocator { async getMeetingParticipantList( token: string, userArray: ZoomParticipant[], - meetingId: string, + zoomMeetingId: string, url: string = '', ): Promise { const headers = { @@ -84,7 +89,7 @@ export class ZoomService implements IOnlineMeetingLocator { let manualPageSize = 100; const finalUrl = - `${this.zoomPastMeetings}/${meetingId}/participants?page_size=${manualPageSize}` + + `${this.zoomPastMeetings}/${zoomMeetingId}/participants?page_size=${manualPageSize}` + url; const response = await this.httpService.axiosRef.get(finalUrl, headers); @@ -96,11 +101,71 @@ export class ZoomService implements IOnlineMeetingLocator { return await this.getMeetingParticipantList( token, retrievedUsersArray, - meetingId, + zoomMeetingId, nextPath, ); } else { return retrievedUsersArray; } } + + async getMeetingParticipantsEmail( + meetingId: string, + ): Promise<{ emailIds: string[]; inMeetingUserDetails: any[] }> { + try { + const token = await this.getToken(); + + const userList = await this.getMeetingParticipantList( + token, + [], + meetingId, + '', + ); + + const inMeetingUserDetails = userList.filter(({ user_email, status }) => { + if (status === 'in_meeting') return user_email; + }); + + const emailIds = inMeetingUserDetails.map(({ user_email }) => user_email); + + if (!emailIds.length) { + throw new BadRequestException(ERROR_MESSAGES.NO_PARTICIPANTS_FOUND); + } + + return { emailIds, inMeetingUserDetails }; + } catch (e) { + if (e.status === 404) { + throw new BadRequestException(ERROR_MESSAGES.MEETING_NOT_FOUND); + } + throw e; + } + } + + getParticipantAttendance( + userList: UserDetails[], + meetingParticipantDetails: InZoomMeetingUserDetails[], + ): AttendanceRecord[] { + const userDetailList = []; + const userMap = new Map(userList.map((user) => [user.email, user])); + meetingParticipantDetails.forEach( + (participantDetail: InZoomMeetingUserDetails) => { + const userDetailExists = userMap.get(participantDetail.user_email); + if (userDetailExists) { + userDetailList.push({ ...userDetailExists, ...participantDetail }); + } + }, + ); + + return userDetailList.map( + ({ userId, duration, join_time, leave_time }) => ({ + userId, + attendance: 'present', + metaData: { + duration, + joinTime: join_time, + leaveTime: leave_time, + }, + }), + ); + } } From 938b1e146297928ddc7423202d855615333ef38d Mon Sep 17 00:00:00 2001 From: Kshitija Kadam <65657373+Xitija@users.noreply.github.com> Date: Mon, 13 Jan 2025 15:59:41 +0530 Subject: [PATCH 10/12] chore : added logger in attendance service --- src/modules/attendance/attendance.service.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/modules/attendance/attendance.service.ts b/src/modules/attendance/attendance.service.ts index 423cafc..4a97bb3 100644 --- a/src/modules/attendance/attendance.service.ts +++ b/src/modules/attendance/attendance.service.ts @@ -20,6 +20,7 @@ import { AttendanceRecord, UserDetails } from 'src/common/utils/types'; import { EventRepetition } from '../event/entities/eventRepetition.entity'; import { Not, Repository } from 'typeorm'; import { InjectRepository } from '@nestjs/typeorm'; +import { LoggerWinston } from 'src/common/logger/logger.util'; @Injectable() export class AttendanceService implements OnModuleInit { @@ -94,6 +95,12 @@ export class AttendanceService implements OnModuleInit { userId, ); + LoggerWinston.log( + SUCCESS_MESSAGES.ATTENDANCE_MARKED_FOR_MEETING, + apiId, + userId, + ); + return response .status(HttpStatus.CREATED) .json( @@ -106,6 +113,7 @@ export class AttendanceService implements OnModuleInit { } async getUserIdList(emailList: string[]): Promise { + // get userIds for emails provided from user service try { const userListResponse = await this.httpService.axiosRef.post( `${this.userServiceUrl}/user/v1/list`, @@ -144,7 +152,7 @@ export class AttendanceService implements OnModuleInit { markMeetingAttendanceDto: MarkMeetingAttendanceDto, loggedInUserId: string, ): Promise { - // mark attendance for each user + // mark attendance for each user in attendance service try { const attendanceMarkResponse = await this.httpService.axiosRef.post( `${this.attendanceServiceUrl}/api/v1/attendance/bulkAttendance?userId=${loggedInUserId}`, From 06ab16a852cec2885f14feb7f48997acd63f6aab Mon Sep 17 00:00:00 2001 From: Kshitija Kadam <65657373+Xitija@users.noreply.github.com> Date: Mon, 13 Jan 2025 18:30:23 +0530 Subject: [PATCH 11/12] chore : resolved PR comments --- src/common/utils/types.ts | 9 --------- src/online-meeting-adapters/onlineMeeting.locator.ts | 2 +- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/src/common/utils/types.ts b/src/common/utils/types.ts index 1f8df4b..f802ba6 100644 --- a/src/common/utils/types.ts +++ b/src/common/utils/types.ts @@ -93,15 +93,6 @@ export type ZoomParticipant = { internal_user: boolean; }; -export type GMeetParticipant = { - name: string; - participant: string; - startTime: string; - endTime: string; - duration: string; - metadata: any; -}; - export type UserDetails = { userId: string; username: string; diff --git a/src/online-meeting-adapters/onlineMeeting.locator.ts b/src/online-meeting-adapters/onlineMeeting.locator.ts index 1cee8bc..074ede9 100644 --- a/src/online-meeting-adapters/onlineMeeting.locator.ts +++ b/src/online-meeting-adapters/onlineMeeting.locator.ts @@ -14,7 +14,7 @@ export interface IOnlineMeetingLocator { ) => Promise; getParticipantAttendance: ( userList: UserDetails[], - meetingParticipantDetails: InZoomMeetingUserDetails[], + meetingParticipantDetails: any[], ) => AttendanceRecord[]; getMeetingParticipantsEmail: (meetingId: string) => Promise; } From a8c966b997520b3b2ab619fb487b4e2ba64cb812 Mon Sep 17 00:00:00 2001 From: Kshitija Kadam <65657373+Xitija@users.noreply.github.com> Date: Mon, 13 Jan 2025 18:32:40 +0530 Subject: [PATCH 12/12] chore : resolved PR comments --- src/online-meeting-adapters/onlineMeeting.locator.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/online-meeting-adapters/onlineMeeting.locator.ts b/src/online-meeting-adapters/onlineMeeting.locator.ts index 074ede9..245304c 100644 --- a/src/online-meeting-adapters/onlineMeeting.locator.ts +++ b/src/online-meeting-adapters/onlineMeeting.locator.ts @@ -1,8 +1,4 @@ -import { - AttendanceRecord, - InZoomMeetingUserDetails, - UserDetails, -} from 'src/common/utils/types'; +import { AttendanceRecord, UserDetails } from 'src/common/utils/types'; export interface IOnlineMeetingLocator { getToken: () => Promise;