diff --git a/services/video-conferencing-service/src/__tests__/helpers.ts b/services/video-conferencing-service/src/__tests__/helpers.ts index 40cb4f47f5..9ab0a2a2df 100644 --- a/services/video-conferencing-service/src/__tests__/helpers.ts +++ b/services/video-conferencing-service/src/__tests__/helpers.ts @@ -17,7 +17,7 @@ import {VonageEnums} from '../enums/video-chat.enum'; import moment from 'moment'; import {sinon} from '@loopback/testlab'; -const meetingLink = 'dummy-meeting-link'; +const meetingLink = 'dummy-meeting-link-id'; const sessionId = 'dummy-session-id'; export function getVideoChatSession( @@ -214,6 +214,17 @@ export function getWebhookPayload( ); } +export const stream = { + id: 'd053fcc8-c681-41d5-8ec2-7a9e1434a21f', + createdAt: 1591599253840, + connection: { + id: 'd053fcc8-c681-41d5-8ec2-7a9e1434a21f', + createdAt: 2470257688144, + data: 'TOKENDATA', + }, + videoType: 'camera', +}; + export function getSessionAttendeesModel() { const data = { sessionId: sessionId, @@ -223,3 +234,20 @@ export function getSessionAttendeesModel() { }; return new SessionAttendees(data); } + +export function getAttendeesList() { + return [ + new SessionAttendees({ + sessionId: sessionId, + attendee: 'User1', + createdOn: getDate('July 01, 2019 00:00:00'), + isDeleted: false, + }), + new SessionAttendees({ + sessionId: sessionId, + attendee: 'User2', + createdOn: getDate('July 01, 2019 00:00:00'), + isDeleted: false, + }), + ]; +} diff --git a/services/video-conferencing-service/src/__tests__/unit/controllers/video-chat-session.controller.unit.ts b/services/video-conferencing-service/src/__tests__/unit/controllers/video-chat-session.controller.unit.ts index 9ced594657..e91774b621 100644 --- a/services/video-conferencing-service/src/__tests__/unit/controllers/video-chat-session.controller.unit.ts +++ b/services/video-conferencing-service/src/__tests__/unit/controllers/video-chat-session.controller.unit.ts @@ -26,6 +26,8 @@ import { getVideoChatSession, getWebhookPayload, setUpMockProvider, + getAttendeesList, + stream, } from '../../helpers'; describe('Session APIs', () => { @@ -278,14 +280,23 @@ describe('Session APIs', () => { sinon.assert.calledOnce(auidtLogCreate); }); - it('updates the attendee for event connectionCreated when attendee re-connects', async () => { + it('updates the metaData and isDeleted status for event connectionCreated if the attendee already exists', async () => { + setUp({}); + const webhookPayload = getWebhookPayload({}); + const findOne = sessionAttendeesRepo.stubs.findOne; + findOne.resolves(getSessionAttendeesModel()); + const updateById = sessionAttendeesRepo.stubs.updateById; + updateById.resolves(); + await controller.checkWebhookPayload(webhookPayload); + sinon.assert.calledOnce(updateById); + sinon.assert.calledOnce(auidtLogCreate); + }); + + it('updates the metaData and isDeleted status for event connectionDestroyed if the attendee already exists', async () => { setUp({}); const webhookPayload = getWebhookPayload({ - connection: { - id: 'd053fcc8-c681-41d5-8ec2-7a9e1434a21f', - createdAt: 2470257688144, - data: 'TOKENDATA', - }, + event: 'connectionDestroyed', + reason: 'clientDisconnected', }); const findOne = sessionAttendeesRepo.stubs.findOne; findOne.resolves(getSessionAttendeesModel()); @@ -296,17 +307,76 @@ describe('Session APIs', () => { sinon.assert.calledOnce(auidtLogCreate); }); - it('audit logs for any other event', async () => { + it('updates the metaData and isDeleted status for event streamCreated if the attendee already exists', async () => { setUp({}); const webhookPayload = getWebhookPayload({ - event: 'connectionDestroyed', + event: 'streamCreated', + stream: stream, + }); + const findOne = sessionAttendeesRepo.stubs.findOne; + findOne.resolves(getSessionAttendeesModel()); + const updateById = sessionAttendeesRepo.stubs.updateById; + updateById.resolves(); + await controller.checkWebhookPayload(webhookPayload); + sinon.assert.calledOnce(updateById); + sinon.assert.calledOnce(auidtLogCreate); + }); + + it('updates the metaData and isDeleted status for event streamDestroyed if the attendee already exists', async () => { + setUp({}); + const webhookPayload = getWebhookPayload({ + event: 'streamDestroyed', reason: 'clientDisconnected', + stream: stream, }); + const findOne = sessionAttendeesRepo.stubs.findOne; + findOne.resolves(getSessionAttendeesModel()); + const updateById = sessionAttendeesRepo.stubs.updateById; + updateById.resolves(); await controller.checkWebhookPayload(webhookPayload); + sinon.assert.calledOnce(updateById); sinon.assert.calledOnce(auidtLogCreate); }); }); + describe('GET /session/{meetingLinkId}/attendees', () => { + it('returns a list of all attendees given a valid meeting link', async () => { + setUp({}); + const findOne = videoChatSessionRepo.stubs.findOne; + findOne.resolves(getVideoChatSession({})); + const find = sessionAttendeesRepo.stubs.find; + find.resolves(getAttendeesList()); + const result = await controller.getAttendeesList(meetingLinkId, 'false'); + expect(result).to.eql(getAttendeesList()); + sinon.assert.calledWith(findOne, {where: {meetingLink: meetingLinkId}}); + sinon.assert.calledWith(find, {where: {sessionId: 'dummy-session-id'}}); + }); + + it('returns an error if the meeting link does not exist', async () => { + setUp({}); + const findOne = videoChatSessionRepo.stubs.findOne; + findOne.resolves(); + const error = await controller + .getAttendeesList(meetingLinkId, 'false') + .catch(err => err); + expect(error).instanceOf(Error); + }); + + it('returns active attendees only if areActive is true', async () => { + setUp({}); + const findOne = videoChatSessionRepo.stubs.findOne; + findOne.resolves(getVideoChatSession({})); + const find = sessionAttendeesRepo.stubs.find; + find.resolves(getAttendeesList()); + const result = await controller.getAttendeesList(meetingLinkId, 'true'); + expect(result).to.eql(getAttendeesList()); + sinon.assert.calledWith(findOne, {where: {meetingLink: meetingLinkId}}); + sinon.assert.calledWith(find, { + where: {sessionId: 'dummy-session-id', isDeleted: false}, + }); + }); + }); + function setUp(providerStub: Partial) { config = { apiKey: 'dummy', diff --git a/services/video-conferencing-service/src/controllers/video-chat-session.controller.ts b/services/video-conferencing-service/src/controllers/video-chat-session.controller.ts index 955f13f457..d5d6cdc6d1 100644 --- a/services/video-conferencing-service/src/controllers/video-chat-session.controller.ts +++ b/services/video-conferencing-service/src/controllers/video-chat-session.controller.ts @@ -1,6 +1,6 @@ import {inject} from '@loopback/context'; import {repository} from '@loopback/repository'; -import {param, patch, post, requestBody, HttpErrors} from '@loopback/rest'; +import {param, patch, post, requestBody, HttpErrors, get} from '@loopback/rest'; import {authorize} from 'loopback4-authorization'; import { MeetingOptions, @@ -14,7 +14,7 @@ import {PermissionKeys} from '../enums/permission-keys.enum'; import {STATUS_CODE, CONTENT_TYPE} from '@sourceloop/core'; import moment from 'moment'; import cryptoRandomString from 'crypto-random-string'; -import {VideoChatSession} from '../models'; +import {VideoChatSession, SessionAttendees} from '../models'; import { AuditLogsRepository, VideoChatSessionRepository, @@ -275,28 +275,58 @@ export class VideoChatSessionController { sessionId, } = webhookPayload; - if (event === VonageEnums.SessionWebhookEvents.ConnectionCreated) { - const sessionAttendeeDetail = await this.sessionAttendeesRepository.findOne( - { - where: { - attendee: data, - }, + const sessionAttendeeDetail = await this.sessionAttendeesRepository.findOne( + { + where: { + sessionId: sessionId, + attendee: data, }, - ); - if (!sessionAttendeeDetail) { + }, + ); + if (!sessionAttendeeDetail) { + if (event === VonageEnums.SessionWebhookEvents.ConnectionCreated) { await this.sessionAttendeesRepository.create({ sessionId: sessionId, attendee: data, createdOn: new Date(), isDeleted: false, + extMetadata: {webhookPayload: webhookPayload}, }); - } else { + } + } else { + const updatedAttendee = { + modifiedOn: new Date(), + isDeleted: sessionAttendeeDetail.isDeleted, + extMetadata: {webhookPayload: webhookPayload}, + }; + + if (event === VonageEnums.SessionWebhookEvents.ConnectionCreated) { + updatedAttendee.isDeleted = false; await this.sessionAttendeesRepository.updateById( sessionAttendeeDetail.id, - { - modifiedOn: new Date(), - }, + updatedAttendee, + ); + } else if (event === VonageEnums.SessionWebhookEvents.StreamCreated) { + await this.sessionAttendeesRepository.updateById( + sessionAttendeeDetail.id, + updatedAttendee, + ); + } else if (event === VonageEnums.SessionWebhookEvents.StreamDestroyed) { + await this.processStreamDestroyedEvent( + webhookPayload, + sessionAttendeeDetail, + updatedAttendee, ); + } else if ( + event === VonageEnums.SessionWebhookEvents.ConnectionDestroyed + ) { + updatedAttendee.isDeleted = true; + await this.sessionAttendeesRepository.updateById( + sessionAttendeeDetail.id, + updatedAttendee, + ); + } else { + //DO NOTHING } } await this.auditLogRepository.create( @@ -325,4 +355,86 @@ export class VideoChatSessionController { ); } } + + async processStreamDestroyedEvent( + webhookPayload: VonageSessionWebhookPayload, + sessionAttendeeDetail: SessionAttendees, + updatedAttendee: Partial, + ) { + if ( + webhookPayload.reason === 'forceUnpublished' || + webhookPayload.reason === 'mediaStopped' + ) { + await this.sessionAttendeesRepository.updateById( + sessionAttendeeDetail.id, + updatedAttendee, + ); + } else { + updatedAttendee.isDeleted = true; + await this.sessionAttendeesRepository.updateById( + sessionAttendeeDetail.id, + updatedAttendee, + ); + } + } + + @authenticate(STRATEGY.BEARER) + @authorize([PermissionKeys.GetAttendees]) + @get('/session/{meetingLinkId}/attendees', { + parameters: [{name: 'active', schema: {type: 'string'}, in: 'query'}], + responses: { + [STATUS_CODE.OK]: { + content: { + [CONTENT_TYPE.TEXT]: {schema: {type: 'array'}}, + }, + }, + }, + }) + async getAttendeesList( + @param.path.string('meetingLinkId') meetingLinkId: string, + @param.query.string('active') active: string, + ): Promise { + const auditLogPayload = { + action: 'session', + actionType: 'session-attendees-list', + before: {meetingLinkId}, + actedAt: moment().format(), + after: {}, + }; + let errorMessage: string; + + const videoSessionDetail = await this.videoChatSessionRepository.findOne({ + where: { + meetingLink: meetingLinkId, + }, + }); + + if (!videoSessionDetail) { + errorMessage = 'Meeting Not Found'; + auditLogPayload.after = {errorMessage}; + await this.auditLogRepository.create(auditLogPayload); + throw new HttpErrors.NotFound(errorMessage); + } + + let whereFilter = {}; + if (active === 'true') { + whereFilter = { + sessionId: videoSessionDetail?.sessionId, + isDeleted: false, + }; + } else { + whereFilter = { + sessionId: videoSessionDetail?.sessionId, + }; + } + + const sessionAttendeeList = await this.sessionAttendeesRepository.find({ + where: whereFilter, + }); + + auditLogPayload.after = {response: 'get attendees successful'}; + await this.auditLogRepository.create(auditLogPayload); + + return sessionAttendeeList; + } } diff --git a/services/video-conferencing-service/src/enums/permission-keys.enum.ts b/services/video-conferencing-service/src/enums/permission-keys.enum.ts index b888efe3f6..ad452a84d4 100644 --- a/services/video-conferencing-service/src/enums/permission-keys.enum.ts +++ b/services/video-conferencing-service/src/enums/permission-keys.enum.ts @@ -7,4 +7,5 @@ export const enum PermissionKeys { DeleteArchive = 'DeleteMeetingArchive', StopMeeting = 'StopMeeting', SetUploadTarget = 'SetMeetingUploadTarget', + GetAttendees = 'GetMeetingAttendees', }