@jsxc/jsxc
Version:
Real-time XMPP chat application with video calls, file transfer and encrypted communication
335 lines (269 loc) • 10.2 kB
text/typescript
import * as Namespace from '@src/connection/xmpp/namespace';
import { ContactSubscription } from '@src/Contact.interface';
import JID from '@src/JID';
import { IJID } from '@src/JID.interface';
import Message from '@src/Message';
import { DIRECTION } from '@src/Message.interface';
import { AbstractPlugin, IMetaData } from '@src/plugin/AbstractPlugin';
import PluginAPI from '@src/plugin/PluginAPI';
import ChatWindow from '@src/ui/ChatWindow';
import Translation from '@src/util/Translation';
import { $msg } from '@src/vendor/Strophe';
const MIN_VERSION = '4.0.0';
const MAX_VERSION = '99.0.0';
// frequently used strings
const CHATMARKERS = 'CHATMARKERS';
const MARKABLE = 'markable';
const RECEIVED = 'received';
const DISPLAYED = 'displayed';
const ACKNOWLEDGED = 'acknowledged';
const ID = 'id';
const FROM = 'from';
export default class ChatMarkersPlugin extends AbstractPlugin {
public static getId(): string {
return 'chat-markers';
}
public static getName(): string {
return 'Chat Markers';
}
public static getMetaData(): IMetaData {
return {
description: Translation.t('chatmarkers-description'),
xeps: [
{
id: 'XEP-0333',
name: 'Chat Markers',
version: '0.3',
},
],
};
}
constructor(pluginAPI: PluginAPI) {
super(MIN_VERSION, MAX_VERSION, pluginAPI);
Namespace.register(CHATMARKERS, 'urn:xmpp:chat-markers:0');
this.pluginAPI.addFeature(Namespace.get(CHATMARKERS));
this.pluginAPI.addPreSendMessageStanzaProcessor(this.preSendMessageStanzaProcessor);
this.pluginAPI.getConnection().registerHandler(this.onChatMarkersMessage, null, 'message');
this.pluginAPI.registerChatWindowInitializedHook((chatWindow: ChatWindow) => {
this.chatWindowInitializedHook(chatWindow);
});
}
private async supportsChatMarkers(jid: IJID) {
if (jid.isBare()) {
// if bare JID, sender MAY send chat markers
return true;
}
// if full JID, sender SHOULD try to determine if recipient supports chat markers
let repository = this.pluginAPI.getDiscoInfoRepository();
try {
return repository.hasFeature(jid, [Namespace.get(CHATMARKERS)]);
} catch (err) {
return false;
}
}
private hasSubscription(jid: IJID): boolean {
let contact = this.pluginAPI.getContact(jid);
if (!contact) {
return false;
}
let subscription = contact.getSubscription();
return subscription === ContactSubscription.FROM || subscription === ContactSubscription.BOTH;
}
// add "markable" element according to XEP-0333
private addMarkable(xmlStanza: Strophe.Builder) {
xmlStanza
.c(MARKABLE, {
xmlns: Namespace.get(CHATMARKERS),
})
.up();
}
// send "received" message according to XEP-0333
private sendReceived(lastReceivedMsgId: string, to: IJID) {
this.pluginAPI.Log.debug(`sending ${RECEIVED} message. Yaay! =)`);
this.pluginAPI.send(
$msg({
to: to.full,
type: 'chat',
})
.c(RECEIVED, {
xmlns: Namespace.get(CHATMARKERS),
id: lastReceivedMsgId,
})
.up()
.c('store', {
xmlns: 'urn:xmpp:hints',
})
);
}
// send "displayed" message according to XEP-0333
private sendDisplayed(lastDisplayedMsgId: string, to: IJID) {
this.pluginAPI.Log.debug(`sending ${DISPLAYED} message. Yaay! =)`);
this.pluginAPI.send(
$msg({
to: to.full,
type: 'chat',
})
.c(DISPLAYED, {
xmlns: Namespace.get(CHATMARKERS),
id: lastDisplayedMsgId,
})
.up()
.c('store', {
xmlns: 'urn:xmpp:hints',
})
);
}
// send "acknowledged" message according to XEP-0333
// private sendAcknowledged(lastAcknowledgedMsgId: string, to: IJID) {
// this.pluginAPI.Log.debug(`sending ${ACKNOWLEDGED} message. Yaay! =)`);
// this.pluginAPI.send($msg({
// to: to.full
// }).c(ACKNOWLEDGED, {
// xmlns: Namespace.get(CHATMARKERS),
// id: lastAcknowledgedMsgId
// }).up().c('store', {
// xmlns: 'urn:xmpp:hints'
// }));
// }
private preSendMessageStanzaProcessor = (msg: Message, stanza: Strophe.Builder): Promise<any> => {
if (msg.getType() === Message.MSGTYPE.CHAT) {
return this.supportsChatMarkers(msg.getPeer()).then(hasFeature => {
if (hasFeature) {
this.addMarkable(stanza);
}
return [msg, stanza];
});
}
return Promise.resolve([msg, stanza]);
};
private onChatMarkersMessage = (stanza: string) => {
let stanzaElement = $(stanza);
let markerElement = stanzaElement.find(Namespace.getFilter(CHATMARKERS));
if (markerElement.length === 0) {
return true;
}
let mamResultElement =
stanzaElement.find(Namespace.getFilter('MAM2', 'result')) ||
stanzaElement.find(Namespace.getFilter('MAM1', 'result'));
let isMam = mamResultElement.length > 0;
let carbonReceivedElement = stanzaElement.find(Namespace.getFilter('CARBONS', 'received'));
let carbonSentElement = stanzaElement.find(Namespace.getFilter('CARBONS', 'sent'));
let isCarbon = carbonReceivedElement.length > 0 || carbonSentElement.length > 0;
if (isCarbon && stanzaElement.attr('from') !== this.pluginAPI.getConnection().getJID().bare) {
this.pluginAPI.Log.warn(`Received carbon copy from "${stanzaElement.attr('from')}". Ignoring.`);
return true;
}
let messageElement: JQuery<HTMLElement>;
if (carbonReceivedElement.length > 0) {
messageElement = carbonReceivedElement.find('message');
} else if (carbonSentElement.length > 0) {
messageElement = carbonSentElement.find('message');
} else if (mamResultElement.length > 0) {
messageElement = mamResultElement.find('message');
} else {
messageElement = stanzaElement;
}
let idAttr = messageElement.attr(ID);
let fromAttr = messageElement.attr(FROM);
let toAttr = messageElement.attr('to');
let typeAttr = messageElement.attr('type');
if (typeAttr === Message.MSGTYPE.GROUPCHAT || typeAttr === 'error') {
return true;
}
let markableMessageId = markerElement.attr(ID);
let marker = markerElement.prop('tagName').toLowerCase() as string;
this.pluginAPI.Log.debug(`"${marker}" marker received from "${fromAttr}" to "${toAttr}"`);
if ([MARKABLE, RECEIVED, DISPLAYED, ACKNOWLEDGED].indexOf(marker) < 0) {
this.pluginAPI.Log.info(`"${marker}" is no valid marker`);
return true;
}
if (marker === MARKABLE) {
if (!idAttr || !fromAttr) {
return true;
}
if (!isCarbon && !isMam) {
let peer = new JID(fromAttr);
this.sendReceived(idAttr, peer);
}
} else {
if (!markableMessageId || !fromAttr) {
return true;
}
let peerJid = new JID(carbonSentElement.length > 0 ? toAttr : fromAttr);
let direction = carbonSentElement.length > 0 ? DIRECTION.IN : DIRECTION.OUT;
this.markMessages(markableMessageId, peerJid, marker, direction);
}
return true;
};
private markMessages(markableMessageId: string, peer: IJID, status: string, direction: DIRECTION) {
let contact = this.pluginAPI.getContact(peer);
if (!contact) {
return;
}
let transcript = contact.getTranscript();
let msg = transcript.getFirstMessage();
while (msg && msg.getAttrId() !== markableMessageId) {
try {
msg = transcript.getMessage(msg.getNextId());
} catch (error) {
msg = undefined;
break;
}
}
// @REVIEW spec is not clear if only markable message from the same resource should be marked
while (!!msg) {
if (msg.getDirection() === direction && msg.isTransferred() && !msg.getErrorMessage()) {
if (status === RECEIVED) {
if (msg.isReceived()) {
// no need to traverse all messages
break;
}
msg.received();
} else if (status === DISPLAYED) {
if (msg.isDisplayed()) {
break;
}
msg.read();
msg.displayed();
} else if (status === ACKNOWLEDGED) {
if (msg.isAcknowledged()) {
break;
}
msg.read();
msg.acknowledged();
}
}
try {
msg = transcript.getMessage(msg.getNextId());
} catch (error) {
break;
}
}
}
private chatWindowInitializedHook(chatWindow: ChatWindow) {
if (chatWindow.getContact().isGroupChat()) {
return;
}
let windowElement = chatWindow.getDom().find('.jsxc-message-input');
windowElement.on('focus', () => this.onChatWindowFocus(chatWindow));
}
private onChatWindowFocus = (chatWindow: ChatWindow) => {
let transcript = chatWindow.getTranscript();
let msg = transcript.getFirstMessage();
while (!!msg) {
if (msg.getDirection() === DIRECTION.IN && msg.getType() !== Message.MSGTYPE.GROUPCHAT) {
if (!msg.isDisplayed()) {
this.supportsChatMarkers(msg.getPeer()).then(hasFeature => {
if (hasFeature && this.hasSubscription(msg.getPeer())) {
this.sendDisplayed(msg.getAttrId(), msg.getPeer());
}
});
msg.displayed();
}
break;
} else {
msg = transcript.getMessage(msg.getNextId());
}
}
};
}