@jsxc/jsxc
Version:
Real-time XMPP chat application with video calls, file transfer and encrypted communication
349 lines (275 loc) • 8.77 kB
text/typescript
import Message from '../Message';
import JID from '../JID';
import * as NS from './xmpp/namespace';
import Log from '../util/Log';
import { Strophe, $iq, $msg, $pres } from '../vendor/Strophe';
import Account from '../Account';
import PEPService from './services/PEP';
import SearchService from './services/Search';
import PubSubService from './services/PubSub';
import MUCService from './services/MUC';
import RosterService from './services/Roster';
import VcardService from './services/Vcard';
import DiscoService from './services/Disco';
export const STANZA_KEY = 'stanza';
export const STANZA_IQ_KEY = 'stanzaIQ';
export const STANZA_JINGLE_KEY = 'stanzaJingle';
enum Presence {
online,
chat,
away,
xa,
dnd,
offline,
}
type ExtensivePresence = { presence: Presence; status: string };
abstract class AbstractConnection {
protected abstract connection;
protected abstract send(stanzaElement: Element);
protected abstract send(stanzaElement: Strophe.Builder);
protected abstract sendIQ(stanzaElement: Element): Promise<Element>;
protected abstract sendIQ(stanzaElement: Strophe.Builder): Promise<Element>;
public abstract registerHandler(
handler: (stanza: string) => boolean,
ns?: string,
name?: string,
type?: string,
id?: string,
from?: string
);
protected jingleHandler;
public abstract getJingleHandler();
protected node = 'https://jsxc.org';
protected services = { pubSub: null, pep: null };
constructor(protected account: Account) {
let discoInfo = this.account.getDiscoInfo();
discoInfo.addIdentity('client', 'web', 'JSXC');
NS.register('VCARD', 'vcard-temp');
NS.register('FORWARD', 'urn:xmpp:forward:0');
}
public getPubSubService = (): PubSubService => {
//@TODO connect? supported?
return this.getService('pubsub', PubSubService);
};
public getPEPService = (): PEPService => {
return this.getService('pep', PEPService);
};
public getSearchService = (): SearchService => {
return this.getService('search', SearchService);
};
public getMUCService = (): MUCService => {
return this.getService('muc', MUCService);
};
public getRosterService = (): RosterService => {
return this.getService('roster', RosterService);
};
public getVcardService = (): VcardService => {
return this.getService('vcard', VcardService);
};
public getDiscoService = (): DiscoService => {
return this.getService('disco', DiscoService);
};
private getService(key: string, Service) {
if (!this.services[key]) {
let self = this;
this.services[key] = new Service(
function () {
return self.send.apply(self, arguments);
},
function () {
return self.sendIQ.apply(self, arguments);
},
this,
this.account
);
}
return this.services[key];
}
public pluginOnlySend(stanzaElement: Element);
public pluginOnlySend(stanzaElement: Strophe.Builder);
public pluginOnlySend(stanzaElement) {
this.send(stanzaElement);
}
public pluginOnlySendIQ(stanzaElement: Element): Promise<Element>;
public pluginOnlySendIQ(stanzaElement: Strophe.Builder): Promise<Element>;
public pluginOnlySendIQ(stanzaElement) {
return this.sendIQ(stanzaElement);
}
public getJID(): JID {
return this.account.getJID();
}
public getServerJID(): JID {
return new JID('', this.getJID().domain, '');
}
public sendMessage(message: Message) {
if (message.getDirection() !== Message.DIRECTION.OUT) {
return;
}
let xmlMsg = $msg({
to: message.getPeer().full,
type: message.getType(),
id: message.getAttrId(),
});
let htmlMessage = this.getMessage(message, message.getEncryptedHtmlMessage, message.getHtmlMessage);
if (htmlMessage) {
xmlMsg
.c('html', {
xmlns: Strophe.NS.XHTML_IM,
})
.c('body', {
xmlns: Strophe.NS.XHTML,
});
for (const node of $(htmlMessage).get()) {
xmlMsg.cnode(node).up();
}
xmlMsg.up().up();
}
let plaintextMessage = this.getMessage(
message,
message.getEncryptedPlaintextMessage,
message.getPlaintextMessage
);
if (plaintextMessage) {
xmlMsg.c('body').t(plaintextMessage).up();
}
xmlMsg
.c('origin-id', {
xmlns: 'urn:xmpp:sid:0',
id: message.getUid(),
})
.up();
let pipe = this.account.getPipe('preSendMessageStanza');
pipe
.run(message, xmlMsg)
.then(([message, xmlMsg]: [Message, Element]) => {
if (message.hasAttachment() && !message.getAttachment().isProcessed()) {
Log.warn('Attachment was not processed');
if (!message.getErrorMessage()) {
message.setErrorMessage('Attachment was not processed');
}
if (!message.getPlaintextMessage()) {
message.aborted();
return;
}
}
this.send(xmlMsg);
message.transferred();
})
.catch(err => {
message.aborted();
Log.warn('Error during preSendMessageStanza pipe:', err);
});
}
private getMessage(message: Message, getEncryptedMessage: () => string, getMessage: () => string): string {
if (message.isEncrypted() && getEncryptedMessage.call(message)) {
return getEncryptedMessage.call(message);
} else if (getMessage.call(message)) {
if (!message.isEncrypted()) {
return getMessage.call(message);
}
Log.warn('This message should be encrypted');
}
}
public async sendPresence(presence?: Presence, statusText?: string) {
let presenceStanza = $pres();
presenceStanza.c('c', this.generateCapsAttributes()).up();
if (typeof presence !== 'undefined' && presence !== Presence.online) {
presenceStanza.c('show').t(Presence[presence]).up();
}
if (statusText) {
presenceStanza.c('status').t(statusText).up();
}
let avatarHash = '';
try {
const avatar = await this.account.getContact().getAvatar();
avatarHash = avatar.getHash();
} catch (err) {
// we don't have an avatar
}
presenceStanza.c('x', { xmlns: 'vcard-temp:x:update' }).c('photo').t(avatarHash).up().up();
Log.debug('Send presence', presenceStanza.toString());
this.send(presenceStanza);
}
public queryArchive(
archive: JID,
version: string,
queryId: string,
contact?: JID,
beforeResultId?: string,
end?: Date
): Promise<Element> {
let iq = $iq({
type: 'set',
to: archive.bare,
});
iq.c('query', {
xmlns: version,
queryid: queryId,
});
iq.c('x', {
xmlns: 'jabber:x:data',
type: 'submit',
});
iq.c('field', {
var: 'FORM_TYPE',
type: 'hidden',
})
.c('value')
.t(version)
.up()
.up();
if (contact) {
iq.c('field', {
var: 'with',
})
.c('value')
.t(contact.bare)
.up()
.up();
}
if (end) {
iq.c('field', {
var: 'end',
})
.c('value')
.t(end.toISOString())
.up()
.up();
}
iq.up()
.c('set', {
xmlns: 'http://jabber.org/protocol/rsm',
})
.c('max')
.t('20')
.up();
if (typeof beforeResultId === 'string' || typeof beforeResultId === 'number') {
iq.c('before').t(beforeResultId);
}
iq.up();
return this.sendIQ(iq);
}
public changePassword(newPassword: string): Promise<Element> {
let iq = $iq({
type: 'set',
});
iq.c('query', {
xmlns: 'jabber:iq:register',
});
iq.c('username').t(this.getJID().node).up();
iq.c('password').t(newPassword);
return this.sendIQ(iq);
}
protected getStorage() {
return this.account.getSessionStorage();
}
private generateCapsAttributes() {
return {
xmlns: NS.get('CAPS'),
hash: 'sha-1',
node: this.node,
ver: this.account.getDiscoInfo().getCapsVersion(),
};
}
}
export { AbstractConnection, Presence, ExtensivePresence };