UNPKG

@jsxc/jsxc

Version:

Real-time XMPP chat application with video calls, file transfer and encrypted communication

249 lines (195 loc) 7.85 kB
import { AbstractPlugin, IMetaData } from '../plugin/AbstractPlugin'; import PluginAPI from '../plugin/PluginAPI'; import Translation from '@util/Translation'; import * as Namespace from '@connection/xmpp/namespace'; import { IContact } from '@src/Contact.interface'; import UUID from '@util/UUID'; import JID from '@src/JID'; import { Call } from '@src/Call'; import { CallState, CallType } from '@src/CallManager'; import { IJID } from '@src/JID.interface'; /** * XEP-0353: Jingle Message Initiation * * @version: 0.3.1 * @see: https://xmpp.org/extensions/xep-0353.html * */ const JMI = 'urn:xmpp:jingle-message:0'; type Actions = 'propose' | 'retract' | 'accept' | 'reject' | 'proceed'; const MIN_VERSION = '4.0.0'; const MAX_VERSION = '99.0.0'; Namespace.register('JINGLE_MESSAGE_INITIATION', JMI); export default class JingleMessageInitiationPlugin extends AbstractPlugin { public static getId(): string { return 'jmi'; } public static getName(): string { return 'Jingle Message Initiation'; } public static getMetaData(): IMetaData { return { description: Translation.t('setting-jmi-enable'), xeps: [ { id: 'XEP-0353', name: 'Jingle Message Initiation', version: '0.3.1', }, ], }; } private calls: { [id: string]: Call } = {}; private pendingOutgoingSessionIds: string[] = []; constructor(pluginAPI: PluginAPI) { super(MIN_VERSION, MAX_VERSION, pluginAPI); this.pluginAPI.addFeature(JMI); this.pluginAPI.getConnection().registerHandler(this.onJingleMessageInitiation, null, 'message', null); this.pluginAPI.addCallProcessor(this.callProcessor); this.pluginAPI.addTerminateCallProcessor(this.terminateCallProcessor); this.pluginAPI .getSessionStorage() .registerHook('*', (value: { action: Actions; jid: string; type: CallType }, _, sessionId) => { if (!value || !value.action || !value.jid || !value.type) { return; } const jid = new JID(value.jid); const action = value.action; const type = value.type; const peer = this.pluginAPI.getContact(jid); if (!peer) { return; } const ownJID = this.pluginAPI.getConnection().getJID(); if (action === 'propose') { this.calls[sessionId] = this.pluginAPI.getCallManager().onIncomingCall(type, sessionId, peer); this.calls[sessionId].getState().then(state => { if (state === CallState.Accepted) { this.sendMessage(ownJID.toBareJID(), sessionId, 'accept'); this.sendMessage(jid, sessionId, 'proceed'); } else if (state === CallState.Declined) { this.sendMessage(ownJID.toBareJID(), sessionId, 'reject'); this.sendMessage(jid, sessionId, 'reject'); } }); } else if (action === 'retract' || (action === 'accept' && jid.full !== ownJID.full)) { if (this.calls[sessionId]) { this.calls[sessionId].abort(); } } }); } private onJingleMessageInitiation = (stanza: string): boolean => { let stanzaElement = $(stanza); let element = stanzaElement.find(`[xmlns="${JMI}"]`); if (element.length !== 1) { return true; } const action = element.prop('tagName')?.toString().toLowerCase(); if (!['propose', 'retract', 'accept', 'reject', 'proceed'].includes(action)) { return true; } const fromAttribute = stanzaElement.attr('from'); const fromJid = new JID(fromAttribute); const sessionId = element.attr('id'); const contact = this.pluginAPI.getContact(fromJid); if (!contact || !sessionId) { return true; } const requestedMedia = element .find('> description[xmlns="urn:xmpp:jingle:apps:rtp:1"]') .map((_, descElement) => $(descElement).attr('media')) .get(); const type: CallType = requestedMedia.includes('video') && requestedMedia.includes('audio') ? 'video' : requestedMedia.includes('audio') ? 'audio' : 'stream'; this.pluginAPI.getSessionStorage().setItem(sessionId, { action, jid: fromJid.full, type, }); return true; }; private callProcessor = async ( contact: IContact, type: 'video' | 'audio' | 'screen', resources: string[], sessionId: string ): Promise<[IContact, 'video' | 'audio' | 'screen', string[], string]> => { let capableResources = await contact.getCapableResources(JMI); if (capableResources.length === 0 || sessionId) { return [contact, type, resources, sessionId]; } sessionId = UUID.v4(); const descriptions = []; if (type === 'video' || type === 'audio') { descriptions.push('audio'); } if (type === 'video') { descriptions.push('video'); } this.pendingOutgoingSessionIds.push(sessionId); this.sendMessage(contact.getJid().toBareJID(), sessionId, 'propose', descriptions); return new Promise(resolve => { const storage = this.pluginAPI.getSessionStorage(); const hook = (data: { action: Actions; jid: string }) => { if (data.action === 'retract') { this.sendMessage(contact.getJid().toBareJID(), sessionId, 'retract'); } else if (data?.action === 'proceed') { resolve([contact, type, [new JID(data.jid).resource], sessionId]); } else if (data?.action === 'reject') { resolve([contact, type, [], sessionId]); } if (data?.action !== 'propose') { storage.removeHook(sessionId, hook); } this.pendingOutgoingSessionIds = this.pendingOutgoingSessionIds.filter(id => id !== sessionId); }; storage.registerHook(sessionId, hook); setTimeout(() => { resolve([contact, type, [], undefined]); }, 30 * 1000); }); }; private terminateCallProcessor = async (sessionId?: string): Promise<[string]> => { const ids = sessionId ? [sessionId] : [...this.pendingOutgoingSessionIds]; const sessionStorage = this.pluginAPI.getSessionStorage(); this.pendingOutgoingSessionIds = sessionId ? this.pendingOutgoingSessionIds.filter(id => id !== sessionId) : []; ids.forEach(id => { sessionStorage.setItem(id, { action: 'retract' }); }); return [sessionId]; }; private sendMessage(jid: IJID, sessionId: string, action: 'propose', descriptions?: ('audio' | 'video')[]): void; private sendMessage(jid: IJID, sessionId: string, action: 'retract' | 'accept' | 'proceed' | 'reject'): void; private sendMessage(jid: IJID, sessionId: string, action, descriptions = []) { let xmlMsg = $msg({ to: jid.full, }).c(action, { xmlns: JMI, id: sessionId, }); if (action === 'propose') { if (descriptions.includes('video') || descriptions.includes('audio')) { xmlMsg .c('description', { xmlns: 'urn:xmpp:jingle:apps:rtp:1', media: 'audio', }) .up(); } if (descriptions.includes('video')) { xmlMsg .c('description', { xmlns: 'urn:xmpp:jingle:apps:rtp:1', media: 'video', }) .up(); } } this.pluginAPI.send(xmlMsg); } }