From 151c698d46bcca9942adb675c56ea62888a14bc7 Mon Sep 17 00:00:00 2001 From: Hippo Date: Sun, 8 Jan 2023 20:52:30 +0530 Subject: [PATCH 1/6] Split omemo/utils.js into headless and UI versions This replaces 'plugins/omemo' with 'plugins/omemo-views' and 'headless/plugins/omemo'. Most of the original code is still in the former, but the contents of 'util' has been moved to the headless version. Still pending: moving 'index.js' and all the other files in the plugin! --- docs/source/plugin_development.rst | 2 +- src/converse.js | 2 +- src/{ => headless}/plugins/omemo/consts.js | 0 src/{ => headless}/plugins/omemo/utils.js | 155 ++---------------- src/plugins/{omemo => omemo-views}/api.js | 0 src/plugins/{omemo => omemo-views}/device.js | 0 .../{omemo => omemo-views}/devicelist.js | 0 .../{omemo => omemo-views}/devicelists.js | 0 src/plugins/{omemo => omemo-views}/devices.js | 0 src/plugins/{omemo => omemo-views}/errors.js | 0 .../{omemo => omemo-views}/fingerprints.js | 0 src/plugins/{omemo => omemo-views}/index.js | 19 ++- .../{omemo => omemo-views}/mixins/converse.js | 0 src/plugins/{omemo => omemo-views}/profile.js | 0 src/plugins/{omemo => omemo-views}/store.js | 0 .../templates/fingerprints.js | 2 +- .../templates/profile.js | 2 +- .../tests/corrections.js | 0 .../tests/media-sharing.js | 0 .../{omemo => omemo-views}/tests/muc.js | 0 .../{omemo => omemo-views}/tests/omemo.js | 0 src/plugins/omemo-views/utils.js | 154 +++++++++++++++++ .../profile/templates/profile_modal.js | 4 +- src/shared/constants.js | 2 +- 24 files changed, 186 insertions(+), 156 deletions(-) rename src/{ => headless}/plugins/omemo/consts.js (100%) rename src/{ => headless}/plugins/omemo/utils.js (84%) rename src/plugins/{omemo => omemo-views}/api.js (100%) rename src/plugins/{omemo => omemo-views}/device.js (100%) rename src/plugins/{omemo => omemo-views}/devicelist.js (100%) rename src/plugins/{omemo => omemo-views}/devicelists.js (100%) rename src/plugins/{omemo => omemo-views}/devices.js (100%) rename src/plugins/{omemo => omemo-views}/errors.js (100%) rename src/plugins/{omemo => omemo-views}/fingerprints.js (100%) rename src/plugins/{omemo => omemo-views}/index.js (93%) rename src/plugins/{omemo => omemo-views}/mixins/converse.js (100%) rename src/plugins/{omemo => omemo-views}/profile.js (100%) rename src/plugins/{omemo => omemo-views}/store.js (100%) rename src/plugins/{omemo => omemo-views}/templates/fingerprints.js (96%) rename src/plugins/{omemo => omemo-views}/templates/profile.js (97%) rename src/plugins/{omemo => omemo-views}/tests/corrections.js (100%) rename src/plugins/{omemo => omemo-views}/tests/media-sharing.js (100%) rename src/plugins/{omemo => omemo-views}/tests/muc.js (100%) rename src/plugins/{omemo => omemo-views}/tests/omemo.js (100%) create mode 100644 src/plugins/omemo-views/utils.js diff --git a/docs/source/plugin_development.rst b/docs/source/plugin_development.rst index a6feae307d..21112d7be6 100644 --- a/docs/source/plugin_development.rst +++ b/docs/source/plugin_development.rst @@ -33,7 +33,7 @@ modular and self-contained way, without having to change other files. This ensures that plugins are fully optional (one of the design goals of Converse) and can be removed from the main build without breaking the app. -For example, the ``converse-omemo``, +For example, the ``converse-omemo-views``, ``converse-rosterview``, ``converse-dragresize``, ``converse-minimize``, ``converse-muc`` and ``converse-muc-views`` plugins can all be removed from the build without breaking the app. diff --git a/src/converse.js b/src/converse.js index e93ee169a1..355ac63ce1 100644 --- a/src/converse.js +++ b/src/converse.js @@ -28,7 +28,7 @@ import "./plugins/muc-views/index.js"; // Views related to MUC import "./plugins/minimize/index.js"; // Allows chat boxes to be minimized import "./plugins/notifications/index.js"; import "./plugins/profile/index.js"; -import "./plugins/omemo/index.js"; +import "./plugins/omemo-views/index.js"; import "./plugins/push/index.js"; // XEP-0357 Push Notifications import "./plugins/register/index.js"; // XEP-0077 In-band registration import "./plugins/roomslist/index.js"; // Show currently open chat rooms diff --git a/src/plugins/omemo/consts.js b/src/headless/plugins/omemo/consts.js similarity index 100% rename from src/plugins/omemo/consts.js rename to src/headless/plugins/omemo/consts.js diff --git a/src/plugins/omemo/utils.js b/src/headless/plugins/omemo/utils.js similarity index 84% rename from src/plugins/omemo/utils.js rename to src/headless/plugins/omemo/utils.js index 8a462310b0..6dc749579c 100644 --- a/src/plugins/omemo/utils.js +++ b/src/headless/plugins/omemo/utils.js @@ -2,19 +2,11 @@ import concat from 'lodash-es/concat'; import difference from 'lodash-es/difference'; import log from '@converse/headless/log'; -import tpl_audio from 'templates/audio.js'; -import tpl_file from 'templates/file.js'; -import tpl_image from 'templates/image.js'; -import tpl_video from 'templates/video.js'; import { KEY_ALGO, UNTRUSTED, TAG_LENGTH } from './consts.js'; import { MIMETYPES_MAP } from 'utils/file.js'; import { __ } from 'i18n'; import { _converse, converse, api } from '@converse/headless/core'; -import { html } from 'lit'; import { initStorage } from '@converse/headless/utils/storage.js'; -import { isError } from '@converse/headless/utils/core.js'; -import { isAudioURL, isImageURL, isVideoURL, getURI } from '@converse/headless/utils/url.js'; -import { until } from 'lit/directives/until.js'; import { appendArrayBuffer, arrayBufferToBase64, @@ -25,7 +17,7 @@ import { stringToArrayBuffer } from '@converse/headless/utils/arraybuffer.js'; -const { Strophe, URI, sizzle, u } = converse.env; +const { Strophe, sizzle, u } = converse.env; export function formatFingerprint (fp) { fp = fp.replace(/^05/, ''); @@ -197,53 +189,13 @@ async function getAndDecryptFile (uri) { } } -function getTemplateForObjectURL (uri, obj_url, richtext) { - if (isError(obj_url)) { - return html`

${obj_url.message}

`; - } - - const file_url = uri.toString(); - if (isImageURL(file_url)) { - return tpl_image({ - 'src': obj_url, - 'onClick': richtext.onImgClick, - 'onLoad': richtext.onImgLoad - }); - } else if (isAudioURL(file_url)) { - return tpl_audio(obj_url); - } else if (isVideoURL(file_url)) { - return tpl_video(obj_url); - } else { - return tpl_file(obj_url, uri.filename()); - } - +export const omemo = { + decryptMessage, + encryptMessage, + formatFingerprint } -function addEncryptedFiles(text, offset, richtext) { - const objs = []; - try { - const parse_options = { 'start': /\b(aesgcm:\/\/)/gi }; - URI.withinString( - text, - (url, start, end) => { - objs.push({ url, start, end }); - return url; - }, - parse_options - ); - } catch (error) { - log.debug(error); - return; - } - objs.forEach(o => { - const uri = getURI(text.slice(o.start, o.end)); - const promise = getAndDecryptFile(uri) - .then(obj_url => getTemplateForObjectURL(uri, obj_url, richtext)); - - const template = html`${until(promise, '')}`; - richtext.addTemplateResult(o.start + offset, o.end + offset, template); - }); -} +// TODO: addEncryptedFiles (without the template part) export function handleEncryptedFiles (richtext) { if (!_converse.config.get('trusted')) { @@ -306,31 +258,14 @@ export function onChatBoxesInitialized () { }); } -export function onChatInitialized (el) { - el.listenTo(el.model.messages, 'add', message => { - if (message.get('is_encrypted') && !message.get('is_error')) { - el.model.save('omemo_supported', true); - } - }); - el.listenTo(el.model, 'change:omemo_supported', () => { - if (!el.model.get('omemo_supported') && el.model.get('omemo_active')) { - el.model.set('omemo_active', false); - } else { - // Manually trigger an update, setting omemo_active to - // false above will automatically trigger one. - el.querySelector('converse-chat-toolbar')?.requestUpdate(); - } - }); - el.listenTo(el.model, 'change:omemo_active', () => { - el.querySelector('converse-chat-toolbar').requestUpdate(); - }); -} +// TODO: add headless part of onChatInitialized export function getSessionCipher (jid, id) { const address = new libsignal.SignalProtocolAddress(jid, id); return new window.libsignal.SessionCipher(_converse.omemo_store, address); } + function getJIDForDecryption (attrs) { const from_jid = attrs.from_muc ? attrs.from_real_jid : attrs.from; if (!from_jid) { @@ -459,6 +394,7 @@ export function addKeysToMessageStanza (stanza, dicts, iv) { return Promise.resolve(stanza); } + /** * Given an XML element representing a user's OMEMO bundle, parse it * and return a map. @@ -676,6 +612,7 @@ export async function initOMEMO (reconnecting) { api.trigger('OMEMOInitialized'); } + async function onOccupantAdded (chatroom, occupant) { if (occupant.isSelf() || !chatroom.features.get('nonanonymous') || !chatroom.features.get('membersonly')) { return; @@ -710,71 +647,9 @@ async function checkOMEMOSupported (chatbox) { } } -function toggleOMEMO (ev) { - ev.stopPropagation(); - ev.preventDefault(); - const toolbar_el = u.ancestor(ev.target, 'converse-chat-toolbar'); - if (!toolbar_el.model.get('omemo_supported')) { - let messages; - if (toolbar_el.model.get('type') === _converse.CHATROOMS_TYPE) { - messages = [ - __( - 'Cannot use end-to-end encryption in this groupchat, ' + - 'either the groupchat has some anonymity or not all participants support OMEMO.' - ) - ]; - } else { - messages = [ - __( - "Cannot use end-to-end encryption because %1$s uses a client that doesn't support OMEMO.", - toolbar_el.model.contact.getDisplayName() - ) - ]; - } - return api.alert('error', __('Error'), messages); - } - toolbar_el.model.save({ 'omemo_active': !toolbar_el.model.get('omemo_active') }); -} - -export function getOMEMOToolbarButton (toolbar_el, buttons) { - const model = toolbar_el.model; - const is_muc = model.get('type') === _converse.CHATROOMS_TYPE; - let title; - if (model.get('omemo_supported')) { - const i18n_plaintext = __('Messages are being sent in plaintext'); - const i18n_encrypted = __('Messages are sent encrypted'); - title = model.get('omemo_active') ? i18n_encrypted : i18n_plaintext; - } else if (is_muc) { - title = __( - 'This groupchat needs to be members-only and non-anonymous in ' + - 'order to support OMEMO encrypted messages' - ); - } else { - title = __('OMEMO encryption is not supported'); - } +// TODO: add headless version of toggleOMEMO function here - let color; - if (model.get('omemo_supported')) { - if (model.get('omemo_active')) { - color = is_muc ? `var(--muc-color)` : `var(--chat-toolbar-btn-color)`; - } else { - color = `var(--error-color)`; - } - } else { - color = `var(--muc-toolbar-btn-disabled-color)`; - } - buttons.push(html` - - `); - return buttons; -} +// TODO: add headless version of getOMEMOToolbarButton (if any) async function getBundlesAndBuildSessions (chatbox) { @@ -858,9 +733,3 @@ export async function createOMEMOMessageStanza (chat, data) { stanza.c('encryption', { 'xmlns': Strophe.NS.EME, namespace: Strophe.NS.OMEMO }); return { message, stanza }; } - -export const omemo = { - decryptMessage, - encryptMessage, - formatFingerprint -} diff --git a/src/plugins/omemo/api.js b/src/plugins/omemo-views/api.js similarity index 100% rename from src/plugins/omemo/api.js rename to src/plugins/omemo-views/api.js diff --git a/src/plugins/omemo/device.js b/src/plugins/omemo-views/device.js similarity index 100% rename from src/plugins/omemo/device.js rename to src/plugins/omemo-views/device.js diff --git a/src/plugins/omemo/devicelist.js b/src/plugins/omemo-views/devicelist.js similarity index 100% rename from src/plugins/omemo/devicelist.js rename to src/plugins/omemo-views/devicelist.js diff --git a/src/plugins/omemo/devicelists.js b/src/plugins/omemo-views/devicelists.js similarity index 100% rename from src/plugins/omemo/devicelists.js rename to src/plugins/omemo-views/devicelists.js diff --git a/src/plugins/omemo/devices.js b/src/plugins/omemo-views/devices.js similarity index 100% rename from src/plugins/omemo/devices.js rename to src/plugins/omemo-views/devices.js diff --git a/src/plugins/omemo/errors.js b/src/plugins/omemo-views/errors.js similarity index 100% rename from src/plugins/omemo/errors.js rename to src/plugins/omemo-views/errors.js diff --git a/src/plugins/omemo/fingerprints.js b/src/plugins/omemo-views/fingerprints.js similarity index 100% rename from src/plugins/omemo/fingerprints.js rename to src/plugins/omemo-views/fingerprints.js diff --git a/src/plugins/omemo/index.js b/src/plugins/omemo-views/index.js similarity index 93% rename from src/plugins/omemo/index.js rename to src/plugins/omemo-views/index.js index b1f37bcdbe..75a36ef918 100644 --- a/src/plugins/omemo/index.js +++ b/src/plugins/omemo-views/index.js @@ -16,19 +16,26 @@ import omemo_api from './api.js'; import { _converse, api, converse } from '@converse/headless/core'; import { createOMEMOMessageStanza, - encryptFile, getOMEMOToolbarButton, + onChatInitialized, +} from './utils.js'; + +// Temporary: to be removed to headless if possible! +import { + // createOMEMOMessageStanza, + encryptFile, + // getOMEMOToolbarButton, getOutgoingMessageAttributes, handleEncryptedFiles, handleMessageSendError, initOMEMO, omemo, - onChatBoxesInitialized, - onChatInitialized, + /onChatBoxesInitialized, + // onChatInitialized, parseEncryptedMessage, registerPEPPushHandler, setEncryptedFileURL, -} from './utils.js'; +} from '@converse/headless/plugins/omemo/utils.js' const { Strophe } = converse.env; @@ -40,13 +47,13 @@ Strophe.addNamespace('OMEMO_WHITELISTED', Strophe.NS.OMEMO + '.whitelisted'); Strophe.addNamespace('OMEMO_BUNDLES', Strophe.NS.OMEMO + '.bundles'); -converse.plugins.add('converse-omemo', { +converse.plugins.add('converse-omemo-views', { enabled (_converse) { return ( window.libsignal && _converse.config.get('trusted') && !api.settings.get('clear_cache_on_logout') && - !_converse.api.settings.get('blacklisted_plugins').includes('converse-omemo') + !_converse.api.settings.get('blacklisted_plugins').includes('converse-omemo-views') ); }, diff --git a/src/plugins/omemo/mixins/converse.js b/src/plugins/omemo-views/mixins/converse.js similarity index 100% rename from src/plugins/omemo/mixins/converse.js rename to src/plugins/omemo-views/mixins/converse.js diff --git a/src/plugins/omemo/profile.js b/src/plugins/omemo-views/profile.js similarity index 100% rename from src/plugins/omemo/profile.js rename to src/plugins/omemo-views/profile.js diff --git a/src/plugins/omemo/store.js b/src/plugins/omemo-views/store.js similarity index 100% rename from src/plugins/omemo/store.js rename to src/plugins/omemo-views/store.js diff --git a/src/plugins/omemo/templates/fingerprints.js b/src/plugins/omemo-views/templates/fingerprints.js similarity index 96% rename from src/plugins/omemo/templates/fingerprints.js rename to src/plugins/omemo-views/templates/fingerprints.js index c30493dda4..3563ebdf6b 100644 --- a/src/plugins/omemo/templates/fingerprints.js +++ b/src/plugins/omemo-views/templates/fingerprints.js @@ -1,6 +1,6 @@ import { __ } from 'i18n'; import { html } from 'lit'; -import { formatFingerprint } from '../utils.js'; +import { formatFingerprint } from '@converse/headless/plugins/omemo/utils.js'; const device_fingerprint = (el, device) => { const i18n_trusted = __('Trusted'); diff --git a/src/plugins/omemo/templates/profile.js b/src/plugins/omemo-views/templates/profile.js similarity index 97% rename from src/plugins/omemo/templates/profile.js rename to src/plugins/omemo-views/templates/profile.js index 42278c13b1..e75d7a80f4 100644 --- a/src/plugins/omemo/templates/profile.js +++ b/src/plugins/omemo-views/templates/profile.js @@ -1,5 +1,5 @@ import spinner from "templates/spinner.js"; -import { formatFingerprint } from 'plugins/omemo/utils.js'; +import { formatFingerprint } from '@converse/headless/plugins/omemo/utils.js'; import { html } from "lit"; import { __ } from 'i18n'; diff --git a/src/plugins/omemo/tests/corrections.js b/src/plugins/omemo-views/tests/corrections.js similarity index 100% rename from src/plugins/omemo/tests/corrections.js rename to src/plugins/omemo-views/tests/corrections.js diff --git a/src/plugins/omemo/tests/media-sharing.js b/src/plugins/omemo-views/tests/media-sharing.js similarity index 100% rename from src/plugins/omemo/tests/media-sharing.js rename to src/plugins/omemo-views/tests/media-sharing.js diff --git a/src/plugins/omemo/tests/muc.js b/src/plugins/omemo-views/tests/muc.js similarity index 100% rename from src/plugins/omemo/tests/muc.js rename to src/plugins/omemo-views/tests/muc.js diff --git a/src/plugins/omemo/tests/omemo.js b/src/plugins/omemo-views/tests/omemo.js similarity index 100% rename from src/plugins/omemo/tests/omemo.js rename to src/plugins/omemo-views/tests/omemo.js diff --git a/src/plugins/omemo-views/utils.js b/src/plugins/omemo-views/utils.js new file mode 100644 index 0000000000..0030ec059a --- /dev/null +++ b/src/plugins/omemo-views/utils.js @@ -0,0 +1,154 @@ +/* global libsignal */ +import concat from 'lodash-es/concat'; +import difference from 'lodash-es/difference'; +import log from '@converse/headless/log'; +import tpl_audio from 'templates/audio.js'; +import tpl_file from 'templates/file.js'; +import tpl_image from 'templates/image.js'; +import tpl_video from 'templates/video.js'; +import { __ } from 'i18n'; +import { _converse, converse, api } from '@converse/headless/core'; +import { html } from 'lit'; +import { isError } from '@converse/headless/utils/core.js'; +import { isAudioURL, isImageURL, isVideoURL, getURI } from '@converse/headless/utils/url.js'; +import { until } from 'lit/directives/until.js'; + +const { Strophe, URI, sizzle, u } = converse.env; + +function getTemplateForObjectURL (uri, obj_url, richtext) { + if (isError(obj_url)) { + return html`

${obj_url.message}

`; + } + + const file_url = uri.toString(); + if (isImageURL(file_url)) { + return tpl_image({ + 'src': obj_url, + 'onClick': richtext.onImgClick, + 'onLoad': richtext.onImgLoad + }); + } else if (isAudioURL(file_url)) { + return tpl_audio(obj_url); + } else if (isVideoURL(file_url)) { + return tpl_video(obj_url); + } else { + return tpl_file(obj_url, uri.filename()); + } + +} + +// TODO: move the non-template part to headless +function addEncryptedFiles(text, offset, richtext) { + const objs = []; + try { + const parse_options = { 'start': /\b(aesgcm:\/\/)/gi }; + URI.withinString( + text, + (url, start, end) => { + objs.push({ url, start, end }); + return url; + }, + parse_options + ); + } catch (error) { + log.debug(error); + return; + } + objs.forEach(o => { + const uri = getURI(text.slice(o.start, o.end)); + const promise = getAndDecryptFile(uri) + .then(obj_url => getTemplateForObjectURL(uri, obj_url, richtext)); + + const template = html`${until(promise, '')}`; + richtext.addTemplateResult(o.start + offset, o.end + offset, template); + }); +} + +// TODO: move non-UI functions (eg. el.model stuff) to headless +export function onChatInitialized (el) { + el.listenTo(el.model.messages, 'add', message => { + if (message.get('is_encrypted') && !message.get('is_error')) { + el.model.save('omemo_supported', true); + } + }); + el.listenTo(el.model, 'change:omemo_supported', () => { + if (!el.model.get('omemo_supported') && el.model.get('omemo_active')) { + el.model.set('omemo_active', false); + } else { + // Manually trigger an update, setting omemo_active to + // false above will automatically trigger one. + el.querySelector('converse-chat-toolbar')?.requestUpdate(); + } + }); + el.listenTo(el.model, 'change:omemo_active', () => { + el.querySelector('converse-chat-toolbar').requestUpdate(); + }); +} + +// TODO: move non-UI part of this into headless +function toggleOMEMO (ev) { + ev.stopPropagation(); + ev.preventDefault(); + const toolbar_el = u.ancestor(ev.target, 'converse-chat-toolbar'); + if (!toolbar_el.model.get('omemo_supported')) { + let messages; + if (toolbar_el.model.get('type') === _converse.CHATROOMS_TYPE) { + messages = [ + __( + 'Cannot use end-to-end encryption in this groupchat, ' + + 'either the groupchat has some anonymity or not all participants support OMEMO.' + ) + ]; + } else { + messages = [ + __( + "Cannot use end-to-end encryption because %1$s uses a client that doesn't support OMEMO.", + toolbar_el.model.contact.getDisplayName() + ) + ]; + } + return api.alert('error', __('Error'), messages); + } + toolbar_el.model.save({ 'omemo_active': !toolbar_el.model.get('omemo_active') }); +} + +// TODO: move non-UI part of this into headless (but is there any?) +export function getOMEMOToolbarButton (toolbar_el, buttons) { + const model = toolbar_el.model; + const is_muc = model.get('type') === _converse.CHATROOMS_TYPE; + let title; + if (model.get('omemo_supported')) { + const i18n_plaintext = __('Messages are being sent in plaintext'); + const i18n_encrypted = __('Messages are sent encrypted'); + title = model.get('omemo_active') ? i18n_encrypted : i18n_plaintext; + } else if (is_muc) { + title = __( + 'This groupchat needs to be members-only and non-anonymous in ' + + 'order to support OMEMO encrypted messages' + ); + } else { + title = __('OMEMO encryption is not supported'); + } + + let color; + if (model.get('omemo_supported')) { + if (model.get('omemo_active')) { + color = is_muc ? `var(--muc-color)` : `var(--chat-toolbar-btn-color)`; + } else { + color = `var(--error-color)`; + } + } else { + color = `var(--muc-toolbar-btn-disabled-color)`; + } + buttons.push(html` + + `); + return buttons; +} diff --git a/src/plugins/profile/templates/profile_modal.js b/src/plugins/profile/templates/profile_modal.js index da9f2a8234..ed10498919 100644 --- a/src/plugins/profile/templates/profile_modal.js +++ b/src/plugins/profile/templates/profile_modal.js @@ -51,7 +51,7 @@ export default (el) => { ` ); - if (_converse.pluggable.plugins['converse-omemo']?.enabled(_converse)) { + if (_converse.pluggable.plugins['converse-omemo-views']?.enabled(_converse)) { navigation_tabs.push( html`