Commit 97dade10 authored by JC Brand's avatar JC Brand

Initial work on #1038

parent a15c9e54
......@@ -22,7 +22,7 @@
it("can be bookmarked", mock.initConverseWithPromises(
null, ['rosterGroupsFetched'], {}, function (done, _converse) {
test_utils.waitUntilDiscoConfirmed(
_converse, _converse.bare_jid,
[{'category': 'pubsub', 'type': 'pep'}],
......@@ -143,6 +143,224 @@
});
}));
describe("when publish-options is not supported", function () {
it("can not be bookmarked if the node can't be configured", mock.initConverseWithPromises(
null, ['rosterGroupsFetched'], {}, function (done, _converse) {
test_utils.waitUntilDiscoConfirmed(
_converse, _converse.bare_jid,
[{'category': 'pubsub', 'type': 'pep'}],
['http://jabber.org/protocol/pubsub#create-nodes']
).then(function () {
var sent_stanza, IQ_id;
var sendIQ = _converse.connection.sendIQ;
spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
sent_stanza = iq;
IQ_id = sendIQ.bind(this)(iq, callback, errback);
});
spyOn(_converse.connection, 'getUniqueId').and.callThrough();
test_utils.openChatRoom(_converse, 'theplay', 'conference.shakespeare.lit', 'JC');
var jid = 'theplay@conference.shakespeare.lit';
var view = _converse.chatboxviews.get(jid);
spyOn(view, 'renderBookmarkForm').and.callThrough();
spyOn(view, 'closeForm').and.callThrough();
test_utils.waitUntil(function () {
return !_.isNull(view.el.querySelector('.toggle-bookmark'));
}, 300).then(function () {
expect(view.model.get('bookmarked')).toBeFalsy();
/* Client uploads data:
* --------------------
* <iq from='juliet@capulet.lit/balcony' type='set' id='pip1'>
* <pubsub xmlns='http://jabber.org/protocol/pubsub'>
* <publish node='storage:bookmarks'>
* <item id='current'>
* <storage xmlns='storage:bookmarks'>
* <conference name='The Play&apos;s the Thing'
* autojoin='true'
* jid='theplay@conference.shakespeare.lit'>
* <nick>JC</nick>
* </conference>
* </storage>
* </item>
* </publish>
* </pubsub>
* </iq>
*/
var $bookmark = $(view.el).find('.toggle-bookmark');
$bookmark[0].click();
expect(view.renderBookmarkForm).toHaveBeenCalled();
var $form = $(view.el).find('.chatroom-form');
$form.find('input[name="name"]').val('Play&apos;s the Thing');
$form.find('input[name="autojoin"]').prop('checked', true);
$form.find('input[name="nick"]').val('JC');
view.el.querySelector('.btn-primary').click();
expect(view.model.get('bookmarked')).toBeTruthy();
expect(_converse.bookmarks.models.length).toBe(1);
expect($bookmark.hasClass('on-button'), true);
return test_utils.waitUntil(function () {
return sent_stanza.toLocaleString() ===
"<iq type='get' from='dummy@localhost/resource' xmlns='jabber:client' id='"+IQ_id+"'>"+
"<pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>" +
"<configure node='storage:bookmarks'/>" +
"</pubsub>" +
"</iq>";
});
}).then(function () {
/* Server says that the node may not be configured
*
* <iq type='error'
* from='hamlet@denmark.lit/elsinore'
* to='pubsub.shakespeare.lit'
* id='config1'>
* <error type='cancel'>
* <not-allowed xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
* </error>
* </iq>
*/
var stanza = $iq({
'to': _converse.connection.jid,
'type': 'error',
'id': IQ_id
}).c('error', {'type': 'cancel'})
.c('not-allowed', {'xmlns': 'urn:ietf:params:xml:ns:xmpp-stanzas'});
_converse.connection._dataRecv(test_utils.createRequest(stanza));
// TODO: check that the bookmark toggle goes off.
return test_utils.waitUntil(function () {
return !view.model.get('bookmarked');
});
}).then(function () {
// TODO: show erorr modal
expect(_converse.bookmarks.models.length).toBe(0);
done();
});
});
}));
describe("and no PEP node exists", function () {
it("can still be bookmarked if the node can be created", mock.initConverseWithPromises(
null, ['rosterGroupsFetched'], {}, function (done, _converse) {
test_utils.waitUntilDiscoConfirmed(
_converse, _converse.bare_jid,
[{'category': 'pubsub', 'type': 'pep'}],
['http://jabber.org/protocol/pubsub#create-nodes']
).then(function () {
var sent_stanza, IQ_id;
var sendIQ = _converse.connection.sendIQ;
spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
sent_stanza = iq;
IQ_id = sendIQ.bind(this)(iq, callback, errback);
});
spyOn(_converse.connection, 'getUniqueId').and.callThrough();
test_utils.openChatRoom(_converse, 'theplay', 'conference.shakespeare.lit', 'JC');
var jid = 'theplay@conference.shakespeare.lit';
var view = _converse.chatboxviews.get(jid);
spyOn(view, 'renderBookmarkForm').and.callThrough();
spyOn(view, 'closeForm').and.callThrough();
test_utils.waitUntil(function () {
return !_.isNull(view.el.querySelector('.toggle-bookmark'));
}, 300).then(function () {
expect(view.model.get('bookmarked')).toBeFalsy();
/* Client uploads data:
* --------------------
* <iq from='juliet@capulet.lit/balcony' type='set' id='pip1'>
* <pubsub xmlns='http://jabber.org/protocol/pubsub'>
* <publish node='storage:bookmarks'>
* <item id='current'>
* <storage xmlns='storage:bookmarks'>
* <conference name='The Play&apos;s the Thing'
* autojoin='true'
* jid='theplay@conference.shakespeare.lit'>
* <nick>JC</nick>
* </conference>
* </storage>
* </item>
* </publish>
* </pubsub>
* </iq>
*/
var $bookmark = $(view.el).find('.toggle-bookmark');
$bookmark[0].click();
expect(view.renderBookmarkForm).toHaveBeenCalled();
var $form = $(view.el).find('.chatroom-form');
$form.find('input[name="name"]').val('Play&apos;s the Thing');
$form.find('input[name="autojoin"]').prop('checked', true);
$form.find('input[name="nick"]').val('JC');
view.el.querySelector('.btn-primary').click();
expect(view.model.get('bookmarked')).toBeTruthy(); // FIXME: this should be reset when bookmarking fails
expect($bookmark.hasClass('on-button'), true);
return test_utils.waitUntil(function () {
return sent_stanza.toLocaleString() ===
"<iq type='get' from='dummy@localhost/resource' xmlns='jabber:client' id='"+IQ_id+"'>"+
"<pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>" +
"<configure node='storage:bookmarks'/>" +
"</pubsub>" +
"</iq>";
});
}).then(function () {
/* Server says that the node doesn't exist.
* <iq type='error'
* from='pubsub.shakespeare.lit'
* to='hamlet@denmark.lit/elsinore'
* id='config1'>
* <error type='cancel'>
* <item-not-found xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
* </error>
* </iq>
*/
var stanza = $iq({
'to': _converse.connection.jid,
'type': 'error',
'id': IQ_id
}).c('error', {'type': 'cancel'})
.c('item-not-found', {'xmlns': 'urn:ietf:params:xml:ns:xmpp-stanzas'});
_converse.connection._dataRecv(test_utils.createRequest(stanza));
return test_utils.waitUntil(function () {
return sent_stanza.nodeTree.getAttribute('type') === 'set';
});
}).then(function () {
// Converse.js creates and configures the node in one go.
expect(sent_stanza.toLocaleString()).toBe(
"<iq type='set' from='dummy@localhost/resource' xmlns='jabber:client' id='"+IQ_id+"'>"+
"<pubsub xmlns='http://jabber.org/protocol/pubsub'>" +
"<create node='storage:bookmarks'/>" +
"<configure>" +
"<x xmlns='jabber:x:data' type='submit'>"+
"<field var='FORM_TYPE' type='hidden'>"+
"<value>http://jabber.org/protocol/pubsub#node_config</value>"+
"</field>"+
"<field var='pubsub#access_model'><value>whitelist</value></field>"+
"<field var='pubsub#persist_items'><value>1</value></field>"+
"</x>"+
"</configure>" +
"</pubsub>" +
"</iq>");
expect(view.model.get('bookmarked')).toBeTruthy();
expect(_converse.bookmarks.models.length).toBe(1);
done();
});
});
}));
})
});
it("will be automatically opened if 'autojoin' is set on the bookmark", mock.initConverseWithPromises(
null, ['rosterGroupsFetched'], {}, function (done, _converse) {
......
......@@ -87,6 +87,7 @@ require.config({
"converse-otr": "src/converse-otr",
"converse-ping": "src/converse-ping",
"converse-profile": "src/converse-profile",
"converse-pubsub": "src/converse-pubsub",
"converse-register": "src/converse-register",
"converse-roomslist": "src/converse-roomslist",
"converse-rosterview": "src/converse-rosterview",
......
......@@ -27,7 +27,7 @@
tpl_bookmarks_list
) {
const { Backbone, Promise, Strophe, $iq, b64_sha1, sizzle, _ } = converse.env;
const { Backbone, Promise, Strophe, f, $iq, b64_sha1, sizzle, _ } = converse.env;
const u = converse.env.utils;
converse.plugins.add('converse-bookmarks', {
......@@ -257,7 +257,7 @@
initialize () {
this.on('add', _.flow(this.openBookmarkedRoom, this.markRoomAsBookmarked));
this.on('remove', this.markRoomAsUnbookmarked, this);
this.on('remove', this.sendBookmarkStanza, this);
this.on('remove', this.persistBookmarks, this);
const cache_key = `converse.room-bookmarks${_converse.bare_jid}`;
this.fetched_flag = b64_sha1(cache_key+'fetched');
......@@ -300,11 +300,141 @@
},
createBookmark (options) {
_converse.bookmarks.create(options);
_converse.bookmarks.sendBookmarkStanza();
this.create(options);
this.persistBookmarks().catch(() => {
this.findWhere({'jid': options.jid}).destroy();
const model = _converse.chatboxes.get(options.jid);
model.set('bookmarked', false);
});
},
addFormFields (stanza, config) {
_.each(_.keys(config), (key) => {
if (config[key].type === 'hidden') {
stanza.c('field', {'var': key, 'type': 'hidden'});
} else {
stanza.c('field', {'var': key});
}
if (_.isEmpty(config[key].values)) {
stanza.c('value').t('').up().up();
} else {
_.each(config[key].values, (value) => {
stanza.c('value').t(value).up().up();
});
}
});
return stanza;
},
sendBookmarkStanza () {
createNode (jid, node, config) {
return _converse.api.disco.supports(Strophe.NS.PUBSUB+'#create-nodes', _converse.bare_jid)
.then((result) => {
return new Promise((resolve, reject) => {
const stanza = $iq({
'type': 'set',
'from': _converse.connection.jid
}).c('pubsub', {'xmlns': Strophe.NS.PUBSUB})
.c('create', {'node': node}).up();
if (config) {
stanza.c('configure').c('x', {'xmlns': Strophe.NS.XFORM, 'type': 'submit'});
this.addFormFields(stanza, config);
}
_converse.connection.sendIQ(stanza, resolve, resolve);
});
}).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
},
fetchNodeConfiguration (jid, node) {
return new Promise((resolve, reject) => {
const stanza = $iq({
'type': 'get',
'from': _converse.connection.jid
}).c('pubsub', {'xmlns': Strophe.NS.PUBSUB+'#owner'})
.c('configure', {'node': node});
_converse.connection.sendIQ(stanza, resolve, resolve);
}).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
},
parseConfiguration (iq) {
const fields = iq.querySelectorAll('field');
const get_key_value = (el) => {
let values = _.map(_.get(el.querySelectorAll('value'), 'textContent'));
if (_.isEmpty(values)) {
// We need an empty `<values></values>` element if no value (and required)
values = [''];
}
return [
el.getAttribute('var'),
{
'values': values,
'required': !_.isNull(el.querySelector('required')),
'type': el.getAttribute('type')
}
];
}
return f.fromPairs(f.map(get_key_value, fields));
},
constructConfigStanza (jid, node, config) {
const stanza = $iq({
'type': 'set',
'from': _converse.connection.jid,
'to': jid
}).c('pubsub', {'xmlns': Strophe.NS.PUBSUB+'#owner'})
.c('configure', {'node': node})
.c('x', {'xmlns': Strophe.NS.XFORM, 'type': 'submit'});
this.addFormFields(stanza, config);
return stanza;
},
configureNode (jid, node, config) {
return new Promise((resolve, reject) => {
_converse.connection.sendIQ(this.constructConfigStanza(jid, node, config), resolve, resolve);
}).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
},
saveConfiguration (config) {
// FIXME: check what the config settings are before setting them
return this.fetchNodeConfiguration()
.then(this.parseConfiguration)
.then((config) => {
config['pubsub#access_model'] = {'values': ['whitelist']};
config['pubsub#persist_items'] = {'values': ['1']}; // More compatible with older OpenFire than `true`
this.configureNode(undefined, 'storage:bookmarks', config);
})
.catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
},
persistBookmarks () {
return new Promise((resolve, reject) => {
_converse.api.disco.supports(Strophe.NS.PUBSUB+'#publish-options', _converse.bare_jid).then((result) => {
if (result.supported) {
this.sendBookmarkStanza(resolve);
} else {
const config = {
'FORM_TYPE': {'values': ['http://jabber.org/protocol/pubsub#node_config'], 'type': 'hidden'},
'pubsub#access_model': {'values': ['whitelist']},
'pubsub#persist_items': {'values': ['1']} // More compatible with older OpenFire than `true`
}
this.fetchNodeConfiguration(undefined, 'storage:bookmarks').then((iq) => {
if (iq.getAttribute('type') === 'error') {
if (iq.querySelector('error[type="cancel"] item-not-found')) {
this.createNode(undefined, 'storage:bookmarks', config).then(resolve);
} else {
reject(new Error("Error while trying to configure 'storage:bookmarks' PEP node"), iq);
}
} else {
this.saveConfiguration(config).then(this.sendBookmarkStanza.bind(this, resolve));
}
});
}
});
});
},
sendBookmarkStanza (callback) {
let stanza = $iq({
'type': 'set',
'from': _converse.connection.jid,
......@@ -320,16 +450,18 @@
'jid': model.get('jid'),
}).c('nick').t(model.get('nick')).up().up();
});
stanza.up().up().up();
stanza.c('publish-options')
.c('x', {'xmlns': Strophe.NS.XFORM, 'type':'submit'})
.c('field', {'var':'FORM_TYPE', 'type':'hidden'})
.c('value').t('http://jabber.org/protocol/pubsub#publish-options').up().up()
.c('field', {'var':'pubsub#persist_items'})
.c('value').t('true').up().up()
.c('field', {'var':'pubsub#access_model'})
.c('value').t('whitelist');
_converse.connection.sendIQ(stanza, null, this.onBookmarkError.bind(this));
_converse.api.disco.supports(Strophe.NS.PUBSUB+'#publish-options', _converse.bare_jid).then(() => {
stanza.up().up().up().c('publish-options')
.c('x', {'xmlns': Strophe.NS.XFORM, 'type':'submit'})
.c('field', {'var':'FORM_TYPE', 'type':'hidden'})
.c('value').t('http://jabber.org/protocol/pubsub#publish-options').up().up()
.c('field', {'var':'pubsub#persist_items'})
.c('value').t('true').up().up()
.c('field', {'var':'pubsub#access_model'})
.c('value').t('whitelist');
});
_converse.connection.sendIQ(stanza, callback, this.onBookmarkError.bind(this));
},
onBookmarkError (iq) {
......
......@@ -91,6 +91,7 @@
'converse-otr',
'converse-ping',
'converse-profile',
'converse-pubsub',
'converse-register',
'converse-roomslist',
'converse-rosterview',
......@@ -1571,9 +1572,11 @@
};
if (this.debug) {
this.connection.xmlInput = function (body) {
console.log(body);
_converse.log(body.outerHTML, Strophe.LogLevel.DEBUG, 'color: darkgoldenrod');
};
this.connection.xmlOutput = function (body) {
console.log(body);
_converse.log(body.outerHTML, Strophe.LogLevel.DEBUG, 'color: darkcyan');
};
}
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment