diff --git a/packages/restapi/src/lib/payloads/helpers.ts b/packages/restapi/src/lib/payloads/helpers.ts index d6bb076f4..449f74962 100644 --- a/packages/restapi/src/lib/payloads/helpers.ts +++ b/packages/restapi/src/lib/payloads/helpers.ts @@ -7,7 +7,7 @@ import { ISendNotificationInputOptions, INotificationPayload, walletType, - VideNotificationRules, + VideoNotificationRules, } from '../types'; import { IDENTITY_TYPE, @@ -212,7 +212,7 @@ export async function getVerificationProof({ wallet?: walletType; pgpPrivateKey?: string; env?: ENV; - rules?:VideNotificationRules; + rules?:VideoNotificationRules; }) { let message = null; let verificationProof = null; diff --git a/packages/restapi/src/lib/payloads/sendNotifications.ts b/packages/restapi/src/lib/payloads/sendNotifications.ts index 482faa3d3..2d6eec58e 100644 --- a/packages/restapi/src/lib/payloads/sendNotifications.ts +++ b/packages/restapi/src/lib/payloads/sendNotifications.ts @@ -185,7 +185,7 @@ export async function sendNotification(options: ISendNotificationInputOptions) { uuid, // for the pgpv2 verfication proof chatId: - rules?.access.data ?? // for backwards compatibilty with 'chatId' param + rules?.access.data.chatId ?? // for backwards compatibilty with 'chatId' param chatId, pgpPrivateKey, }); @@ -231,7 +231,7 @@ export async function sendNotification(options: ISendNotificationInputOptions) { ? { rules: rules ?? { access: { - data: chatId, + data: { chatId }, type: VIDEO_NOTIFICATION_ACCESS_TYPE.PUSH_CHAT, }, }, diff --git a/packages/restapi/src/lib/pushapi/PushAPI.ts b/packages/restapi/src/lib/pushapi/PushAPI.ts index 82e6e4f54..8beea967d 100644 --- a/packages/restapi/src/lib/pushapi/PushAPI.ts +++ b/packages/restapi/src/lib/pushapi/PushAPI.ts @@ -16,6 +16,7 @@ import { STREAM, } from '../pushstream/pushStreamTypes'; import { ALPHA_FEATURE_CONFIG } from '../config'; +import { Video } from './video'; import { isValidCAIP10NFTAddress } from '../helpers'; export class PushAPI { @@ -29,6 +30,8 @@ export class PushAPI { private progressHook?: (progress: ProgressHookType) => void; public chat: Chat; // Public instances to be accessed from outside the class + public video: Video; + public profile: Profile; public encryption: Encryption; private user: User; @@ -86,6 +89,13 @@ export class PushAPI { this.progressHook ); this.user = new User(this.account, this.env); + + this.video = new Video(this.account, + this.env, + this.decryptedPgpPvtKey, + this.signer + ); + this.errors = initializationErrors || []; } // Overloaded initialize method signatures diff --git a/packages/restapi/src/lib/pushapi/pushAPITypes.ts b/packages/restapi/src/lib/pushapi/pushAPITypes.ts index e58757d86..48753dbc4 100644 --- a/packages/restapi/src/lib/pushapi/pushAPITypes.ts +++ b/packages/restapi/src/lib/pushapi/pushAPITypes.ts @@ -1,4 +1,5 @@ import Constants, { ENV } from '../constants'; +import type { PushStream } from '../pushstream/PushStream'; import { ChatStatus, ProgressHookType, Rules } from '../types'; export enum ChatListType { @@ -66,3 +67,12 @@ export interface ParticipantStatus { role: 'admin' | 'member'; participant: boolean; } + +export interface VideoInitializeOptions { + stream: PushStream; + config: { + video?: boolean; + audio?: boolean; + }; + media?: MediaStream; +} diff --git a/packages/restapi/src/lib/pushapi/video.ts b/packages/restapi/src/lib/pushapi/video.ts new file mode 100644 index 000000000..9a1495841 --- /dev/null +++ b/packages/restapi/src/lib/pushapi/video.ts @@ -0,0 +1,146 @@ +import { ENV } from '../constants'; +import CONSTANTS from '../constantsV2'; +import { SignerType, VideoCallData, VideoCallStatus } from '../types'; +import { Signer as PushSigner } from '../helpers'; + +import { Video as VideoV1, initVideoCallData } from '../video/Video'; +import { VideoV2 } from '../video/VideoV2'; +import { VideoInitializeOptions } from './pushAPITypes'; +import { VideoEvent, VideoEventType } from '../pushstream/pushStreamTypes'; +import { produce } from 'immer'; +import { endStream } from '../video/helpers/mediaToggle'; + +export class Video { + constructor( + private account: string, + private env: ENV, + private decryptedPgpPvtKey?: string, + private signer?: SignerType + ) {} + + async initialize( + onChange: (fn: (data: VideoCallData) => VideoCallData) => void, + options: VideoInitializeOptions + ) { + const { stream, config, media } = options; + + if (!this.signer) { + throw new Error('Signer is required for push video'); + } + + if (!this.decryptedPgpPvtKey) { + throw new Error( + 'PushSDK was initialized in readonly mode. Video functionality is not available.' + ); + } + + const chainId = await new PushSigner(this.signer).getChainId(); + + if (!chainId) { + throw new Error('Chain Id not retrievable from signer'); + } + + // Initialize the video instance with the provided options + const videoV1Instance = new VideoV1({ + signer: this.signer!, + chainId, + pgpPrivateKey: this.decryptedPgpPvtKey!, + env: this.env, + setData: onChange, + }); + + // Create the media stream with the provided options + await videoV1Instance.create({ + ...(media && { + stream: media, + }), + ...(config?.audio && { + audio: config.audio, + }), + ...(config?.video && { + video: config.video, + }), + }); + + // Setup video event handlers + stream.on(CONSTANTS.STREAM.VIDEO, (data: VideoEvent) => { + const { + address, + signal, + meta: { rules }, + } = data.peerInfo; + + const chatId = rules.access.data.chatId; + + // If the event is RequestVideo, update the video call 'data' state with the incoming call data + if (data.event === VideoEventType.RequestVideo) { + videoV1Instance.setData((oldData) => { + return produce(oldData, (draft) => { + draft.local.address = this.account; + draft.incoming[0].address = address; + draft.incoming[0].status = VideoCallStatus.RECEIVED; + draft.meta.chatId = chatId!; + draft.meta.initiator.address = address; + draft.meta.initiator.signal = signal; + }); + }); + } + + // Check if the chatId from the incoming video event matches the chatId of the current video instance + if (chatId && chatId === videoV1Instance.data.meta.chatId) { + // If the event is DenyVideo, destroy the local stream & reset the video call data + if (data.event === VideoEventType.DenyVideo) { + // destroy the local stream + if (videoV1Instance.data.local.stream) { + endStream(videoV1Instance.data.local.stream); + } + + videoV1Instance.setData(() => initVideoCallData); + } + + // If the event is ApproveVideo or RetryApproveVideo, connect to the video + if ( + data.event === VideoEventType.ApproveVideo || + data.event === VideoEventType.RetryApproveVideo + ) { + videoV1Instance.connect({ peerAddress: address, signalData: signal }); + } + + // If the event is RetryRequestVideo and the current instance is the initiator, send a request + if ( + data.event === VideoEventType.RetryRequestVideo && + videoV1Instance.isInitiator() + ) { + videoV1Instance.request({ + senderAddress: this.account, + recipientAddress: address, + rules, + retry: true, + }); + } + + // If the event is RetryRequestVideo and the current instance is not the initiator, accept the request + if ( + data.event === VideoEventType.RetryRequestVideo && + !videoV1Instance.isInitiator() + ) { + videoV1Instance.acceptRequest({ + signalData: signal, + senderAddress: this.account, + recipientAddress: address, + rules, + retry: true, + }); + } + } + }); + + // Return an instance of the video v2 class + return new VideoV2({ + videoV1Instance, + account: this.account, + decryptedPgpPvtKey: this.decryptedPgpPvtKey!, + env: this.env, + }); + } +} diff --git a/packages/restapi/src/lib/pushstream/DataModifier.ts b/packages/restapi/src/lib/pushstream/DataModifier.ts index 8b18e8cf1..2c4cb62cd 100644 --- a/packages/restapi/src/lib/pushstream/DataModifier.ts +++ b/packages/restapi/src/lib/pushstream/DataModifier.ts @@ -17,7 +17,13 @@ import { NotificationType, NOTIFICATION, ProposedEventNames, + VideoEventType, + MessageOrigin, + VideoEvent, } from './pushStreamTypes'; +import { VideoCallStatus, VideoPeerInfo } from '../types'; +import { VideoDataType } from '../video'; +import { VIDEO_NOTIFICATION_ACCESS_TYPE } from '../payloads/constants'; export class DataModifier { public static handleChatGroupEvent(data: any, includeRaw = false): any { @@ -388,4 +394,73 @@ export class DataModifier { break; } } + + public static convertToProposedNameForVideo( + currentVideoStatus: VideoCallStatus + ): VideoEventType { + switch (currentVideoStatus) { + case VideoCallStatus.INITIALIZED: + return VideoEventType.RequestVideo; + case VideoCallStatus.RECEIVED: + return VideoEventType.ApproveVideo; + case VideoCallStatus.CONNECTED: + return VideoEventType.ConnectVideo; + case VideoCallStatus.ENDED: + return VideoEventType.DisconnectVideo; + case VideoCallStatus.DISCONNECTED: + return VideoEventType.DenyVideo; + case VideoCallStatus.RETRY_INITIALIZED: + return VideoEventType.RetryRequestVideo; + case VideoCallStatus.RETRY_RECEIVED: + return VideoEventType.RetryApproveVideo; + default: + throw new Error(`Unknown video call status: ${currentVideoStatus}`); + } + } + + public static mapToVideoEvent( + data: any, + origin: MessageOrigin, + includeRaw = false + ): VideoEvent { + const { senderAddress, signalData, status, chatId }: VideoDataType = + JSON.parse(data.payload.data.additionalMeta?.data); + + // To maintain backward compatibility, if the rules object is not present in the payload, + // we create a new rules object with chatId from additionalMeta.data + const rules = data.payload.rules ?? { + access: { + type: VIDEO_NOTIFICATION_ACCESS_TYPE.PUSH_CHAT, + data: { + chatId, + }, + }, + }; + + const peerInfo: VideoPeerInfo = { + address: senderAddress, + signal: signalData, + meta: { + rules, + }, + }; + + const videoEventType: VideoEventType = + DataModifier.convertToProposedNameForVideo(status); + + const videoEvent: VideoEvent = { + event: videoEventType, + origin: origin, + timestamp: data.epoch, + peerInfo, + }; + + if (includeRaw) { + videoEvent.raw = { + verificationProof: data.payload.verificationProof, + }; + } + + return videoEvent; + } } diff --git a/packages/restapi/src/lib/pushstream/PushStream.ts b/packages/restapi/src/lib/pushstream/PushStream.ts index 3f270fc3d..3b944a884 100644 --- a/packages/restapi/src/lib/pushstream/PushStream.ts +++ b/packages/restapi/src/lib/pushstream/PushStream.ts @@ -4,16 +4,18 @@ import { ENV, PACKAGE_BUILD } from '../constants'; import { GroupEventType, MessageEventType, + MessageOrigin, NotificationEventType, PushStreamInitializeProps, STREAM, - EVENTS, + EVENTS } from './pushStreamTypes'; import { DataModifier } from './DataModifier'; import { pCAIP10ToWallet, walletToPCAIP10 } from '../helpers'; import { Chat } from '../pushapi/chat'; import { ProgressHookType, SignerType } from '../types'; import { ALPHA_FEATURE_CONFIG } from '../config'; +import { ADDITIONAL_META_TYPE } from '../payloads'; export class PushStream extends EventEmitter { private pushChatSocket: any; @@ -103,7 +105,8 @@ export class PushStream extends EventEmitter { !this.listen || this.listen.length === 0 || this.listen.includes(STREAM.NOTIF) || - this.listen.includes(STREAM.NOTIF_OPS); + this.listen.includes(STREAM.NOTIF_OPS) || + this.listen.includes(STREAM.VIDEO); let isChatSocketConnected = false; let isNotifSocketConnected = false; @@ -313,16 +316,33 @@ export class PushStream extends EventEmitter { this.pushNotificationSocket.on(EVENTS.USER_FEEDS, (data: any) => { try { - const modifiedData = DataModifier.mapToNotificationEvent( - data, - NotificationEventType.INBOX, - this.account === data.sender ? 'self' : 'other', - this.raw - ); + if ( + data.payload.data.additionalMeta?.type === + `${ADDITIONAL_META_TYPE.PUSH_VIDEO}+1` && + shouldEmit(STREAM.VIDEO) && + this.shouldEmitVideo(data.sender) + ) { + // Video Notification + const modifiedData = DataModifier.mapToVideoEvent( + data, + this.account === data.sender ? MessageOrigin.Self : MessageOrigin.Other, + this.raw + ); - if (this.shouldEmitChannel(modifiedData.from)) { - if (shouldEmit(STREAM.NOTIF)) { - this.emit(STREAM.NOTIF, modifiedData); + this.emit(STREAM.VIDEO, modifiedData); + } else { + // Channel Notification + const modifiedData = DataModifier.mapToNotificationEvent( + data, + NotificationEventType.INBOX, + this.account === data.sender ? 'self' : 'other', + this.raw + ); + + if (this.shouldEmitChannel(modifiedData.from)) { + if (shouldEmit(STREAM.NOTIF)) { + this.emit(STREAM.NOTIF, modifiedData); + } } } } catch (error) { @@ -397,4 +417,15 @@ export class PushStream extends EventEmitter { } return this.options.filter.channels.includes(dataChannelId); } + + private shouldEmitVideo(dataVideoId: string): boolean { + if ( + !this.options.filter?.video || + this.options.filter.video.length === 0 || + this.options.filter.video.includes('*') + ) { + return true; + } + return this.options.filter.video.includes(dataVideoId); + } } diff --git a/packages/restapi/src/lib/pushstream/pushStreamTypes.ts b/packages/restapi/src/lib/pushstream/pushStreamTypes.ts index a25bd1a74..01cf61e7b 100644 --- a/packages/restapi/src/lib/pushstream/pushStreamTypes.ts +++ b/packages/restapi/src/lib/pushstream/pushStreamTypes.ts @@ -1,10 +1,11 @@ -import { Rules } from '../types'; +import { Rules, VideoPeerInfo } from '../types'; import { ENV } from '../constants'; export type PushStreamInitializeProps = { filter?: { channels?: string[]; chats?: string[]; + video?: string[]; }; connection?: { auto?: boolean; @@ -22,6 +23,7 @@ export enum STREAM { NOTIF_OPS = 'STREAM.NOTIF_OPS', CHAT = 'STREAM.CHAT', CHAT_OPS = 'STREAM.CHAT_OPS', + VIDEO = 'STREAM.VIDEO', CONNECT = 'STREAM.CONNECT', DISCONNECT = 'STREAM.DISCONNECT', } @@ -51,6 +53,17 @@ export enum GroupEventType { Remove = 'remove', } +export enum VideoEventType { + RequestVideo = 'video.request', + ApproveVideo = 'video.approve', + DenyVideo = 'video.deny', + ConnectVideo = 'video.connect', + DisconnectVideo = 'video.disconnect', + // retry events + RetryRequestVideo = 'video.retry.request', + RetryApproveVideo = 'video.retry.approve' +} + export enum ProposedEventNames { Message = 'chat.message', Request = 'chat.request', @@ -225,6 +238,14 @@ export interface MessageRawData { previousReference: string; } +export interface VideoEvent { + event: VideoEventType; + origin: MessageOrigin; + timestamp: string; + peerInfo: VideoPeerInfo; + raw?: GroupEventRawData; +} + export enum EVENTS { // Websocket CONNECT = 'connect', diff --git a/packages/restapi/src/lib/types/index.ts b/packages/restapi/src/lib/types/index.ts index fd90638c3..841229adc 100644 --- a/packages/restapi/src/lib/types/index.ts +++ b/packages/restapi/src/lib/types/index.ts @@ -13,6 +13,7 @@ import { ENV, MessageType } from '../constants'; import { EthEncryptedData } from '@metamask/eth-sig-util'; import { Message, MessageObj } from './messageTypes'; export * from './messageTypes'; +export * from './videoTypes'; export type Env = typeof ENV[keyof typeof ENV]; @@ -83,15 +84,17 @@ export type ParsedResponseType = { }; }; -export interface VideNotificationRules { +export interface VideoNotificationRules { access: { type: VIDEO_NOTIFICATION_ACCESS_TYPE; - data: string; + data: { + chatId?: string; + }; }; } // SendNotificationRules can be extended in the future for other use cases -export type SendNotificationRules = VideNotificationRules; +export type SendNotificationRules = VideoNotificationRules; export interface ISendNotificationInputOptions { senderType?: 0 | 1; @@ -778,6 +781,7 @@ export enum VideoCallStatus { RECEIVED, CONNECTED, DISCONNECTED, + ENDED, RETRY_INITIALIZED, RETRY_RECEIVED, } @@ -824,7 +828,7 @@ export type VideoRequestInputOptions = { recipientAddress: string | string[]; /** @deprecated - Use `rules` object instead */ chatId?: string; - rules?: VideNotificationRules; + rules?: VideoNotificationRules; onReceiveMessage?: (message: string) => void; retry?: boolean; details?: { @@ -839,7 +843,7 @@ export type VideoAcceptRequestInputOptions = { recipientAddress: string; /** @deprecated - Use `rules` object instead */ chatId?: string; - rules?: VideNotificationRules; + rules?: VideoNotificationRules; onReceiveMessage?: (message: string) => void; retry?: boolean; details?: { diff --git a/packages/restapi/src/lib/types/videoTypes.ts b/packages/restapi/src/lib/types/videoTypes.ts new file mode 100644 index 000000000..e6f018a2c --- /dev/null +++ b/packages/restapi/src/lib/types/videoTypes.ts @@ -0,0 +1,9 @@ +import { VideoNotificationRules } from "."; + +export type VideoPeerInfo = { + address: string; + signal: any; + meta: { + rules: VideoNotificationRules; + }; +}; diff --git a/packages/restapi/src/lib/video/Video.ts b/packages/restapi/src/lib/video/Video.ts index 380e3c507..66870c62d 100644 --- a/packages/restapi/src/lib/video/Video.ts +++ b/packages/restapi/src/lib/video/Video.ts @@ -34,7 +34,9 @@ import { SPACE_DISCONNECT_TYPE, SPACE_REQUEST_TYPE, VIDEO_CALL_TYPE, + VIDEO_NOTIFICATION_ACCESS_TYPE, } from '../payloads/constants'; +import { validateVideoRules } from './helpers/validateVideoRules'; export const initVideoCallData: VideoCallData = { meta: { @@ -86,7 +88,7 @@ export class Video { [key: string]: any; } = {}; - protected data: VideoCallData; + data: VideoCallData; setData: (fn: (data: VideoCallData) => VideoCallData) => void; constructor({ @@ -171,6 +173,9 @@ export class Video { details, } = options || {}; + // If rules object is passed, validate it + rules && validateVideoRules(rules); + const recipientAddresses = Array.isArray(recipientAddress) ? recipientAddress : [recipientAddress]; @@ -181,7 +186,7 @@ export class Video { this.setData((oldData) => { return produce(oldData, (draft) => { draft.local.address = senderAddress; - draft.meta.chatId = chatId ?? rules!.access.data; + draft.meta.chatId = chatId ?? rules!.access.data.chatId!; draft.meta.initiator.address = senderAddress; const incomingIndex = getIncomingIndexFromAddress( @@ -382,7 +387,7 @@ export class Video { this.setData(() => initVideoCallData); } } - } else if(onReceiveMessage) { + } else if (onReceiveMessage) { onReceiveMessage(data); } }); @@ -424,6 +429,9 @@ export class Video { details, } = options || {}; + // If rules object is passed, validate it + rules && validateVideoRules(rules); + try { // if peerInstance is not null -> acceptRequest/request was called before if (this.peerInstances[recipientAddress]) { @@ -448,7 +456,7 @@ export class Video { this.setData((oldData) => { return produce(oldData, (draft) => { draft.local.address = senderAddress; - draft.meta.chatId = chatId ?? rules!.access.data; + draft.meta.chatId = chatId ?? rules!.access.data.chatId!; draft.meta.initiator.address = senderAddress; const incomingIndex = getIncomingIndexFromAddress( @@ -674,7 +682,7 @@ export class Video { this.setData(() => initVideoCallData); } } - } else if(onReceiveMessage) { + } else if (onReceiveMessage) { onReceiveMessage(data); } }); @@ -749,6 +757,32 @@ export class Video { draft.incoming[incomingIndex].status = VideoCallStatus.CONNECTED; }); }); + + // Notifying the recipient that the video call is now connected + sendVideoCallNotification( + { + signer: this.signer, + chainId: this.chainId, + pgpPrivateKey: this.pgpPrivateKey, + }, + { + senderAddress: this.data.local.address, + recipientAddress: peerAddress + ? peerAddress + : this.data.incoming[0].address, + status: VideoCallStatus.CONNECTED, + rules: { + access: { + type: VIDEO_NOTIFICATION_ACCESS_TYPE.PUSH_CHAT, + data: { + chatId: this.data.meta.chatId, + }, + }, + }, + signalData, + env: this.env, + } + ); } catch (err) { console.error('error in connect', err); } @@ -766,37 +800,43 @@ export class Video { ? getIncomingIndexFromAddress(this.data.incoming, peerAddress) : 0; - if ( - this.data.incoming[incomingIndex].status === VideoCallStatus.CONNECTED - ) { + const isCallConnected = + this.data.incoming[incomingIndex].status === VideoCallStatus.CONNECTED; + + if (isCallConnected) { this.peerInstances[ peerAddress ? peerAddress : this.data.incoming[0].address ]?.send(JSON.stringify({ type: 'endCall', value: true, details })); this.peerInstances[ peerAddress ? peerAddress : this.data.incoming[0].address ]?.destroy(); - } else { - // for disconnecting during status INITIALIZED, RECEIVED, RETRY_INITIALIZED, RETRY_RECEIVED - // send a notif to the other user signaling status = DISCONNECTED - sendVideoCallNotification( - { - signer: this.signer, - chainId: this.chainId, - pgpPrivateKey: this.pgpPrivateKey, - }, - { - senderAddress: this.data.local.address, - recipientAddress: this.data.incoming[incomingIndex].address, - status: VideoCallStatus.DISCONNECTED, - chatId: this.data.meta.chatId, - signalData: null, - env: this.env, - callType: this.callType, - callDetails: details, - } - ); } + /* + * Send a notification to the other user signaling: + * status = ENDED if the call was connected + * status = DISCONNECTED otherwise. + */ + sendVideoCallNotification( + { + signer: this.signer, + chainId: this.chainId, + pgpPrivateKey: this.pgpPrivateKey, + }, + { + senderAddress: this.data.local.address, + recipientAddress: this.data.incoming[incomingIndex].address, + status: isCallConnected + ? VideoCallStatus.ENDED + : VideoCallStatus.DISCONNECTED, + chatId: this.data.meta.chatId, + signalData: null, + env: this.env, + callType: this.callType, + callDetails: details, + } + ); + // destroy the peerInstance this.peerInstances[ peerAddress ? peerAddress : this.data.incoming[0].address diff --git a/packages/restapi/src/lib/video/VideoV2.ts b/packages/restapi/src/lib/video/VideoV2.ts new file mode 100644 index 000000000..cad064e07 --- /dev/null +++ b/packages/restapi/src/lib/video/VideoV2.ts @@ -0,0 +1,209 @@ +import { produce } from 'immer'; +import { chats } from '../chat'; +import { ENV } from '../constants'; +import { + isValidETHAddress, + pCAIP10ToWallet, + walletToPCAIP10, +} from '../helpers'; +import { VIDEO_NOTIFICATION_ACCESS_TYPE } from '../payloads/constants'; +import { + VideoCallStatus, + VideoNotificationRules, + VideoPeerInfo, +} from '../types'; +import { Video as VideoV1 } from './Video'; +import { validatePeerInfo } from './helpers/validatePeerInfo'; + +/** + * VideoV2 class + */ +export class VideoV2 { + private account: string; + private decryptedPgpPvtKey: string; + private env: ENV; + + private videoInstance: VideoV1; + + /** + * VideoV2 constructor + * @param {object} params - The constructor parameters + * @param {VideoV1} params.videoV1Instance - The VideoV1 instance + * @param {string} params.account - The account + * @param {string} params.decryptedPgpPvtKey - The decrypted PGP private key + * @param {ENV} params.env - The environment + */ + constructor({ + videoV1Instance, + account, + decryptedPgpPvtKey, + env, + }: { + videoV1Instance: VideoV1; + account: string; + decryptedPgpPvtKey: string; + env: ENV; + }) { + this.videoInstance = videoV1Instance; + this.account = account; + this.decryptedPgpPvtKey = decryptedPgpPvtKey; + this.env = env; + } + + /** + * Request a video call + * @param {string[]} recipients - The recipients of the video call + * @param {object} options - The options for the video call + * @param {object} options.rules - The rules for the video call + * @param {object} options.rules.access - The access rules for the video call + * @param {string} options.rules.access.type - The type of the video call + * @param {object} options.rules.access.data - The data for the video call + * @param {string} options.rules.access.data.chatId - The chat ID for the video call + */ + async request( + recipients: string[], + options?: { + rules: VideoNotificationRules; + } + ) { + const { rules } = options || {}; + + for (const recipient of recipients) { + if (!isValidETHAddress(recipient)) { + throw new Error('Invalid recipient address found'); + } + } + + if (recipients.length === 0) { + throw new Error( + 'Alteast one recipient address is required for a video call' + ); + } + + if ( + recipients.length > 1 && + rules?.access.type === VIDEO_NOTIFICATION_ACCESS_TYPE.PUSH_CHAT && + !rules.access.data.chatId + ) { + throw new Error( + 'For multiple recipient addresses, chatId is required for a video call' + ); + } + + // If chatId is not passed, find a w2w chat between the addresses and use the chatId from there + let retrievedChatId = ''; + if (!rules?.access.data.chatId) { + let page = 1; + const limit = 30; + while (!retrievedChatId) { + const response = await chats({ + account: this.account, + toDecrypt: true, + pgpPrivateKey: this.decryptedPgpPvtKey, + env: this.env, + page, + limit, + }); + + if (response.length === 0) break; + + response.forEach((chat) => { + if (chat.did === walletToPCAIP10(recipients[0]) && chat.chatId) { + retrievedChatId = chat.chatId; + } + }); + + page++; + } + + if (!retrievedChatId) { + throw new Error( + `ChatId not found between local user (${this.account}) and recipient (${recipients[0]}).` + ); + } + } + + this.videoInstance.setData((oldData) => { + return produce(oldData, (draft: any) => { + draft.local.address = this.account; + draft.incoming = recipients.map((recipient) => ({ + address: pCAIP10ToWallet(recipient), + status: VideoCallStatus.INITIALIZED, + })); + draft.meta.chatId = rules?.access.data.chatId ?? retrievedChatId; + }); + }); + + await this.videoInstance.request({ + senderAddress: pCAIP10ToWallet(this.account), + recipientAddress: recipients.map((recipient) => + pCAIP10ToWallet(recipient) + ), + rules: rules ?? { + access: { + type: VIDEO_NOTIFICATION_ACCESS_TYPE.PUSH_CHAT, + data: { + chatId: retrievedChatId, + }, + }, + }, + }); + } + + /** + * Approve a video call + * @param {VideoPeerInfo} peerInfo - The peer information + */ + async approve(peerInfo: VideoPeerInfo) { + validatePeerInfo(peerInfo); + + const { signal, address, meta } = peerInfo; + + await this.videoInstance.acceptRequest({ + senderAddress: pCAIP10ToWallet(this.account), + recipientAddress: pCAIP10ToWallet(address), + signalData: signal, + rules: meta.rules, + }); + } + + /** + * Deny a video call + * @param {VideoPeerInfo} peerInfo - The peer information + */ + async deny(peerInfo: VideoPeerInfo) { + validatePeerInfo(peerInfo); + + const { address } = peerInfo; + + await this.videoInstance.disconnect({ + peerAddress: pCAIP10ToWallet(address), + }); + } + + /** + * Disconnect from a video call + * @param {string} address - The address to disconnect from + */ + async disconnect(address: string) { + await this.videoInstance.disconnect({ + peerAddress: pCAIP10ToWallet(address), + }); + } + + /** + * Enable or disable media (video, audio) + * @param {object} params - The parameters + * @param {boolean} params.video - The video state + * @param {boolean} params.audio - The audio state + */ + media({ video, audio }: { video?: boolean; audio?: boolean }) { + if (typeof video === 'boolean') { + this.videoInstance.enableVideo({ state: video }); + } + + if (typeof audio === 'boolean') { + this.videoInstance.enableAudio({ state: audio }); + } + } +} diff --git a/packages/restapi/src/lib/video/helpers/sendVideoCallNotification.ts b/packages/restapi/src/lib/video/helpers/sendVideoCallNotification.ts index 4675e5be4..5408a8c6b 100644 --- a/packages/restapi/src/lib/video/helpers/sendVideoCallNotification.ts +++ b/packages/restapi/src/lib/video/helpers/sendVideoCallNotification.ts @@ -13,7 +13,7 @@ import { EnvOptionsType, SignerType, VideoCallStatus, - VideNotificationRules, + VideoNotificationRules, } from '../../types'; interface CallDetailsType { @@ -33,7 +33,7 @@ export interface VideoDataType { interface VideoCallInfoType extends VideoDataType, EnvOptionsType { callType?: VIDEO_CALL_TYPE; - rules?: VideNotificationRules; + rules?: VideoNotificationRules; } interface UserInfoType { @@ -60,7 +60,7 @@ const sendVideoCallNotification = async ( const videoData: VideoDataType = { recipientAddress, senderAddress, - chatId: rules?.access.data ?? chatId, + chatId: rules?.access.data.chatId ?? chatId, signalData, status, callDetails, diff --git a/packages/restapi/src/lib/video/helpers/validatePeerInfo.ts b/packages/restapi/src/lib/video/helpers/validatePeerInfo.ts new file mode 100644 index 000000000..b72f20a4b --- /dev/null +++ b/packages/restapi/src/lib/video/helpers/validatePeerInfo.ts @@ -0,0 +1,22 @@ +import { isValidETHAddress } from '../../helpers'; +import { VIDEO_NOTIFICATION_ACCESS_TYPE } from '../../payloads/constants'; +import { VideoPeerInfo } from '../../types'; + +export const validatePeerInfo = (peerInfo: VideoPeerInfo) => { + const { signal, address, meta } = peerInfo; + + if (!signal) { + throw new Error('Invalid signal data received'); + } + + if (!isValidETHAddress(address)) { + throw new Error('Invalid address received'); + } + + if ( + meta.rules.access.type === VIDEO_NOTIFICATION_ACCESS_TYPE.PUSH_CHAT && + !meta.rules.access.data.chatId + ) { + throw new Error('ChatId not found in meta.rules'); + } +}; diff --git a/packages/restapi/src/lib/video/helpers/validateVideoRules.ts b/packages/restapi/src/lib/video/helpers/validateVideoRules.ts new file mode 100644 index 000000000..bb185fe8a --- /dev/null +++ b/packages/restapi/src/lib/video/helpers/validateVideoRules.ts @@ -0,0 +1,13 @@ +import { VIDEO_NOTIFICATION_ACCESS_TYPE } from '../../payloads/constants'; +import { VideoNotificationRules } from '../../types'; + +export const validateVideoRules = (rules: VideoNotificationRules) => { + if ( + rules.access.type === VIDEO_NOTIFICATION_ACCESS_TYPE.PUSH_CHAT && + (!rules.access.data.chatId || rules.access.data.chatId === '') + ) { + throw new Error( + 'Invalid rules object recieved. For access as Push Chat, chatId is required!' + ); + } +}; diff --git a/packages/restapi/tests/lib/notification/delegate.test.ts b/packages/restapi/tests/lib/notification/delegate.test.ts index 6888d9b08..ae517abe0 100644 --- a/packages/restapi/tests/lib/notification/delegate.test.ts +++ b/packages/restapi/tests/lib/notification/delegate.test.ts @@ -149,9 +149,13 @@ describe('PushAPI.delegate functionality', () => { it('With viem signer: Should add delegate', async () => { // create polygon mumbai provider - const provider = new ethers.providers.JsonRpcProvider( - 'https://rpc-mumbai.maticvigil.com' - ); + const provider = (ethers as any).providers + ? new (ethers as any).providers.JsonRpcProvider( + 'https://rpc-mumbai.maticvigil.com/v1' + ) + : new (ethers as any).JsonRpcProvider( + 'https://rpc-mumbai.maticvigil.com/v1' + ); signer2 = new ethers.Wallet( `0x${process.env['WALLET_PRIVATE_KEY']}`, diff --git a/packages/restapi/tests/lib/video/sendVideoNotification.test.ts b/packages/restapi/tests/lib/video/sendVideoNotification.test.ts index d45055b25..bbc8c73e3 100644 --- a/packages/restapi/tests/lib/video/sendVideoNotification.test.ts +++ b/packages/restapi/tests/lib/video/sendVideoNotification.test.ts @@ -113,7 +113,7 @@ describe('sendNotification functionality for video calls', () => { rules: { access: { type: VIDEO_NOTIFICATION_ACCESS_TYPE.PUSH_CHAT, - data: chatId, + data: { chatId }, }, }, notification: {