xmpp.chapril.org-conversejs/src/converse-mam.js

438 lines
19 KiB
JavaScript
Raw Normal View History

// Converse.js (A browser based XMPP chat client)
// http://conversejs.org
//
// Copyright (c) 2012-2017, Jan-Carel Brand <jc@opkode.com>
// Licensed under the Mozilla Public License (MPLv2)
//
/*global define */
// XEP-0059 Result Set Management
(function (root, factory) {
define(["sizzle",
"converse-core",
"converse-disco",
"strophe.rsm"
], factory);
}(this, function (sizzle, converse) {
"use strict";
const CHATROOMS_TYPE = 'chatroom';
const { Promise, Strophe, $iq, _, moment } = converse.env;
const u = converse.env.utils;
const RSM_ATTRIBUTES = ['max', 'first', 'last', 'after', 'before', 'index', 'count'];
// XEP-0313 Message Archive Management
const MAM_ATTRIBUTES = ['with', 'start', 'end'];
2018-01-09 23:05:23 +01:00
function getMessageArchiveID (stanza) {
const result = sizzle(`result[xmlns="${Strophe.NS.MAM}"]`, stanza).pop();
if (!_.isUndefined(result)) {
return result.getAttribute('id');
}
const stanza_id = sizzle(`stanza-id[xmlns="${Strophe.NS.SID}"]`, stanza).pop();
if (!_.isUndefined(stanza_id)) {
return stanza_id.getAttribute('id');
}
}
2018-01-16 12:20:18 +01:00
function queryForArchivedMessages (_converse, options, callback, errback) {
/* Internal function, called by the "archive.query" API method.
*/
let date;
if (_.isFunction(options)) {
callback = options;
errback = callback;
}
const queryid = _converse.connection.getUniqueId();
const attrs = {'type':'set'};
if (!_.isUndefined(options) && options.groupchat) {
if (!options['with']) { // eslint-disable-line dot-notation
throw new Error(
'You need to specify a "with" value containing '+
'the chat room JID, when querying groupchat messages.');
}
attrs.to = options['with']; // eslint-disable-line dot-notation
}
const stanza = $iq(attrs).c('query', {'xmlns':Strophe.NS.MAM, 'queryid':queryid});
if (!_.isUndefined(options)) {
stanza.c('x', {'xmlns':Strophe.NS.XFORM, 'type': 'submit'})
.c('field', {'var':'FORM_TYPE', 'type': 'hidden'})
.c('value').t(Strophe.NS.MAM).up().up();
if (options['with'] && !options.groupchat) { // eslint-disable-line dot-notation
stanza.c('field', {'var':'with'}).c('value')
.t(options['with']).up().up(); // eslint-disable-line dot-notation
}
_.each(['start', 'end'], function (t) {
if (options[t]) {
date = moment(options[t]);
if (date.isValid()) {
stanza.c('field', {'var':t}).c('value').t(date.format()).up().up();
} else {
throw new TypeError(`archive.query: invalid date provided for: ${t}`);
}
}
});
stanza.up();
if (options instanceof Strophe.RSM) {
stanza.cnode(options.toXML());
} else if (_.intersection(RSM_ATTRIBUTES, _.keys(options)).length) {
stanza.cnode(new Strophe.RSM(options).toXML());
}
}
const messages = [];
const message_handler = _converse.connection.addHandler((message) => {
2018-01-22 10:41:13 +01:00
if (options.groupchat && message.getAttribute('from') !== options['with']) { // eslint-disable-line dot-notation
2018-01-22 09:53:35 +01:00
return true;
2018-01-21 22:02:12 +01:00
}
2018-01-16 12:20:18 +01:00
const result = message.querySelector('result');
if (!_.isNull(result) && result.getAttribute('queryid') === queryid) {
messages.push(message);
}
return true;
}, Strophe.NS.MAM);
_converse.connection.sendIQ(
stanza,
function (iq) {
_converse.connection.deleteHandler(message_handler);
if (_.isFunction(callback)) {
const set = iq.querySelector('set');
let rsm;
if (!_.isUndefined(set)) {
rsm = new Strophe.RSM({xml: set});
_.extend(rsm, _.pick(options, _.concat(MAM_ATTRIBUTES, ['max'])));
}
callback(messages, rsm);
}
},
function () {
_converse.connection.deleteHandler(message_handler);
if (_.isFunction(errback)) { errback.apply(this, arguments); }
},
_converse.message_archiving_timeout
);
}
2018-01-09 23:05:23 +01:00
converse.plugins.add('converse-mam', {
dependencies: ['converse-chatview', 'converse-muc', 'converse-muc-views'],
overrides: {
// Overrides mentioned here will be picked up by converse.js's
// plugin architecture they will replace existing methods on the
// relevant objects or classes.
//
// New functions which don't exist yet can also be added.
ChatBox: {
getMessageAttributesFromStanza (message, original_stanza) {
2018-08-04 19:41:06 +02:00
return new Promise((resolve, reject) => {
this.__super__.getMessageAttributesFromStanza.apply(this, arguments)
.then((attrs) => {
const archive_id = getMessageArchiveID(original_stanza);
if (archive_id) {
attrs.archive_id = archive_id;
}
resolve(attrs);
}).catch(reject);
});
}
},
ChatBoxView: {
render () {
const result = this.__super__.render.apply(this, arguments);
if (!this.disable_mam) {
2018-01-02 21:44:46 +01:00
this.content.addEventListener('scroll', _.debounce(this.onScroll.bind(this), 100));
}
return result;
},
fetchNewestMessages () {
/* Fetches messages that might have been archived *after*
* the last archived message in our local cache.
*/
if (this.disable_mam) { return; }
const { _converse } = this.__super__,
most_recent_msg = u.getMostRecentMessage(this.model);
if (_.isNil(most_recent_msg)) {
this.fetchArchivedMessages();
} else {
const archive_id = most_recent_msg.get('archive_id');
if (archive_id) {
this.fetchArchivedMessages({
'after': most_recent_msg.get('archive_id')
});
} else {
this.fetchArchivedMessages({
'start': most_recent_msg.get('time')
});
}
}
},
fetchArchivedMessagesIfNecessary () {
/* Check if archived messages should be fetched, and if so, do so. */
if (this.disable_mam || this.model.get('mam_initialized')) {
return;
}
const { _converse } = this.__super__;
_converse.api.disco.supports(Strophe.NS.MAM, _converse.bare_jid).then(
(result) => { // Success
if (result.length) {
this.fetchArchivedMessages();
}
this.model.save({'mam_initialized': true});
},
() => { // Error
_converse.log(
"Error or timeout while checking for MAM support",
Strophe.LogLevel.ERROR
);
}
).catch((msg) => {
this.clearSpinner();
_converse.log(msg, Strophe.LogLevel.FATAL);
});
},
fetchArchivedMessages (options) {
const { _converse } = this.__super__;
if (this.disable_mam) { return; }
const is_groupchat = this.model.get('type') === CHATROOMS_TYPE;
let mam_jid, message_handler;
if (is_groupchat) {
mam_jid = this.model.get('jid');
message_handler = this.model.onMessage.bind(this.model);
} else {
mam_jid = _converse.bare_jid;
message_handler = _converse.chatboxes.onMessage.bind(_converse.chatboxes)
}
_converse.api.disco.supports(Strophe.NS.MAM, mam_jid).then(
(results) => { // Success
if (!results.length) { return; }
this.addSpinner();
_converse.api.archive.query(
_.extend({
'groupchat': is_groupchat,
'before': '', // Page backwards from the most recent message
'max': _converse.archived_messages_page_size,
'with': this.model.get('jid'),
}, options),
(messages) => { // Success
this.clearSpinner();
_.each(messages, message_handler);
},
() => { // Error
this.clearSpinner();
_converse.log(
"Error or timeout while trying to fetch "+
"archived messages", Strophe.LogLevel.ERROR);
}
);
},
() => { // Error
_converse.log(
"Error or timeout while checking for MAM support",
Strophe.LogLevel.ERROR
);
}
).catch((msg) => {
this.clearSpinner();
_converse.log(msg, Strophe.LogLevel.FATAL);
});
},
onScroll (ev) {
const { _converse } = this.__super__;
if (this.content.scrollTop === 0 && this.model.messages.length) {
const oldest_message = this.model.messages.at(0);
const archive_id = oldest_message.get('archive_id');
if (archive_id) {
this.fetchArchivedMessages({
'before': archive_id
});
} else {
this.fetchArchivedMessages({
'end': oldest_message.get('time')
});
}
}
},
},
ChatRoom: {
2017-06-23 22:53:11 +02:00
2018-01-09 23:05:23 +01:00
isDuplicate (message, original_stanza) {
const result = this.__super__.isDuplicate.apply(this, arguments);
if (result) {
return result;
}
const archive_id = getMessageArchiveID(original_stanza);
if (archive_id) {
return this.messages.filter({'archive_id': archive_id}).length > 0;
2018-01-09 23:05:23 +01:00
}
}
},
ChatRoomView: {
initialize () {
const { _converse } = this.__super__;
this.__super__.initialize.apply(this, arguments);
this.model.on('change:mam_enabled', this.fetchArchivedMessagesIfNecessary, this);
this.model.on('change:connection_status', this.fetchArchivedMessagesIfNecessary, this);
2018-01-09 23:05:23 +01:00
},
2017-12-20 11:02:01 +01:00
renderChatArea () {
const result = this.__super__.renderChatArea.apply(this, arguments);
if (!this.disable_mam) {
2018-01-02 21:44:46 +01:00
this.content.addEventListener('scroll', _.debounce(this.onScroll.bind(this), 100));
}
return result;
},
fetchArchivedMessagesIfNecessary () {
if (this.model.get('connection_status') !== converse.ROOMSTATUS.ENTERED ||
!this.model.get('mam_enabled') ||
this.model.get('mam_initialized')) {
return;
}
this.fetchArchivedMessages();
this.model.save({'mam_initialized': true});
}
}
},
initialize () {
/* The initialize function gets called as soon as the plugin is
* loaded by Converse.js's plugin machinery.
*/
const { _converse } = this;
_converse.api.settings.update({
2017-02-27 22:12:15 +01:00
archived_messages_page_size: '50',
message_archiving: undefined, // Supported values are 'always', 'never', 'roster' (https://xmpp.org/extensions/xep-0313.html#prefs)
message_archiving_timeout: 8000, // Time (in milliseconds) to wait before aborting MAM request
});
_converse.onMAMError = function (model, iq) {
if (iq.querySelectorAll('feature-not-implemented').length) {
_converse.log(
"Message Archive Management (XEP-0313) not supported by this server",
Strophe.LogLevel.WARN);
} else {
_converse.log(
"An error occured while trying to set archiving preferences.",
Strophe.LogLevel.ERROR);
_converse.log(iq);
}
};
_converse.onMAMPreferences = function (feature, iq) {
/* Handle returned IQ stanza containing Message Archive
* Management (XEP-0313) preferences.
*
* XXX: For now we only handle the global default preference.
* The XEP also provides for per-JID preferences, which is
* currently not supported in converse.js.
*
* Per JID preferences will be set in chat boxes, so it'll
* probbaly be handled elsewhere in any case.
*/
const preference = sizzle(`prefs[xmlns="${Strophe.NS.MAM}"]`, iq).pop();
const default_pref = preference.getAttribute('default');
if (default_pref !== _converse.message_archiving) {
const stanza = $iq({'type': 'set'})
.c('prefs', {
'xmlns':Strophe.NS.MAM,
'default':_converse.message_archiving
});
_.each(preference.children, function (child) {
stanza.cnode(child).up();
});
_converse.connection.sendIQ(stanza, _.partial(function (feature, iq) {
// XXX: Strictly speaking, the server should respond with the updated prefs
// (see example 18: https://xmpp.org/extensions/xep-0313.html#config)
// but Prosody doesn't do this, so we don't rely on it.
feature.save({'preferences': {'default':_converse.message_archiving}});
}, feature),
_converse.onMAMError
);
} else {
feature.save({'preferences': {'default':_converse.message_archiving}});
}
};
/* Event handlers */
_converse.on('serviceDiscovered', (feature) => {
const prefs = feature.get('preferences') || {};
if (feature.get('var') === Strophe.NS.MAM &&
prefs['default'] !== _converse.message_archiving && // eslint-disable-line dot-notation
!_.isUndefined(_converse.message_archiving) ) {
// Ask the server for archiving preferences
_converse.connection.sendIQ(
$iq({'type': 'get'}).c('prefs', {'xmlns': Strophe.NS.MAM}),
_.partial(_converse.onMAMPreferences, feature),
_.partial(_converse.onMAMError, feature)
);
}
});
_converse.on('addClientFeatures', () => {
_converse.api.disco.own.features.add(Strophe.NS.MAM);
});
_converse.on('afterMessagesFetched', (chatboxview) => {
chatboxview.fetchNewestMessages();
});
_converse.on('reconnected', () => {
const private_chats = _converse.chatboxviews.filter(
(view) => _.at(view, 'model.attributes.type')[0] === 'chatbox'
);
_.each(private_chats, (view) => view.fetchNewestMessages())
});
_.extend(_converse.api, {
/* Extend default converse.js API to add methods specific to MAM
*/
'archive': {
2018-01-16 12:20:18 +01:00
'query': function (options, callback, errback) {
/* Do a MAM (XEP-0313) query for archived messages.
*
* Parameters:
* (Object) options - Query parameters, either
* MAM-specific or also for Result Set Management.
* (Function) callback - A function to call whenever
* we receive query-relevant stanza.
* (Function) errback - A function to call when an
* error stanza is received.
*
* The options parameter can also be an instance of
* Strophe.RSM to enable easy querying between results pages.
*
* When the the callback is called, a Strophe.RSM object is
* returned on which "next" or "previous" can be called
* before passing it in again to this method, to
* get the next or previous page in the result set.
*/
if (!_converse.api.connection.connected()) {
throw new Error('Can\'t call `api.archive.query` before having established an XMPP session');
}
2018-01-16 12:20:18 +01:00
return queryForArchivedMessages(_converse, options, callback, errback);
}
}
});
}
});
}));