From b11045602993faeb4d322fe60daa8d4505a7c92c Mon Sep 17 00:00:00 2001 From: John Livingston Date: Tue, 25 Jun 2024 09:59:46 +0200 Subject: [PATCH] Terms&Conditions (#18) WIP: * Converse module to display terms. * Prosody module to send terms. --- conversejs/custom/index.js | 2 + .../plugins/terms/components/muc-terms.js | 62 +++++++++++++++++ conversejs/custom/plugins/terms/index.js | 48 ++++++++++++++ .../plugins/terms/styles/muc-terms.scss | 41 ++++++++++++ .../custom/templates/muc-bottom-panel.js | 2 +- conversejs/custom/templates/muc.js | 27 ++++++++ conversejs/custom/webpack.livechat.js | 1 + .../mod_muc_peertubelivechat_terms/README.md | 34 ++++++++++ .../mod_muc_peertubelivechat_terms.lua | 66 +++++++++++++++++++ server/lib/prosody/config.ts | 8 ++- server/lib/prosody/config/content.ts | 39 ++++++++++- 11 files changed, 324 insertions(+), 6 deletions(-) create mode 100644 conversejs/custom/plugins/terms/components/muc-terms.js create mode 100644 conversejs/custom/plugins/terms/index.js create mode 100644 conversejs/custom/plugins/terms/styles/muc-terms.scss create mode 100644 conversejs/custom/templates/muc.js create mode 100644 prosody-modules/mod_muc_peertubelivechat_terms/README.md create mode 100644 prosody-modules/mod_muc_peertubelivechat_terms/mod_muc_peertubelivechat_terms.lua diff --git a/conversejs/custom/index.js b/conversejs/custom/index.js index df11239c..d5fda0a1 100644 --- a/conversejs/custom/index.js +++ b/conversejs/custom/index.js @@ -46,6 +46,7 @@ import './plugins/fullscreen/index.js' import '../custom/plugins/size/index.js' import '../custom/plugins/tasks/index.js' +import '../custom/plugins/terms/index.js' /* END: Removable components */ import { CORE_PLUGINS } from './headless/shared/constants.js' @@ -53,6 +54,7 @@ import { ROOM_FEATURES } from './headless/plugins/muc/constants.js' // We must add our custom plugins to CORE_PLUGINS (so it is white listed): CORE_PLUGINS.push('livechat-converse-size') CORE_PLUGINS.push('livechat-converse-tasks') +CORE_PLUGINS.push('livechat-converse-terms') // We must also add our custom ROOM_FEATURES, so that they correctly resets // (see headless/plugins/muc, getDiscoInfoFeatures, which loops on this const) ROOM_FEATURES.push('x_peertubelivechat_mute_anonymous') diff --git a/conversejs/custom/plugins/terms/components/muc-terms.js b/conversejs/custom/plugins/terms/components/muc-terms.js new file mode 100644 index 00000000..ac57d74a --- /dev/null +++ b/conversejs/custom/plugins/terms/components/muc-terms.js @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: 2024 John Livingston +// +// SPDX-License-Identifier: AGPL-3.0-only + +import { CustomElement } from 'shared/components/element.js' +import { api } from '@converse/headless/core' +import { html } from 'lit' +import { __ } from 'i18n' + +import '../styles/muc-terms.scss' + +export default class MUCTermsView extends CustomElement { + static get properties () { + return { + model: { type: Object, attribute: true }, + termstype: { type: String, attribute: true } + } + } + + async initialize () { + if (!this.model) { + return + } + this.listenTo(this.model, 'change:x_livechat_terms_' + this.termstype, () => this.requestUpdate()) + } + + render () { + const terms = this.model?.get('x_livechat_terms_' + this.termstype) + return html` + ${terms && terms.body && !this._hideInfoBox(terms.body) + ? html` +
+ + + + +
` + : '' + }` + } + + closeInfoBox (ev) { + ev.preventDefault() + const terms = this.model?.get('x_livechat_terms_' + this.termstype) + if (terms) { + localStorage?.setItem('x_livechat_terms_' + this.termstype + '_hidden', terms.body) + } + this.requestUpdate() + } + + _hideInfoBox (body) { + // When hiding the infobox, we store in localStorage the current body, so we will show it again if message change. + // Note: for termstype=global we don't store the MUC server, so if user join chat from different instances, + // it will show terms again + // Note: same for termstype=muc, we don't store the MUC JID, so if user changes channel, + // it will probably show terms again + const lsHideInfoBox = localStorage?.getItem('x_livechat_terms_' + this.termstype + '_hidden') + return lsHideInfoBox === body + } +} + +api.elements.define('livechat-converse-muc-terms', MUCTermsView) diff --git a/conversejs/custom/plugins/terms/index.js b/conversejs/custom/plugins/terms/index.js new file mode 100644 index 00000000..838820d8 --- /dev/null +++ b/conversejs/custom/plugins/terms/index.js @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2024 John Livingston +// +// SPDX-License-Identifier: AGPL-3.0-only + +import { converse, api } from '../../../src/headless/core.js' +import './components/muc-terms.js' + +const { sizzle } = converse.env + +converse.plugins.add('livechat-converse-terms', { + dependencies: ['converse-muc'], + initialize () { + api.listen.on('parseMUCMessage', (stanza, attrs) => { + const livechatTerms = sizzle('x-livechat-terms', stanza) + if (!livechatTerms.length) { + return attrs + } + return Object.assign( + attrs, + { + x_livechat_terms: livechatTerms[0].getAttribute('type') + } + ) + }) + }, + overrides: { + ChatRoom: { + onMessage: function onMessage (attrs) { + if (!attrs.x_livechat_terms) { + return this.__super__.onMessage(attrs) + } + // We received a x-livechat-terms message, we don't forward it to standard onMessage, + // but we just update the room attribute. + const type = attrs.x_livechat_terms + if (type !== 'global' && type !== 'muc') { + console.error('Invalid x-livechat-terms type: ', type) + return + } + // console.info('Received a x-livechat-terms message', attrs) + const options = {} + options['x_livechat_terms_' + type] = attrs + this.set(options) + // this will be displayed by the livechat-converse-muc-terms custom element, + // which is inserted in the DOM by the muc.js template overload. + } + } + } +}) diff --git a/conversejs/custom/plugins/terms/styles/muc-terms.scss b/conversejs/custom/plugins/terms/styles/muc-terms.scss new file mode 100644 index 00000000..27f7ccef --- /dev/null +++ b/conversejs/custom/plugins/terms/styles/muc-terms.scss @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: 2024 John Livingston + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +.conversejs { + livechat-converse-muc-terms { + background-color: var(--peertube-main-background); + color: var(--peertube-main-foreground); + + div { + align-items: center; + border: 1px solid var(--peertube-menu-background); + display: flex; + flex-flow: row; + justify-content: space-between; + margin: 5px; + padding: 5px; + + converse-rich-text { + flex-grow: 2; + max-height: 5em; + overflow-y: scroll; + white-space: pre-wrap; + } + + .livechat-hide-terms-info-box { + cursor: pointer; + font-size: var(--font-size-small); + flex-shrink: 2; + } + } + } +} + +.livechat-readonly .conversejs { + livechat-converse-muc-terms { + display: none !important; + } +} diff --git a/conversejs/custom/templates/muc-bottom-panel.js b/conversejs/custom/templates/muc-bottom-panel.js index ea5ecb8b..cfac6d2c 100644 --- a/conversejs/custom/templates/muc-bottom-panel.js +++ b/conversejs/custom/templates/muc-bottom-panel.js @@ -63,7 +63,7 @@ class SlowMode extends CustomElement { LOC_slow_mode_info, this.model.config.get('slow_mode_duration') )} - + ` diff --git a/conversejs/custom/templates/muc.js b/conversejs/custom/templates/muc.js new file mode 100644 index 00000000..bb9820c6 --- /dev/null +++ b/conversejs/custom/templates/muc.js @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2013-2024 JC Brand +// SPDX-FileCopyrightText: 2024 John Livingston +// +// SPDX-License-Identifier: MPL-2.0 +// SPDX-License-Identifier: AGPL-3.0-only + +// Must import the original muc.js, because it imports some custom elements files. +import '../../src/plugins/muc-views/templates/muc.js' +import { getChatRoomBodyTemplate } from '../../src/plugins/muc-views/utils.js' +import { html } from 'lit' + +// Overloading the original muc.js, to add some custom elements. +export default (o) => { + return html` +
+ + ${ + o.model + ? html` + + + + +
${getChatRoomBodyTemplate(o)}
` + : ''} +
` +} diff --git a/conversejs/custom/webpack.livechat.js b/conversejs/custom/webpack.livechat.js index 85e68895..bf6e041c 100644 --- a/conversejs/custom/webpack.livechat.js +++ b/conversejs/custom/webpack.livechat.js @@ -40,6 +40,7 @@ module.exports = merge(prod, { alias: { './templates/muc-bottom-panel.js': path.resolve('custom/templates/muc-bottom-panel.js'), './templates/muc-head.js': path.resolve(__dirname, 'custom/templates/muc-head.js'), + './templates/muc.js': path.resolve(__dirname, 'custom/templates/muc.js'), '../../templates/background_logo.js$': path.resolve(__dirname, 'custom/templates/background_logo.js'), './templates/muc-chatarea.js': path.resolve('custom/templates/muc-chatarea.js'), diff --git a/prosody-modules/mod_muc_peertubelivechat_terms/README.md b/prosody-modules/mod_muc_peertubelivechat_terms/README.md new file mode 100644 index 00000000..0c2229f5 --- /dev/null +++ b/prosody-modules/mod_muc_peertubelivechat_terms/README.md @@ -0,0 +1,34 @@ + + +# mod_muc_peertubelivechat_terms + +This module is a custom module to handle Terms&Conditions in the livechat Peertube plugin. + +This module is part of peertube-plugin-livechat, and is under the same LICENSE. + +## Features + +When a new occupant session is created for a MUC, this module will send to the user the global terms, +and the MUC-specific terms (if defined). + +This is done by sending groupchat messages. +These messages will contain a "x-livechat-terms" tag, so that livechat front-end can detect these messages, and display them differently. +For standard XMPP clients, these messages will show as standard MUC message coming from a specific nickname. + +## Configuration + +This modules take following options. + +### muc_terms_service_nickname + +The nickname that will be used by service messages. +This module reserves the nickname, so than nobody can use it in MUC rooms +(we don't want any user to spoof this nickname). + +### muc_terms + +The global terms. diff --git a/prosody-modules/mod_muc_peertubelivechat_terms/mod_muc_peertubelivechat_terms.lua b/prosody-modules/mod_muc_peertubelivechat_terms/mod_muc_peertubelivechat_terms.lua new file mode 100644 index 00000000..be4272ca --- /dev/null +++ b/prosody-modules/mod_muc_peertubelivechat_terms/mod_muc_peertubelivechat_terms.lua @@ -0,0 +1,66 @@ +-- mod_muc_peertubelivechat_terms +-- +-- SPDX-FileCopyrightText: 2024 John Livingston +-- SPDX-License-Identifier: AGPL-3.0-only +-- +-- This file is AGPL-v3 licensed. +-- Please see the Peertube livechat plugin copyright information. +-- https://livingston.frama.io/peertube-plugin-livechat/credits/ +-- +local jid_escape = require "util.jid".escape; +local jid_resource = require "util.jid".resource; +local st = require "util.stanza"; +local id = require "util.id"; + +local service_nickname = module:get_option_string("muc_terms_service_nickname", "Service"); +local global_terms = module:get_option_string("muc_terms", ""); + +-- send the terms when joining: +function send_terms(event) + local origin = event.origin; + local room = event.room; + local occupant = event.occupant; + if global_terms then + local from = room.jid .. '/' .. jid_escape(service_nickname); + module:log("debug", "Sending global terms to %s from %s (room %s)", occupant.jid, from, room); + local message = st.message({ + type = "groupchat", + to = occupant.jid, + from = from, + id = id.medium() + }, global_terms) + :tag('x-livechat-terms', { type = "global" }):up(); -- adding a custom tag to specify that it is a "terms" message, so that frontend can display it with a special template. + origin.send(message); + end +end +-- Note: we could do that on muc-occupant-joined or muc-occupant-session-new. +-- The first will not send it to multiple clients, the second will. +-- After some reflexion, i will try muc-occupant-session-new, and see if it works as expected. +module:hook("muc-occupant-session-new", send_terms); + +-- reserve the service_nickname: +function enforce_nick_policy(event) + local origin, stanza = event.origin, event.stanza; + local requested_nick = jid_resource(stanza.attr.to); + local room = event.room; + if not room then return; end + + if requested_nick == service_nickname then + module:log("debug", "Occupant tried to use the %s reserved nickname, blocking it.", service_nickname); + local reply = st.error_reply(stanza, "cancel", "conflict", nil, room.jid):up(); + origin.send(reply); + return true; + end +end +module:hook("muc-occupant-pre-join", enforce_nick_policy); +module:hook("muc-occupant-pre-change", enforce_nick_policy); + +-- security check: we must remove all "x-livechat-terms" tag, to be sure nobody tries to spoof terms! +module:hook("muc-occupant-groupchat", function(event) + event.stanza:maptags(function (child) + if child.name == 'x-livechat-terms' then + return nil; + end + return child; + end); +end, 100); diff --git a/server/lib/prosody/config.ts b/server/lib/prosody/config.ts index 90e00c0b..d8f8bd54 100644 --- a/server/lib/prosody/config.ts +++ b/server/lib/prosody/config.ts @@ -175,7 +175,8 @@ async function getProsodyConfig (options: RegisterServerOptionsV5): Promise() @@ -199,6 +200,9 @@ async function getProsodyConfig (options: RegisterServerOptionsV5): Promise