Show fingerprints in the user details modal

updates #497
This commit is contained in:
JC Brand 2018-07-22 16:12:36 +02:00
parent ce447e4035
commit bcd6845756
12 changed files with 698 additions and 383 deletions

View File

@ -2346,7 +2346,7 @@
--primary: #387592;
--secondary: #6c757d;
--success: #3AA569;
--info: #17a2b8;
--info: #3AA569;
--warning: #ffc107;
--danger: #E77051;
--light: #f8f9fa;
@ -3594,24 +3594,24 @@
box-shadow: 0 0 0 0.2rem rgba(58, 165, 105, 0.5); }
#conversejs .btn-info {
color: #fff;
background-color: #17a2b8;
border-color: #17a2b8; }
background-color: #3AA569;
border-color: #3AA569; }
#conversejs .btn-info:hover {
color: #fff;
background-color: #138496;
border-color: #117a8b; }
background-color: #308957;
border-color: #2d7f51; }
#conversejs .btn-info:focus, #conversejs .btn-info.focus {
box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5); }
box-shadow: 0 0 0 0.2rem rgba(58, 165, 105, 0.5); }
#conversejs .btn-info.disabled, #conversejs .btn-info:disabled {
color: #fff;
background-color: #17a2b8;
border-color: #17a2b8; }
background-color: #3AA569;
border-color: #3AA569; }
#conversejs .btn-info:not(:disabled):not(.disabled):active, #conversejs .btn-info:not(:disabled):not(.disabled).active, .show > #conversejs .btn-info.dropdown-toggle {
color: #fff;
background-color: #117a8b;
border-color: #10707f; }
background-color: #2d7f51;
border-color: #29764b; }
#conversejs .btn-info:not(:disabled):not(.disabled):active:focus, #conversejs .btn-info:not(:disabled):not(.disabled).active:focus, .show > #conversejs .btn-info.dropdown-toggle:focus {
box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5); }
box-shadow: 0 0 0 0.2rem rgba(58, 165, 105, 0.5); }
#conversejs .btn-warning {
color: #212529;
background-color: #ffc107;
@ -3753,25 +3753,25 @@
#conversejs .btn-outline-success:not(:disabled):not(.disabled):active:focus, #conversejs .btn-outline-success:not(:disabled):not(.disabled).active:focus, .show > #conversejs .btn-outline-success.dropdown-toggle:focus {
box-shadow: 0 0 0 0.2rem rgba(58, 165, 105, 0.5); }
#conversejs .btn-outline-info {
color: #17a2b8;
color: #3AA569;
background-color: transparent;
background-image: none;
border-color: #17a2b8; }
border-color: #3AA569; }
#conversejs .btn-outline-info:hover {
color: #fff;
background-color: #17a2b8;
border-color: #17a2b8; }
background-color: #3AA569;
border-color: #3AA569; }
#conversejs .btn-outline-info:focus, #conversejs .btn-outline-info.focus {
box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5); }
box-shadow: 0 0 0 0.2rem rgba(58, 165, 105, 0.5); }
#conversejs .btn-outline-info.disabled, #conversejs .btn-outline-info:disabled {
color: #17a2b8;
color: #3AA569;
background-color: transparent; }
#conversejs .btn-outline-info:not(:disabled):not(.disabled):active, #conversejs .btn-outline-info:not(:disabled):not(.disabled).active, .show > #conversejs .btn-outline-info.dropdown-toggle {
color: #fff;
background-color: #17a2b8;
border-color: #17a2b8; }
background-color: #3AA569;
border-color: #3AA569; }
#conversejs .btn-outline-info:not(:disabled):not(.disabled):active:focus, #conversejs .btn-outline-info:not(:disabled):not(.disabled).active:focus, .show > #conversejs .btn-outline-info.dropdown-toggle:focus {
box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5); }
box-shadow: 0 0 0 0.2rem rgba(58, 165, 105, 0.5); }
#conversejs .btn-outline-warning {
color: #ffc107;
background-color: transparent;
@ -4582,11 +4582,11 @@
background-color: #2d7f51; }
#conversejs .badge-info {
color: #fff;
background-color: #17a2b8; }
background-color: #3AA569; }
#conversejs .badge-info[href]:hover, #conversejs .badge-info[href]:focus {
color: #fff;
text-decoration: none;
background-color: #117a8b; }
background-color: #2d7f51; }
#conversejs .badge-warning {
color: #212529;
background-color: #ffc107; }
@ -4658,13 +4658,13 @@
#conversejs .alert-success .alert-link {
color: #11301f; }
#conversejs .alert-info {
color: #0c5460;
background-color: #d1ecf1;
border-color: #bee5eb; }
color: #1e5637;
background-color: #d8ede1;
border-color: #c8e6d5; }
#conversejs .alert-info hr {
border-top-color: #abdde5; }
border-top-color: #b6dec8; }
#conversejs .alert-info .alert-link {
color: #062c33; }
color: #11301f; }
#conversejs .alert-warning {
color: #856404;
background-color: #fff3cd;
@ -4782,15 +4782,15 @@
background-color: #1e5637;
border-color: #1e5637; }
#conversejs .list-group-item-info {
color: #0c5460;
background-color: #bee5eb; }
color: #1e5637;
background-color: #c8e6d5; }
#conversejs .list-group-item-info.list-group-item-action:hover, #conversejs .list-group-item-info.list-group-item-action:focus {
color: #0c5460;
background-color: #abdde5; }
color: #1e5637;
background-color: #b6dec8; }
#conversejs .list-group-item-info.list-group-item-action.active {
color: #fff;
background-color: #0c5460;
border-color: #0c5460; }
background-color: #1e5637;
border-color: #1e5637; }
#conversejs .list-group-item-warning {
color: #856404;
background-color: #ffeeba; }
@ -5179,11 +5179,11 @@
#conversejs button.bg-success:focus {
background-color: #2d7f51 !important; }
#conversejs .bg-info {
background-color: #17a2b8 !important; }
background-color: #3AA569 !important; }
#conversejs a.bg-info:hover, #conversejs a.bg-info:focus,
#conversejs button.bg-info:hover,
#conversejs button.bg-info:focus {
background-color: #117a8b !important; }
background-color: #2d7f51 !important; }
#conversejs .bg-warning {
background-color: #ffc107 !important; }
#conversejs a.bg-warning:hover, #conversejs a.bg-warning:focus,
@ -5239,7 +5239,7 @@
#conversejs .border-success {
border-color: #3AA569 !important; }
#conversejs .border-info {
border-color: #17a2b8 !important; }
border-color: #3AA569 !important; }
#conversejs .border-warning {
border-color: #ffc107 !important; }
#conversejs .border-danger {
@ -6792,9 +6792,9 @@
#conversejs a.text-success:hover, #conversejs a.text-success:focus {
color: #2d7f51 !important; }
#conversejs .text-info {
color: #17a2b8 !important; }
color: #3AA569 !important; }
#conversejs a.text-info:hover, #conversejs a.text-info:focus {
color: #117a8b !important; }
color: #2d7f51 !important; }
#conversejs .text-warning {
color: #ffc107 !important; }
#conversejs a.text-warning:hover, #conversejs a.text-warning:focus {
@ -7213,6 +7213,9 @@ body.reset {
@media screen and (max-height: 450px) {
#conversejs {
left: 0; } }
#conversejs .btn--small {
font-size: 80%;
font-weight: normal; }
#conversejs form .form-group {
margin-bottom: 2em; }
#conversejs form .form-check-label {
@ -7291,6 +7294,10 @@ body.reset {
#conversejs #user-profile-modal label {
font-weight: bold; }
#conversejs .fingerprint-trust {
display: flex;
justify-content: space-between;
font-size: 95%; }
#conversejs .chatbox-navback {
display: none; }

681
dist/converse.js vendored

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: Converse.js 0.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-07-22 11:17+0200\n"
"PO-Revision-Date: 2018-07-22 12:12+0200\n"
"PO-Revision-Date: 2018-07-22 15:37+0200\n"
"Last-Translator: JC Brand <jc@opkode.com>\n"
"Language-Team: Afrikaans <https://hosted.weblate.org/projects/conversejs/"
"translations/af/>\n"

View File

@ -1,4 +1,9 @@
#conversejs {
.btn--small {
font-size: 80%;
font-weight: normal;
}
form {
.form-group {
margin-bottom: 2em;

View File

@ -4,4 +4,9 @@
font-weight: bold;
}
}
.fingerprint-trust {
display: flex;
justify-content: space-between;
font-size: 95%;
}
}

View File

@ -34,6 +34,8 @@ $green: #3AA569;
$dark-green: #1E9652;
$darkest-green: #0E763B;
$info: $green !default;
$lightest-green: #E7FBF0;
$light-green: #5CBC86;
$green: #3AA569;

View File

@ -245,7 +245,7 @@
devicelist = _converse.devicelists.get(contact_jid);
expect(devicelist.devices.length).toBe(1);
let device = devicelist.devices.at(0);
expect(device.get('bundle').identity_key).toBe(3333);
expect(device.get('bundle').identity_key).toBe('3333');
expect(device.get('bundle').signed_prekey.public_key).toBe('1111');
expect(device.get('bundle').signed_prekey.id).toBe(4223);
expect(device.get('bundle').signed_prekey.signature).toBe('2222');
@ -276,7 +276,7 @@
devicelist = _converse.devicelists.get(contact_jid);
expect(devicelist.devices.length).toBe(1);
device = devicelist.devices.at(0);
expect(device.get('bundle').identity_key).toBe(7777);
expect(device.get('bundle').identity_key).toBe('7777');
expect(device.get('bundle').signed_prekey.public_key).toBe('5555');
expect(device.get('bundle').signed_prekey.id).toBe(4223);
expect(device.get('bundle').signed_prekey.signature).toBe('6666');
@ -309,7 +309,7 @@
expect(devicelist.devices.at(0).get('id')).toBe('555');
expect(devicelist.devices.at(1).get('id')).toBe('123456789');
device = devicelist.devices.at(1);
expect(device.get('bundle').identity_key).toBe(1111);
expect(device.get('bundle').identity_key).toBe('1111');
expect(device.get('bundle').signed_prekey.public_key).toBe('8888');
expect(device.get('bundle').signed_prekey.id).toBe(9999);
expect(device.get('bundle').signed_prekey.signature).toBe('3333');
@ -396,8 +396,15 @@
`</publish>`+
`</pubsub>`+
`</iq>`)
done();
});
const stanza = $iq({
'from': _converse.bare_jid,
'id': iq_stanza.getAttribute('id'),
'to': _converse.bare_jid,
'type': 'result'});
_converse.connection._dataRecv(test_utils.createRequest(stanza));
return _converse.api.waitUntil('OMEMOInitialized');
}).then(done).catch(_.partial(console.error, _));
}));
it("adds a toolbar button for starting an encrypted chat session",
@ -494,7 +501,6 @@
'type': 'result'});
_converse.connection._dataRecv(test_utils.createRequest(stanza));
return test_utils.waitUntil(() => {
return _.filter(
_converse.connection.IQ_stanzas,
@ -565,6 +571,117 @@
done();
}).catch(_.partial(console.error, _));
}));
it("shows OMEMO device fingerprints in the user details modal",
mock.initConverseWithPromises(
null, ['rosterGroupsFetched'], {},
function (done, _converse) {
let iq_stanza;
test_utils.createContacts(_converse, 'current', 1);
const contact_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@localhost';
test_utils.openChatBoxFor(_converse, contact_jid);
// We simply emit, to avoid doing all the setup work
_converse.emit('OMEMOInitialized');
const view = _converse.chatboxviews.get(contact_jid);
const show_modal_button = view.el.querySelector('.show-user-details-modal');
show_modal_button.click();
const modal = view.user_details_modal;
test_utils.waitUntil(() => u.isVisible(modal.el), 1000).then(() => {
return test_utils.waitUntil(() => {
return _.filter(
_converse.connection.IQ_stanzas,
(iq) => {
const node = iq.nodeTree.querySelector('iq[to="'+contact_jid+'"] query[node="eu.siacs.conversations.axolotl.devicelist"]');
if (node) { iq_stanza = iq.nodeTree; }
return node;
}).length;});
}).then(() => {
iq_stanza;
expect(iq_stanza.outerHTML).toBe(
`<iq type="get" from="dummy@localhost" to="max.frankfurter@localhost" xmlns="jabber:client" id="${iq_stanza.getAttribute('id')}">`+
`<query xmlns="http://jabber.org/protocol/disco#items" node="eu.siacs.conversations.axolotl.devicelist"/>`+
`</iq>`);
const stanza = $iq({
'from': contact_jid,
'id': iq_stanza.getAttribute('id'),
'to': _converse.bare_jid,
'type': 'result',
}).c('query', {
'xmlns': 'http://jabber.org/protocol/disco#items',
'node': 'eu.siacs.conversations.axolotl.devicelist'
}).c('device', {'id': '555'}).up()
_converse.connection._dataRecv(test_utils.createRequest(stanza));
return test_utils.waitUntil(() => u.isVisible(modal.el), 1000).then(function () {
return test_utils.waitUntil(() => {
return _.filter(
_converse.connection.IQ_stanzas,
(iq) => {
const node = iq.nodeTree.querySelector('iq[to="'+contact_jid+'"] items[node="eu.siacs.conversations.axolotl.bundles:555"]');
if (node) { iq_stanza = iq.nodeTree; }
return node;
}).length;});
});
}).then(() => {
expect(iq_stanza.outerHTML).toBe(
`<iq type="get" from="dummy@localhost" to="max.frankfurter@localhost" xmlns="jabber:client" id="${iq_stanza.getAttribute('id')}">`+
`<pubsub xmlns="http://jabber.org/protocol/pubsub">`+
`<items node="eu.siacs.conversations.axolotl.bundles:555"/>`+
`</pubsub>`+
`</iq>`);
const stanza = $iq({
'from': contact_jid,
'id': iq_stanza.getAttribute('id'),
'to': _converse.bare_jid,
'type': 'result',
}).c('pubsub', {
'xmlns': 'http://jabber.org/protocol/pubsub'
}).c('items', {'node': "eu.siacs.conversations.axolotl.bundles:555"})
.c('item')
.c('bundle', {'xmlns': 'eu.siacs.conversations.axolotl'})
.c('signedPreKeyPublic', {'signedPreKeyId': '4223'}).t(btoa('1111')).up()
.c('signedPreKeySignature').t(btoa('2222')).up()
.c('identityKey').t(btoa('3333')).up()
.c('prekeys')
.c('preKeyPublic', {'preKeyId': '1'}).t(btoa('1001')).up()
.c('preKeyPublic', {'preKeyId': '2'}).t(btoa('1002')).up()
.c('preKeyPublic', {'preKeyId': '3'}).t(btoa('1003'));
_converse.connection._dataRecv(test_utils.createRequest(stanza));
const view = _converse.chatboxviews.get(contact_jid);
const modal = view.user_details_modal;
return test_utils.waitUntil(() => modal.el.querySelectorAll('.fingerprints .fingerprint').length);
}).then(() => {
const view = _converse.chatboxviews.get(contact_jid);
const modal = view.user_details_modal;
expect(modal.el.querySelectorAll('.fingerprints .fingerprint').length).toBe(1);
const el = modal.el.querySelector('.fingerprints .fingerprint');
expect(el.textContent).toBe('f56d6351aa71cff0debea014d13525e42036187a');
expect(modal.el.querySelectorAll('input[type="radio"]').length).toBe(2);
let trusted_radio = modal.el.querySelector('input[type="radio"][name="555"][value="1"]');
expect(trusted_radio.checked).toBe(true);
let untrusted_radio = modal.el.querySelector('input[type="radio"][name="555"][value="-1"]');
expect(untrusted_radio.checked).toBe(false);
// Test that the device can be set to untrusted
untrusted_radio.click();
trusted_radio = document.querySelector('input[type="radio"][name="555"][value="1"]');
expect(trusted_radio.hasAttribute('checked')).toBe(false);
untrusted_radio = document.querySelector('input[type="radio"][name="555"][value="-1"]');
expect(untrusted_radio.hasAttribute('checked')).toBe(true);
done();
});
}));
});
describe("A chatbox with an active OMEMO session", function() {

View File

@ -389,7 +389,6 @@
function (done, _converse) {
_converse.roster_groups = true;
spyOn(_converse, 'emit');
spyOn(_converse.rosterview, 'update').and.callThrough();
_converse.rosterview.render();
test_utils.openControlBox();
@ -430,7 +429,6 @@
function (done, _converse) {
_converse.roster_groups = true;
spyOn(_converse, 'emit');
spyOn(_converse.rosterview, 'update').and.callThrough();
_converse.rosterview.render();
@ -477,7 +475,6 @@
_converse.roster_groups = true;
var groups = ['colleagues', 'friends'];
spyOn(_converse, 'emit');
spyOn(_converse.rosterview, 'update').and.callThrough();
test_utils.openControlBox();
_converse.rosterview.render();
@ -576,7 +573,6 @@
null, ['rosterGroupsFetched'], {},
function (done, _converse) {
spyOn(_converse, 'emit');
spyOn(_converse.rosterview, 'update').and.callThrough();
test_utils.openControlBox();
_converse.roster.create({
@ -726,7 +722,6 @@
var i, t;
test_utils.openControlBox();
spyOn(_converse, 'emit');
spyOn(_converse.rosterview, 'update').and.callThrough();
for (i=0; i<mock.pend_names.length; i++) {
_converse.roster.create({
@ -908,7 +903,6 @@
test_utils.waitUntil(() => $(_converse.rosterview.el).find('.roster-group li').length, 700)
.then(function () {
var jid, t;
spyOn(_converse, 'emit');
spyOn(_converse.rosterview, 'update').and.callThrough();
var $roster = $(_converse.rosterview.el);
for (var i=0; i<mock.cur_names.length; i++) {
@ -935,7 +929,6 @@
return $(_converse.rosterview.el).find('.roster-group li').length;
}, 700).then(function () {
var jid, t;
spyOn(_converse, 'emit');
spyOn(_converse.rosterview, 'update').and.callThrough();
var $roster = $(_converse.rosterview.el);
for (var i=0; i<mock.cur_names.length; i++) {
@ -963,7 +956,6 @@
return $(_converse.rosterview.el).find('.roster-group li').length;
}, 700).then(function () {
var jid, t;
spyOn(_converse, 'emit');
spyOn(_converse.rosterview, 'update').and.callThrough();
var $roster = $(_converse.rosterview.el);
for (var i=0; i<mock.cur_names.length; i++) {
@ -991,7 +983,6 @@
return $(_converse.rosterview.el).find('.roster-group li').length;
}, 700).then(function () {
var jid, t;
spyOn(_converse, 'emit');
spyOn(_converse.rosterview, 'update').and.callThrough();
var $roster = $(_converse.rosterview.el);
for (var i=0; i<mock.cur_names.length; i++) {
@ -1020,7 +1011,6 @@
}, 500)
.then(function () {
var jid, t;
spyOn(_converse, 'emit');
spyOn(_converse.rosterview, 'update').and.callThrough();
var $roster = $(_converse.rosterview.el);
for (var i=0; i<mock.cur_names.length; i++) {
@ -1151,7 +1141,6 @@
names.push($(item).text().replace(/^\s+|\s+$/g, ''));
}
};
spyOn(_converse, 'emit');
spyOn(_converse.rosterview, 'update').and.callThrough();
spyOn(_converse.controlboxtoggle, 'showControlBox').and.callThrough();
for (i=0; i<mock.req_names.length; i++) {

View File

@ -239,32 +239,29 @@
events: {
'click button.remove-contact': 'removeContact',
'click button.refresh-contact': 'refreshContact'
'click button.refresh-contact': 'refreshContact',
'click .fingerprint-trust .btn input': 'toggleDeviceTrust'
},
initialize () {
_converse.BootstrapModal.prototype.initialize.apply(this, arguments);
this.model.on('contactAdded', this.registerContactEventHandlers, this);
this.model.on('change', this.render, this);
this.registerContactEventHandlers();
_converse.emit('userDetailsModalInitialized', this.model);
},
toHTML () {
return tpl_user_details_modal(_.extend(
this.model.toJSON(),
this.model.vcard.toJSON(), {
'_': _,
'__': __,
'view': this,
'_converse': _converse,
'allow_contact_removal': _converse.allow_contact_removal,
'alt_profile_image': __("The User's Profile Image"),
'display_name': this.model.getDisplayName(),
'is_roster_contact': !_.isUndefined(this.model.contact),
'label_close': __('Close'),
'label_email': __('Email'),
'label_fullname': __('Full Name'),
'label_jid': __('Jabber ID'),
'label_nickname': __('Nickname'),
'label_remove': __('Remove as contact'),
'label_refresh': __('Refresh'),
'label_role': __('Role'),
'label_url': __('URL')
'is_roster_contact': !_.isUndefined(this.model.contact)
}));
},

View File

@ -44,7 +44,7 @@
}
});
return {
'identity_key': parseInt(bundle_el.querySelector('identityKey').textContent, 10),
'identity_key': bundle_el.querySelector('identityKey').textContent,
'signed_prekey': {
'id': parseInt(signed_prekey_public_el.getAttribute('signedPreKeyId'), 10),
'public_key': signed_prekey_public_el.textContent,
@ -65,6 +65,27 @@
overrides: {
UserDetailsModal: {
events: {
'click .fingerprint-trust .btn input': 'toggleDeviceTrust'
},
initialize () {
const { _converse } = this.__super__;
const jid = this.model.get('jid');
this.devicelist = _converse.devicelists.get(jid) || _converse.devicelists.create({'jid': jid});
this.devicelist.devices.on('change:bundle', this.render, this);
this.devicelist.devices.on('change:trusted', this.render, this);
return this.__super__.initialize.apply(this, arguments);
},
toggleDeviceTrust (ev) {
const radio = ev.target;
const device = this.devicelist.devices.get(radio.getAttribute('name'));
device.save('trusted', parseInt(radio.value, 10));
}
},
ChatBox: {
getBundlesAndBuildSessions () {
@ -77,7 +98,6 @@
this.buildSessions(devices)
.then(() => resolve(devices))
.catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
}).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
}).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
}).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
@ -194,6 +214,27 @@
_converse.NUM_PREKEYS = 100; // Set here so that tests can override
function generateFingerprint (device) {
return new Promise((resolve, reject) => {
device.getBundle().then((bundle) => {
// TODO: only generate fingerprints when necessary
crypto.subtle.digest('SHA-1', u.base64ToArrayBuffer(bundle['identity_key']))
.then((fp) => {
bundle['fingerprint'] = u.arrayBufferToHex(fp);
device.save('bundle', bundle);
device.trigger('change:bundle'); // Doesn't get triggered automatically due to pass-by-reference
resolve();
}).catch(reject);
});
});
}
_converse.getFingerprintsForContact = function (jid) {
return new Promise((resolve, reject) => {
_converse.getDevicesForContact(jid)
.then((devices) => Promise.all(devices.map(d => generateFingerprint(d))).then(resolve).catch(reject));
});
}
_converse.getDevicesForContact = function (jid) {
return new Promise((resolve, reject) => {
@ -405,14 +446,15 @@
'from': _converse.bare_jid,
'to': this.get('jid')
}).c('pubsub', {'xmlns': Strophe.NS.PUBSUB})
.c('items', {'xmlns': `${Strophe.NS.OMEMO_BUNDLES}:${this.get('id')}`});
.c('items', {'node': `${Strophe.NS.OMEMO_BUNDLES}:${this.get('id')}`});
_converse.connection.sendIQ(
stanza,
(iq) => {
const publish_el = sizzle(`items[node="${Strophe.NS.OMEMO_BUNDLES}:${this.get('id')}"]`, stanza).pop();
const bundle_el = sizzle(`bundle[xmlns="${Strophe.NS.OMEMO}"]`, publish_el).pop();
this.save(parseBundle(bundle_el));
resolve();
const publish_el = sizzle(`items[node="${Strophe.NS.OMEMO_BUNDLES}:${this.get('id')}"]`, iq).pop(),
bundle_el = sizzle(`bundle[xmlns="${Strophe.NS.OMEMO}"]`, publish_el).pop(),
bundle = parseBundle(bundle_el);
this.save('bundle', bundle);
resolve(bundle);
},
reject,
_converse.IQ_TIMEOUT
@ -479,7 +521,7 @@
(iq) => {
_.forEach(
iq.querySelectorAll('device'),
(dev) => this.devices.create({'id': dev.getAttribute('id')})
(dev) => this.devices.create({'id': dev.getAttribute('id'), 'jid': this.get('jid')})
);
resolve();
},
@ -493,7 +535,7 @@
* server.
* https://xmpp.org/extensions/xep-0384.html#usecases-announcing
*/
this.devices.create({'id': device_id});
this.devices.create({'id': device_id, 'jid': this.get('jid')});
return new Promise((resolve, reject) => {
const stanza = $iq({
'from': _converse.bare_jid,
@ -589,7 +631,7 @@
jid = stanza.getAttribute('from'),
bundle_el = sizzle(`item > bundle`, items_el).pop(),
devicelist = _converse.devicelists.get(jid) || _converse.devicelists.create({'jid': jid}),
device = devicelist.devices.get(device_id) || devicelist.devices.create({'id': device_id});
device = devicelist.devices.get(device_id) || devicelist.devices.create({'id': device_id, 'jid': jid});
device.save({'bundle': parseBundle(bundle_el)});
}
@ -613,7 +655,7 @@
if (dev) {
dev.save({'active': true});
} else {
devices.create({'id': device_id})
devices.create({'id': device_id, 'jid': jid})
}
});
// Make sure our own device is on the list (i.e. if it was
@ -661,6 +703,11 @@
_converse.api.listen.on('statusInitialized', initOMEMO);
_converse.api.listen.on('addClientFeatures',
() => _converse.api.disco.own.features.add(Strophe.NS.OMEMO_DEVICELIST+"notify"));
_converse.api.listen.on('userDetailsModalInitialized', (contact) => {
const jid = contact.get('jid');
_converse.getFingerprintsForContact(jid).catch(_.partial(_converse.log, _, Strophe.LogLevel.ERROR));
});
}
});
}));

View File

@ -1,39 +1,70 @@
<div class="modal fade" id="user-profile-modal" tabindex="-1" role="dialog" aria-labelledby="user-profile-modal-label" aria-hidden="true">
<div class="modal fade" id="user-details-modal" tabindex="-1" role="dialog" aria-labelledby="user-details-modal-label" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="user-profile-modal-label">{{{o.display_name}}}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="{{{o.label_close}}}"><span aria-hidden="true">&times;</span></button>
<h5 class="modal-title" id="user-details-modal-label">{{{o.display_name}}}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="{{{o.__('Close')}}}"><span aria-hidden="true">&times;</span></button>
</div>
<div class="modal-body">
{[ if (o.image) { ]}
<img alt="{{{o.alt_profile_image}}}"
<img alt="{{{o.__('The User\'s Profile Image')}}}"
class="img-thumbnail avatar align-self-center mb-3"
height="100" width="100" src="data:{{{o.image_type}}};base64,{{{o.image}}}"/>
{[ } ]}
{[ if (o.fullname) { ]}
<p><label>{{{o.label_fullname}}}:</label>&nbsp;{{{o.fullname}}}</p>
<p><label>{{{o.__('Full Name')}}}:</label>&nbsp;{{{o.fullname}}}</p>
{[ } ]}
<p><label>{{{o.label_jid}}}:</label>&nbsp;{{{o.jid}}}</p>
<p><label>{{{o.__('XMPP Address')}}}:</label>&nbsp;{{{o.jid}}}</p>
{[ if (o.nickname) { ]}
<p><label>{{{o.label_nickname}}}:</label>&nbsp;{{{o.nickname}}}</p>
<p><label>{{{o.__('Nickname')}}}:</label>&nbsp;{{{o.nickname}}}</p>
{[ } ]}
{[ if (o.url) { ]}
<p><label>{{{o.label_url}}}:</label>&nbsp;<a target="_blank" rel="noopener" href="{{{o.url}}}">{{{o.url}}}</a></p>
<p><label>{{{o.__('URL')}}}:</label>&nbsp;<a target="_blank" rel="noopener" href="{{{o.url}}}">{{{o.url}}}</a></p>
{[ } ]}
{[ if (o.email) { ]}
<p><label>{{{o.label_email}}}:</label>&nbsp;<a href="mailto:{{{o.email}}}">{{{o.email}}}</a></p>
<p><label>{{{o.__('Email')}}}:</label>&nbsp;<a href="mailto:{{{o.email}}}">{{{o.email}}}</a></p>
{[ } ]}
{[ if (o.role) { ]}
<p><label>{{{o.label_role}}}:</label>&nbsp;{{{o.role}}}</p>
<p><label>{{{o.__('Role')}}}:</label>&nbsp;{{{o.role}}}</p>
{[ } ]}
{[ if (o._converse.pluggable.plugins['converse-omemo'].enabled()) { ]}
<hr>
<ul class="list-group fingerprints">
<li class="list-group-item active">{{{o.__('OMEMO Fingerprints')}}}</li>
{[ if (!o.view.devicelist.devices) { ]}
<li class="list-group-item"><span class="spinner fa fa-spinner centered"/></li>
{[ } ]}
{[ if (o.view.devicelist.devices) { ]}
{[ o.view.devicelist.devices.each(function (device) { ]}
{[ if (device.get('bundle') && device.get('bundle').fingerprint) { ]}
<li class="list-group-item">
<form class="fingerprint-trust">
<span class="fingerprint">{{{device.get('bundle').fingerprint}}}</span>
<div class="btn-group btn-group-toggle">
<label class="btn btn--small {[ if (device.get('trusted') !== -1) { ]} btn-primary active {[ } else { ]} btn-secondary {[ } ]}">
<input type="radio" name="{{{device.get('id')}}}" value="1"
{[ if (device.get('trusted') !== -1) { ]} checked="checked" {[ } ]}>{{{o.__('Trusted')}}}
</label>
<label class="btn btn--small {[ if (device.get('trusted') === -1) { ]} btn-primary active {[ } else { ]} btn-secondary {[ } ]}">
<input type="radio" name="{{{device.get('id')}}}" value="-1"
{[ if (device.get('trusted') === -1) { ]} checked="checked" {[ } ]}>{{{o.__('Untrusted')}}}
</label>
</div>
</form>
</li>
{[ } ]}
{[ }); ]}
{[ } ]}
</ul>
{[ } ]}
</div>
<div class="modal-footer">
{[ if (o.allow_contact_removal && o.is_roster_contact) { ]}
<button type="button" class="btn btn-danger remove-contact"><i class="fa fa-trash"> </i>{{{o.label_remove}}}</button>
<button type="button" class="btn btn-danger remove-contact"><i class="fa fa-trash"> </i>{{{o.__('Remove as contact')}}}</button>
{[ } ]}
<button type="button" class="btn btn-info refresh-contact"><i class="fa fa-refresh"> </i>{{{o.label_refresh}}}</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{{o.label_close}}}</button>
<button type="button" class="btn btn-info refresh-contact"><i class="fa fa-refresh"> </i>{{{o.__('Refresh')}}}</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{{o.__('Close')}}}</button>
</div>
</div>
</div>

View File

@ -846,6 +846,22 @@
return result;
};
u.arrayBufferToHex = function (ab) {
const hexCodes = [];
const padding = '00000000';
const view = new window.DataView(ab);
for (var i = 0; i < view.byteLength; i += 4) {
// Using getUint32 reduces the number of iterations needed (we process 4 bytes each time)
const value = view.getUint32(i)
// toString(16) will give the hex representation of the number without padding
const stringValue = value.toString(16)
// We use concatenation and slice for padding
const paddedValue = (padding + stringValue).slice(-padding.length)
hexCodes.push(paddedValue);
}
return hexCodes.join("");
};
u.arrayBufferToString = function (ab) {
var enc = new TextDecoder("utf-8");
return enc.decode(new Uint8Array(ab));