@jsxc/jsxc
Version:
Real-time XMPP chat application with video calls, file transfer and encrypted communication
215 lines (168 loc) • 6.84 kB
text/typescript
import MessageArchiveManagementPlugin from './Plugin';
import Message from '../../Message';
import { IContact as Contact } from '../../Contact.interface';
import JID from '../../JID';
import UUID from '../../util/UUID';
import Utils from '../../util/Utils';
import Log from '../../util/Log';
import Translation from '../../util/Translation';
import * as Namespace from '../../connection/xmpp/namespace';
import { IMessage, MessageMark } from '@src/Message.interface';
import { IJID } from '@src/JID.interface';
import MultiUserContact from '@src/MultiUserContact';
export default class Archive {
private archiveJid: IJID;
private messageCache: JQuery<HTMLElement>[] = [];
constructor(private plugin: MessageArchiveManagementPlugin, private contact: Contact) {
let jid = contact.isGroupChat() ? contact.getJid() : plugin.getConnection().getJID();
this.archiveJid = new JID(jid.bare);
}
public clear() {
this.setExhausted(false);
this.setFirstResultId(undefined);
}
private getId() {
return this.contact.getJid().bare;
}
private getFirstResultId(): string {
return this.plugin.getStorage().getItem('firstResultId', this.getId()) || '';
}
private setFirstResultId(resultId: string) {
return this.plugin.getStorage().setItem('firstResultId', this.getId(), resultId);
}
public isExhausted(): boolean {
return !!this.plugin.getStorage().getItem('exhausted', this.getId());
}
private setExhausted(exhausted: boolean) {
return this.plugin.getStorage().setItem('exhausted', this.getId(), exhausted);
}
public registerExhaustedHook(hook: (isExhausted) => void) {
let storage = this.plugin.getStorage();
let key = storage.generateKey('exhausted', this.getId());
storage.registerHook(key, hook);
}
public nextMessages() {
if (this.isExhausted()) {
Log.debug('No more archived messages.');
return false;
}
if (this.messageCache.length > 0) {
Log.debug('Ongoing message retrieval');
return false;
}
let queryId = UUID.v4();
this.plugin.addQueryContactRelation(queryId, this.contact);
let firstResultId = this.getFirstResultId();
let endDate: Date;
if (!firstResultId) {
let lastMessage = this.contact.getTranscript().getLastMessage();
if (lastMessage) {
endDate = lastMessage.getStamp();
endDate.setSeconds(endDate.getSeconds() - 1);
} else {
endDate = undefined;
}
}
let connection = this.plugin.getConnection();
this.plugin
.determineServerSupport(this.archiveJid)
.then(version => {
if (!version) {
throw new Error(`Archive JID ${this.archiveJid.full} has no support for MAM.`);
}
let jid = !this.contact.isGroupChat() ? this.contact.getJid() : undefined;
return connection.queryArchive(this.archiveJid, <string>version, queryId, jid, firstResultId, endDate);
})
.then(this.onComplete)
.catch(stanza => {
Log.warn('Error while requesting archive', stanza);
});
}
public onForwardedMessage(forwardedElement: JQuery<HTMLElement>) {
this.messageCache.push(forwardedElement);
}
public async parseForwardedMessage(forwardedElement: JQuery<HTMLElement>): Promise<IMessage> {
let messageElement = forwardedElement.find('message');
let messageId = messageElement.attr('id');
if (messageElement.length !== 1) {
return;
}
let from = new JID(messageElement.attr('from'));
let to = new JID(messageElement.attr('to'));
if (this.archiveJid.bare !== from.bare && this.archiveJid.bare !== to.bare) {
return;
}
let delayElement = forwardedElement.find('delay[xmlns="urn:xmpp:delay"]');
let stamp = delayElement.length > 0 ? new Date(delayElement.attr('stamp')) : new Date();
let plaintextBody = Utils.removeHTML(messageElement.find('> body').text());
let htmlBody = messageElement.find('html body' + Namespace.getFilter('XHTML'));
if (!plaintextBody) {
return;
}
let direction = this.contact.getJid().bare === to.bare ? Message.DIRECTION.OUT : Message.DIRECTION.IN;
let stanzaIdElement = messageElement.find('stanza-id[xmlns="urn:xmpp:sid:0"]');
let originIdElement = messageElement.find('origin-id[xmlns="urn:xmpp:sid:0"]');
let uid =
direction === Message.DIRECTION.OUT && originIdElement.length
? originIdElement.attr('id')
: stanzaIdElement.attr('id');
if (Message.exists(uid)) {
return new Message(uid);
}
let messageProperties = {
uid,
attrId: messageId,
peer: this.contact.getJid(),
direction,
plaintextMessage: plaintextBody,
htmlMessage: htmlBody.html(),
stamp: stamp.getTime(),
mark: MessageMark.transferred,
unread: false,
sender: undefined,
};
if (this.contact.isGroupChat()) {
messageProperties.sender = {
name: from.resource,
};
let contact = <MultiUserContact>this.contact;
messageProperties.direction =
contact.getNickname() === from.resource ? Message.DIRECTION.OUT : Message.DIRECTION.IN;
}
return new Message(messageProperties);
}
public onComplete = async (stanza: Element) => {
let stanzaElement = $(stanza);
let finElement = stanzaElement.find(`fin[xmlns^="urn:xmpp:mam:"]`);
if (finElement.length !== 1) {
Log.warn('No fin element found');
return;
}
let transcript = this.contact.getTranscript();
while (this.messageCache.length > 0) {
let messageElement = this.messageCache.pop();
try {
let message = await this.parseForwardedMessage(messageElement);
transcript.unshiftMessage(message);
} catch (err) {
continue;
}
}
let isArchiveExhausted = finElement.attr('complete') === 'true';
let firstResultId = finElement.find('first').text();
let queryId = finElement.attr('queryid');
if (isArchiveExhausted) {
let archiveExhaustedMessage = new Message({
peer: this.contact.getJid(),
direction: Message.DIRECTION.SYS,
plaintextMessage: Translation.t('Archive_exhausted'),
mark: MessageMark.transferred,
unread: false,
});
transcript.unshiftMessage(archiveExhaustedMessage);
}
this.setExhausted(isArchiveExhausted);
this.setFirstResultId(firstResultId);
this.plugin.removeQueryContactRelation(queryId);
};
}