@jsxc/jsxc
Version:
Real-time XMPP chat application with video calls, file transfer and encrypted communication
471 lines (356 loc) • 12.9 kB
text/typescript
import Storage from './Storage';
import Attachment from './Attachment';
import JID from './JID';
import * as CONST from './CONST';
import Emoticons from './Emoticons';
import IIdentifiable from './Identifiable.interface';
import Client from './Client';
import Utils from './util/Utils';
import { IMessage, DIRECTION, IMessagePayload, MessageMark } from './Message.interface';
import { ContactType } from './Contact.interface';
import PersistentMap from './util/PersistentMap';
import UUID from './util/UUID';
import Pipe from '@util/Pipe';
import { IJID } from './JID.interface';
const ATREGEX = new RegExp('(xmpp:)?(' + CONST.REGEX.JID.source + ')(\\?[^\\s]+\\b)?', 'i');
export default class Message implements IIdentifiable, IMessage {
public static exists(uid: string) {
let data = PersistentMap.getData(Client.getStorage(), uid);
return !!(data && data.attrId);
}
private static formattingPipe = new Pipe();
private static formatText(text: string, direction: DIRECTION, peer: IJID, senderName: string): Promise<string> {
return Message.formattingPipe.run(text, direction, peer, senderName).then(args => args[0]);
}
public static addFormatter(
formatter: (
text: string,
direction: DIRECTION,
peer?: IJID,
senderName?: string
) => Promise<[string, DIRECTION, IJID, string]> | string,
priority?: number
) {
Message.formattingPipe.addProcessor((text: string, direction: DIRECTION, peer: IJID, senderName: string) => {
let returnValue = formatter(text, direction, peer, senderName);
if (typeof returnValue === 'string') {
return Promise.resolve([returnValue, direction, peer, senderName]);
}
return returnValue;
}, priority);
}
private uid: string;
private data: PersistentMap;
private attachment: Attachment;
private replacedBy: IMessage;
private original: IMessage;
public static readonly DIRECTION = DIRECTION;
public static readonly MSGTYPE = ContactType;
private storage: Storage;
constructor(uid: string);
constructor(data: IMessagePayload);
constructor(arg0) {
this.storage = Client.getStorage();
let data;
if (typeof arg0 === 'string' && arg0.length > 0 && arguments.length === 1) {
this.uid = arg0;
} else if (typeof arg0 === 'object' && arg0 !== null) {
data = arg0;
this.uid = data.uid || UUID.v4();
data.attrId = data.attrId || this.uid;
delete data.uid;
}
this.data = new PersistentMap(this.storage, this.uid);
if (data) {
if (data.peer) {
data.peer = data.peer.full;
}
if (data.sender?.jid) {
data.sender.jid = data.sender.jid?.toString();
}
if (data.attachment instanceof Attachment) {
this.attachment = data.attachment;
data.attachment = data.attachment.getUid();
}
this.data.set(
$.extend(
{
unread: true,
mark: MessageMark.pending,
encrypted: null,
forwarded: false,
stamp: new Date().getTime(),
type: ContactType.CHAT,
encryptedHtmlMessage: null,
encryptedPlaintextMessage: null,
},
data
)
);
} else if (!this.data.get('attrId')) {
throw new Error(`Could not load message ${this.uid}`);
}
}
public registerHook(property: string, func: (newValue: any, oldValue: any) => void) {
this.data.registerHook(property, func);
}
public getId() {
// eslint-disable-next-line no-console
console.trace('Deprecated Message.getId called');
return this.getUid();
}
public getUid(): string {
return this.uid;
}
public getAttrId(): string {
return this.data.get('attrId');
}
public delete() {
let attachment = this.getAttachment();
if (attachment) {
attachment.delete();
}
this.data.delete();
this.attachment = undefined;
this.data = undefined;
this.uid = undefined;
}
public getNextId(): string {
return this.data.get('next');
}
public setNext(message: IMessage | string | undefined): void {
let nextId = typeof message === 'string' || typeof message === 'undefined' ? message : message.getUid();
if (this.getNextId() === this.uid) {
// eslint-disable-next-line no-console
console.trace('Loop detected ' + this.uid);
} else {
this.data.set('next', nextId);
}
}
public getCssId(): string {
return this.uid.replace(/:/g, '-');
}
public getDOM(): JQuery<HTMLElement> {
return $('#' + this.getCssId());
}
public getStamp(): Date {
return new Date(this.data.get('stamp'));
}
public getDirection(): DIRECTION {
return this.data.get('direction');
}
public getDirectionString(): string {
return DIRECTION[this.getDirection()].toLowerCase();
}
public isSystem(): boolean {
return this.getDirection() === DIRECTION.SYS;
}
public isIncoming(): boolean {
return this.getDirection() === DIRECTION.IN || this.getDirection() === DIRECTION.PROBABLY_IN;
}
public isOutgoing(): boolean {
return this.getDirection() === DIRECTION.OUT || this.getDirection() === DIRECTION.PROBABLY_OUT;
}
public getAttachment(): Attachment {
if (!this.attachment && this.data.get('attachment')) {
this.attachment = new Attachment(this.data.get('attachment'));
}
return this.attachment;
}
public setAttachment(attachment: Attachment) {
this.attachment = attachment;
this.data.set('attachment', attachment.getUid());
}
public getPeer(): JID {
return new JID(this.data.get('peer'));
}
public getType(): ContactType {
return this.data.get('type');
}
public getTypeString(): string {
return ContactType[this.getType()].toLowerCase();
}
public getHtmlMessage(): string {
return this.data.get('htmlMessage');
}
public setHtmlMessage(htmlMessage: string) {
this.data.set('htmlMessage', htmlMessage);
}
public getEncryptedHtmlMessage(): string {
return this.data.get('encryptedHtmlMessage');
}
public getPlaintextMessage(): string {
return this.data.get('plaintextMessage');
}
public getEncryptedPlaintextMessage(): string {
return this.data.get('encryptedPlaintextMessage');
}
public getSender(): { name: string; jid?: JID } {
let sender = this.data.get('sender');
return {
name: sender?.name,
jid: sender?.jid ? new JID(sender.jid) : undefined,
};
}
public getMark(): MessageMark {
return this.data.get('mark');
}
public aborted() {
let currentMark = this.data.get('mark', MessageMark.pending);
if (currentMark === MessageMark.pending) {
this.data.set('mark', MessageMark.aborted);
}
}
public isAborted(): boolean {
return this.data.get('mark', MessageMark.aborted) === MessageMark.aborted;
}
public transferred() {
let currentMark = this.data.get('mark', MessageMark.pending);
this.data.set('mark', Math.max(currentMark, MessageMark.transferred));
}
public isTransferred(): boolean {
return this.data.get('mark', MessageMark.pending) >= MessageMark.transferred;
}
public received() {
let currentMark = this.data.get('mark', MessageMark.pending);
this.data.set('mark', Math.max(currentMark, MessageMark.received));
}
public isReceived(): boolean {
//this.data.get('received') is deprecated since 4.0.x
return this.data.get('mark', MessageMark.pending) >= MessageMark.received || !!this.data.get('received');
}
public displayed() {
let currentMark = this.data.get('mark', MessageMark.pending);
this.data.set('mark', Math.max(currentMark, MessageMark.displayed));
}
public isDisplayed(): boolean {
return this.data.get('mark', MessageMark.pending) >= MessageMark.displayed;
}
public acknowledged() {
this.data.set('mark', MessageMark.acknowledged);
}
public isAcknowledged(): boolean {
return this.data.get('mark', MessageMark.pending) >= MessageMark.acknowledged;
}
public isForwarded(): boolean {
return !!this.data.get('forwarded');
}
public isEncrypted(): boolean {
return !!this.data.get('encrypted');
}
public hasAttachment(): boolean {
return !!this.data.get('attachment');
}
public isUnread(): boolean {
return !!this.data.get('unread');
}
public read() {
this.data.set('unread', false);
}
public setDirection(direction: DIRECTION) {
this.data.set('direction', direction);
}
public setPlaintextMessage(plaintextMessage: string) {
this.data.set('plaintextMessage', plaintextMessage);
}
public setEncryptedPlaintextMessage(encryptedPlaintextMessage: string) {
this.data.set('encryptedPlaintextMessage', encryptedPlaintextMessage);
}
public setEncrypted(encrypted: boolean = false) {
this.data.set('encrypted', encrypted);
}
public async getProcessedBody(): Promise<string> {
let body = this.getPlaintextMessage();
body = Utils.escapeHTML(body);
body = await Message.formatText(body, this.getDirection(), this.getPeer(), this.getSender().name);
return `<p dir="auto">${body}</p>`;
}
public getPlaintextEmoticonMessage(emotions: 'unicode' | 'image' = 'image'): string {
let body = this.getPlaintextMessage();
body = Utils.escapeHTML(body);
body = emotions === 'unicode' ? Emoticons.toUnicode(body) : Emoticons.toImage(body);
return body;
}
public setErrorMessage(error: string) {
return this.data.set('errorMessage', error);
}
public getErrorMessage(): string {
return this.data.get('errorMessage');
}
public updateProgress(transferred: number, size: number) {
this.data.set('progress', transferred / size);
}
public getLastVersion(): IMessage {
let replacedBy = this.getReplacedBy();
while (replacedBy && replacedBy.getReplacedBy()) {
replacedBy = replacedBy.getReplacedBy();
}
return replacedBy || this;
}
public getReplacedBy(): IMessage {
if (this.replacedBy) {
return this.replacedBy;
}
const replacedByUid = this.data.get('replacedBy');
this.replacedBy = replacedByUid ? new Message(replacedByUid) : undefined;
return this.replacedBy;
}
public setReplacedBy(message: IMessage): void {
this.data.set('replacedBy', message.getUid());
}
public getOriginal(): IMessage {
if (this.original) {
return this.original;
}
const originalUid = this.data.get('original');
this.original = originalUid ? new Message(originalUid) : undefined;
return this.original;
}
public setOriginal(message: IMessage): void {
this.data.set('original', message.getUid());
}
public isReplacement(): boolean {
return !!this.data.get('original');
}
}
function convertUrlToLink(text: string) {
return text.replace(CONST.REGEX.URL, function (url) {
let href = url.match(/^https?:\/\//i) ? url : 'http://' + url;
return '<a href="' + href + '" target="_blank" rel="noopener noreferrer">' + url + '</a>';
});
}
function convertEmailToLink(text: string) {
return text.replace(ATREGEX, function (str, protocol, jid, action) {
if (protocol === 'xmpp:') {
if (typeof action === 'string') {
jid += action;
}
return '<a href="xmpp:' + jid + '">xmpp:' + jid + '</a>';
}
return '<a href="mailto:' + jid + '" target="_blank">' + jid + '</a>';
});
}
function convertGeoToLink(text: string) {
return text.replace(CONST.REGEX.GEOURI, url => {
return `<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`;
});
}
function markQuotation(text: string) {
return text
.split(/(?:\n|\r\n|\r)/)
.map(line => {
return line.indexOf('>') === 0
? '<span class="jsxc-quote">' + line.replace(/^> ?/, '') + '</span>'
: line;
})
.join('\n');
}
function replaceLineBreaks(text: string) {
return text.replace(/(\r\n|\r|\n){2}/g, '</p><p dir="auto">').replace(/(\r\n|\r|\n)/g, '<br/>');
}
Message.addFormatter(convertUrlToLink);
Message.addFormatter(convertEmailToLink);
Message.addFormatter(convertGeoToLink);
Message.addFormatter(Emoticons.toImage.bind(Emoticons));
Message.addFormatter(markQuotation);
Message.addFormatter(replaceLineBreaks);