import type { IMediaCall, IUser, MediaCallContact, MediaCallActorType } from '@rocket.chat/core-typings'; import type { VoipPushNotificationEventType } from '@rocket.chat/media-calls'; import { MediaCalls, Users } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { getPushNotificationType } from './getPushNotificationType'; import { metrics } from '../../../lib/metrics/lib/metrics'; import { Push } from '../../../lib/notifications/push/push'; import PushNotification from '../../../lib/notifications/push-config/lib/PushNotification'; import { getUserAvatarURL } from '../../../lib/utils/getUserAvatarURL'; import { getUserPreference } from '../../../lib/utils/lib/getUserPreference'; import { settings } from '../../../settings'; import { logger } from '../logger'; async function getActorUser(actor: MediaCallContact): Promise | null> { const options = { projection: { name: 1, username: 1, freeSwitchExtension: 1 } }; switch (actor.type) { case 'user': return Users.findOneById(actor.id, options); case 'sip': return Users.findOneByFreeSwitchExtension(actor.id, options); } } async function getActorUserData( actor: MediaCallContact, ): Promise<{ type: MediaCallActorType; id: string; name: string; avatarUrl?: string; username?: string }> { const actorUsername = actor.type === 'user' ? actor.username : undefined; const actorExtension = actor.sipExtension || (actor.type === 'sip' ? actor.id : undefined); const data = { type: actor.type, id: actor.id, name: actor.displayName || actorUsername || actorExtension || '', } as const; const user = await getActorUser(actor); if (user) { const username = user.username || actorUsername; return { ...data, name: user.name || user.username || user.freeSwitchExtension || data.name, ...(username && { username, avatarUrl: getUserAvatarURL(username) }), }; } return { ...data, ...(actorUsername && { username: actorUsername, avatarUrl: getUserAvatarURL(actorUsername) }), }; } async function sendVoipPushNotificationAsync(callId: IMediaCall['_id'], event: VoipPushNotificationEventType): Promise { if (settings.get('Push_enable') !== true || settings.get('VoIP_TeamCollab_Mobile_Ringing_Enabled') !== true) { return; } const call = await MediaCalls.findOneById(callId); if (!call) { logger.error({ msg: 'Failed to send push notification: Media Call not found', callId }); return; } if (call.callee.type !== 'user') { logger.error({ msg: 'Failed to send push notification: Invalid Callee Type', callId }); return; } const { kind, callee: { id: userId }, } = call; // If the call was accepted, we don't need to notify when it ends if (call.acceptedAt && event !== 'answer') { return; } const type = getPushNotificationType(call); // If the state changed before we had a chance to send the incoming call, skip it altogether if (event === 'new' && type !== 'incoming_call') { return; } if (type === 'incoming_call' && event !== 'new') { return; } if (!(await getUserPreference(userId, 'enableMobileRinging'))) { return; } const caller = await getActorUserData(call.caller); metrics.notificationsSent.inc({ notification_type: 'mobile' }); const useVoipToken = type === 'incoming_call'; await Push.send({ useVoipToken, priority: 10, payload: { host: Meteor.absoluteUrl(), hostName: settings.get('Site_Name'), notificationType: 'voip', type, kind, callId: call._id, caller, createdAt: call.createdAt.toISOString(), }, ...(useVoipToken && { apn: { expirationSeconds: 60 } }), userId, notId: PushNotification.getNotificationId(call._id), // We should not send state change notifications to the device where the call was accepted/rejected ...(call.callee.contractId && { skipTokenId: call.callee.contractId }), }); } export function sendVoipPushNotification(callId: IMediaCall['_id'], event: VoipPushNotificationEventType): void { void sendVoipPushNotificationAsync(callId, event).catch((err) => { logger.error({ msg: 'Failed to send VoIP push notification', err, callId, event }); }); }