From 1a75b30c50797f8ad0d83f13df6cffdb397e01c1 Mon Sep 17 00:00:00 2001 From: John Livingston Date: Thu, 5 Sep 2024 18:28:54 +0200 Subject: [PATCH] Emoji only mode WIP --- CHANGELOG.md | 6 + build-prosody.sh | 6 +- conversejs/build-conversejs.sh | 4 + conversejs/custom/index.js | 1 + .../custom/shared/styles/_peertubetheme.scss | 8 ++ .../custom/templates/muc-bottom-panel.js | 15 ++ conversejs/lib/plugins/livechat-specific.ts | 2 + .../livechat-specific/muc-bottom-panel.ts | 23 +++ conversejs/loc.keys.js | 3 +- languages/en.yml | 2 + ...mod_http_peertubelivechat_manage_rooms.lua | 13 ++ .../mod_muc_http_defaults.lua | 9 ++ .../README.markdown | 14 ++ ..._muc_peertubelivechat_restrict_message.lua | 131 ++++++++++++++++++ server/lib/configuration/channel/init.ts | 1 + server/lib/emojis/emojis.ts | 111 ++++++++++++++- server/lib/prosody/api/manage-rooms.ts | 8 ++ server/lib/prosody/config/content.ts | 2 + server/lib/routers/api/configuration.ts | 16 +++ server/lib/routers/api/room.ts | 6 + 20 files changed, 375 insertions(+), 6 deletions(-) create mode 100644 conversejs/lib/plugins/livechat-specific/muc-bottom-panel.ts create mode 100644 prosody-modules/mod_muc_peertubelivechat_restrict_message/README.markdown create mode 100644 prosody-modules/mod_muc_peertubelivechat_restrict_message/mod_muc_peertubelivechat_restrict_message.lua diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e08072f..665ee9b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 11.1.0 (Not Released Yet) + +### New features + +* #131: Emoji only mode. + ## 11.0.1 ### Minor changes and fixes diff --git a/build-prosody.sh b/build-prosody.sh index 301efd70..c98cc2e8 100644 --- a/build-prosody.sh +++ b/build-prosody.sh @@ -10,12 +10,12 @@ set -euo pipefail # This script download the Prosody AppImage from the https://github.com/JohnXLivingston/prosody-appimage project. repo_base_url='https://github.com/JohnXLivingston/prosody-appimage/releases/download' -wanted_release='v0.12.3-1' +wanted_release='v0.12.4-2' x86_64_filename='prosody-x86_64.AppImage' -x86_64_sha256sum='f4af9bfefa2f804ad7e8b03a68f04194abb801f070ae620b3d4bcedb144e8523' +x86_64_sha256sum='664d9f3b1ea6dc5fdbe29ef8e8b4c0655abdff697e8c94bfecc894ef2c2fea08' aarch64_filename='prosody-aarch64.AppImage' -aarch64_sha256sum='878c5be719e1e36a84d637fd2bd44e3059aa91ddb6906ad05f1dd0334078df09' +aarch64_sha256sum='9911c0d581a92a817e9795a7944773a07e85151127233a2e551eb07dc4c44fb5' download_dir="$(pwd)/vendor/prosody-appimage" dist_dir="$(pwd)/dist/server/prosody" diff --git a/conversejs/build-conversejs.sh b/conversejs/build-conversejs.sh index c1078d16..a1f105f6 100644 --- a/conversejs/build-conversejs.sh +++ b/conversejs/build-conversejs.sh @@ -40,6 +40,7 @@ if [ -n "$CONVERSE_COMMIT" ]; then fi converse_build_dir="$rootdir/build/conversejs" converse_destination_dir="$rootdir/dist/client/conversejs" +converse_emoji_destination="$rootdir/dist/converse-emoji.json" if [[ ! -d $src_dir ]]; then echo "$0 must be called from the plugin livechat root dir." @@ -119,6 +120,9 @@ cd $rootdir echo "Copying ConverseJS dist files..." mkdir -p "$converse_destination_dir" && cp -r $converse_build_dir/dist/* "$converse_destination_dir/" +echo "Copying ConverseJS original emoji.json file..." # this is needed for some backend code. +cp "$converse_build_dir/src/headless/plugins/emoji/emoji.json" "$converse_emoji_destination" + echo "ConverseJS OK." exit 0 diff --git a/conversejs/custom/index.js b/conversejs/custom/index.js index fdc969e9..1e54d2a7 100644 --- a/conversejs/custom/index.js +++ b/conversejs/custom/index.js @@ -66,6 +66,7 @@ CORE_PLUGINS.push('livechat-converse-mam-search') // 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') +ROOM_FEATURES.push('x_peertubelivechat_emoji_only_mode') _converse.exports.CustomElement = CustomElement diff --git a/conversejs/custom/shared/styles/_peertubetheme.scss b/conversejs/custom/shared/styles/_peertubetheme.scss index 6ac3f33d..43bff78e 100644 --- a/conversejs/custom/shared/styles/_peertubetheme.scss +++ b/conversejs/custom/shared/styles/_peertubetheme.scss @@ -35,6 +35,14 @@ } } + // Emoji only info box + .livechat-emoji-only-info-box { + border: 1px dashed var(--peertube-menu-background); + color: var(--peertube-main-foreground); + background-color: var(--peertube-main-background); + margin: 0 5px; + } + converse-chat-toolbar { border-top: none !important; // removing border, to avoid confusing the toolbar with an input field. color: var(--peertube-main-foreground); diff --git a/conversejs/custom/templates/muc-bottom-panel.js b/conversejs/custom/templates/muc-bottom-panel.js index 91b12bb4..b1648b09 100644 --- a/conversejs/custom/templates/muc-bottom-panel.js +++ b/conversejs/custom/templates/muc-bottom-panel.js @@ -83,6 +83,20 @@ const tplSlowMode = (o) => { return html`` } +const tplEmojiOnly = (o) => { + if (!o.can_post) { return html`` } + if (!o.model.features?.get?.('x_peertubelivechat_emoji_only_mode')) { + return '' + } + return html`
+ + ${ + // eslint-disable-next-line no-undef + __(LOC_emoji_only_info) + } +
` +} + const tplViewerMode = (o) => { if (!api.settings.get('livechat_enable_viewer_mode')) { return html`` @@ -145,6 +159,7 @@ export default (o) => { return html` ${tplViewerMode(o)} ${tplSlowMode(o)} + ${tplEmojiOnly(o)} ${ mutedAnonymousMessage ? html`${mutedAnonymousMessage}` diff --git a/conversejs/lib/plugins/livechat-specific.ts b/conversejs/lib/plugins/livechat-specific.ts index 6313564d..3f499a34 100644 --- a/conversejs/lib/plugins/livechat-specific.ts +++ b/conversejs/lib/plugins/livechat-specific.ts @@ -9,6 +9,7 @@ import { chatRoomOverrides } from './livechat-specific/chatroom' import { chatRoomMessageOverrides } from './livechat-specific/chatroom-message' import { customizeMessageAction } from './livechat-specific/message-action' import { customizeProfileModal } from './livechat-specific/profile' +import { customizeMUCBottomPanel } from './livechat-specific/muc-bottom-panel' export const livechatSpecificsPlugin = { dependencies: ['converse-muc', 'converse-muc-views'], @@ -26,6 +27,7 @@ export const livechatSpecificsPlugin = { customizeToolbar(this) customizeMessageAction(this) customizeProfileModal(this) + customizeMUCBottomPanel(this) _converse.api.listen.on('chatRoomViewInitialized', function (this: any, _model: any): void { // Remove the spinner if present... diff --git a/conversejs/lib/plugins/livechat-specific/muc-bottom-panel.ts b/conversejs/lib/plugins/livechat-specific/muc-bottom-panel.ts new file mode 100644 index 00000000..5ff33057 --- /dev/null +++ b/conversejs/lib/plugins/livechat-specific/muc-bottom-panel.ts @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2024 John Livingston +// +// SPDX-License-Identifier: AGPL-3.0-only + +/** + * Override the MUCBottomPanel custom element + */ +export function customizeMUCBottomPanel (plugin: any): void { + const _converse = plugin._converse + const MUCBottomPanel = _converse.api.elements.registry['converse-muc-bottom-panel'] + if (MUCBottomPanel) { + class MUCBottomPanelOverloaded extends MUCBottomPanel { + async initialize (): Promise { + await super.initialize() + // We must refresh the bottom panel when these features changes (to display the infobox) + // FIXME: the custom muc-bottom-panel template should be used here, in an overloaded render method, instead + // of using webpack to overload the original file. + this.listenTo(this.model.features, 'change:x_peertubelivechat_emoji_only_mode', () => this.requestUpdate()) + } + } + _converse.api.elements.define('converse-muc-bottom-panel', MUCBottomPanelOverloaded) + } +} diff --git a/conversejs/loc.keys.js b/conversejs/loc.keys.js index ed3ebe95..997726db 100644 --- a/conversejs/loc.keys.js +++ b/conversejs/loc.keys.js @@ -62,7 +62,8 @@ const locKeys = [ 'moderator_note_original_nick', 'search_occupant_message', 'message_search', - 'message_search_original_nick' + 'message_search_original_nick', + 'emoji_only_info' ] module.exports = locKeys diff --git a/languages/en.yml b/languages/en.yml index 5f4bdd6e..04c3a539 100644 --- a/languages/en.yml +++ b/languages/en.yml @@ -636,3 +636,5 @@ prosody_firewall_name_desc: | Can only contain: alphanumerical characters, underscores and hyphens. Scripts will be loaded in alphabetical order. prosody_firewall_content: File content + +emoji_only_info: Emoji only mode is enabled, you can only use emoji in your messages. diff --git a/prosody-modules/mod_http_peertubelivechat_manage_rooms/mod_http_peertubelivechat_manage_rooms.lua b/prosody-modules/mod_http_peertubelivechat_manage_rooms/mod_http_peertubelivechat_manage_rooms.lua index 86eeae15..e616e9a7 100644 --- a/prosody-modules/mod_http_peertubelivechat_manage_rooms/mod_http_peertubelivechat_manage_rooms.lua +++ b/prosody-modules/mod_http_peertubelivechat_manage_rooms/mod_http_peertubelivechat_manage_rooms.lua @@ -16,6 +16,9 @@ module:depends"http"; local mod_muc_peertubelivechat_terms = module:depends"muc_peertubelivechat_terms"; local set_muc_terms = rawget(mod_muc_peertubelivechat_terms, "set_muc_terms"); +local mod_muc_peertubelivechat_restrict_message = module:depends"muc_peertubelivechat_restrict_message"; +local set_peertubelivechat_emoji_only_mode = rawget(mod_muc_peertubelivechat_restrict_message, "set_peertubelivechat_emoji_only_mode"); +local set_peertubelivechat_emoji_only_regexp = rawget(mod_muc_peertubelivechat_restrict_message, "set_peertubelivechat_emoji_only_regexp"); function check_auth(routes) local function check_request_auth(event) @@ -102,6 +105,16 @@ local function update_room(event) room._data.moderation_delay = config.moderation_delay; end end + if type(config.livechat_emoji_only) == "boolean" then + if set_peertubelivechat_emoji_only_mode then + set_peertubelivechat_emoji_only_mode(room, config.livechat_emoji_only) + end + end + if type(config.livechat_emoji_only_regexp) == "string" then + if set_peertubelivechat_emoji_only_mode then + set_peertubelivechat_emoji_only_regexp(room, config.livechat_emoji_only_regexp) + end + end if (type(config.livechat_muc_terms) == "string") then -- to easily detect if the value is given or not, we consider that the caller passes "" when terms must be deleted. if set_muc_terms then diff --git a/prosody-modules/mod_muc_http_defaults/mod_muc_http_defaults.lua b/prosody-modules/mod_muc_http_defaults/mod_muc_http_defaults.lua index 771498ad..805dde14 100644 --- a/prosody-modules/mod_muc_http_defaults/mod_muc_http_defaults.lua +++ b/prosody-modules/mod_muc_http_defaults/mod_muc_http_defaults.lua @@ -9,6 +9,9 @@ -- * "mute_anonymous" -- * "moderation_delay" -- * "anonymize_moderation_actions" +-- * "livechat_emoji_only" +-- * "livechat_emoji_only_regexp" +-- * "livechat_muc_terms" -- These options are introduced in the Peertube livechat plugin. -- -- The "slow_mode_duration" comes with mod_muc_slow_mode. @@ -128,6 +131,12 @@ local function apply_config(room, settings) if (type(config.mute_anonymous) == "boolean") then room._data.x_peertubelivechat_mute_anonymous = config.mute_anonymous; end + if (type(config.livechat_emoji_only) == "boolean") then + room._data.x_peertubelivechat_emoji_only_mode = config.livechat_emoji_only; + end + if (type(config.livechat_emoji_only_regexp) == "string" and config.livechat_emoji_only_regexp ~= "") then + room._data.x_peertubelivechat_emoji_only_regexp = config.emoji_only_regexp; + end if (type(config.livechat_muc_terms) == "string") then -- we don't need to use set_muc_terms here, as this is called for a newly created room -- (and thus we don't need to broadcast changes) diff --git a/prosody-modules/mod_muc_peertubelivechat_restrict_message/README.markdown b/prosody-modules/mod_muc_peertubelivechat_restrict_message/README.markdown new file mode 100644 index 00000000..ccb0211c --- /dev/null +++ b/prosody-modules/mod_muc_peertubelivechat_restrict_message/README.markdown @@ -0,0 +1,14 @@ + +# mod_muc_peertubelivechat_restrict_message + +This module is a custom module designed for the peertube-plugin-livechat project, that can restrict message content to +given regular expression. + +This module is part of peertube-plugin-livechat, and is under the same LICENSE (AGPL-v3). + +## Prerequisites + +This modules needs lrexlib instlaled (available as lua-rex-pcre2 package on Debian). diff --git a/prosody-modules/mod_muc_peertubelivechat_restrict_message/mod_muc_peertubelivechat_restrict_message.lua b/prosody-modules/mod_muc_peertubelivechat_restrict_message/mod_muc_peertubelivechat_restrict_message.lua new file mode 100644 index 00000000..b54e94e6 --- /dev/null +++ b/prosody-modules/mod_muc_peertubelivechat_restrict_message/mod_muc_peertubelivechat_restrict_message.lua @@ -0,0 +1,131 @@ +-- mod_muc_peertubelivechat_roles +-- +-- 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 st = require "util.stanza"; +local jid_bare = require "util.jid".bare; + +local rex = require "rex_pcre2"; -- We are using PCRE2 (Perl Compatible Regular Expression) + +-- Plugin dependencies +local mod_muc = module:depends "muc"; +local muc_util = module:require "muc/util"; +local valid_roles = muc_util.valid_roles; + + +function get_peertubelivechat_emoji_only_mode(room) + return room._data.x_peertubelivechat_emoji_only_mode; +end + +function set_peertubelivechat_emoji_only_mode(room, emoji_only) + emoji_only = emoji_only and true or nil; + if get_peertubelivechat_emoji_only_mode(room) == emoji_only then return false; end + room._data.x_peertubelivechat_emoji_only_mode = emoji_only; + return true; +end + +function get_peertubelivechat_emoji_only_regexp(room) + return room._data.x_peertubelivechat_emoji_only_regexp; +end + +function set_peertubelivechat_emoji_only_regexp(room, emoji_only_regexp) + if (emoji_only_regexp ~= nil and type(emoji_only_regexp) ~= "string") then + return false; + end + if emoji_only_regexp == "" then emoji_only_regexp = nil; end + if get_peertubelivechat_emoji_only_regexp(room) == emoji_only_regexp then return false; end + room._data.x_peertubelivechat_emoji_only_regexp = emoji_only_regexp; + + -- and we must decache the compile regexp + room.x_peertubelivechat_emoji_only_compiled_regexp = nil; + return true; +end + +module:hook("muc-disco#info", function(event) + if get_peertubelivechat_emoji_only_mode(event.room) and get_peertubelivechat_emoji_only_regexp(event.room) ~= nil then + event.reply:tag("feature", {var = "x_peertubelivechat_emoji_only_mode"}):up(); + end +end); + +module:hook("muc-config-form", function(event) + if (get_peertubelivechat_emoji_only_regexp(event.room) ~= nil) then + table.insert(event.form, { + name = "muc#roomconfig_x_peertubelivechat_emoji_only_mode"; + type = "boolean"; + label = "Emoji only mode"; + desc = "Occupants will only be able to send emoji. This does not affect moderators."; + value = get_peertubelivechat_emoji_only_mode(event.room); + }); + end +end, 121); + +module:hook("muc-config-submitted/muc#roomconfig_x_peertubelivechat_emoji_only_mode", function(event) + if get_peertubelivechat_emoji_only_regexp(event.room) ~= nil and set_peertubelivechat_emoji_only_mode(event.room, event.value) then + event.status_codes["104"] = true; + end +end); + + +-- handling groupchat messages +function handle_groupchat(event) + local origin, stanza = event.origin, event.stanza; + local room = event.room; + + if (not get_peertubelivechat_emoji_only_mode(room)) then + return; + end + + if not room.x_peertubelivechat_emoji_only_compiled_regexp then + -- compute the regexp on first access + local r = get_peertubelivechat_emoji_only_regexp(room); + if (r == nil) then + return; + end + room.x_peertubelivechat_emoji_only_compiled_regexp = rex.new(r, "i"); + end + + -- only consider messages with body (ie: ignore chatstate and other non-text xmpp messages) + local body = stanza:get_child_text("body") + if not body or #body < 1 then + -- module:log("debug", "No body, message accepted"); + return; + end + + -- Checking user's permissions (moderators are not subject to restrictions) + local actor = stanza.attr.from; + local actor_nick = room:get_occupant_jid(actor); + local actor_jid = jid_bare(actor); + -- Only checking role, not affiliation (restrictions only applies on users currently connected to the room) + local role = room:get_role(actor_nick); + if valid_roles[role or "none"] >= valid_roles.moderator then + -- user bypasses + -- module:log("debug", "User is moderator, bypassing restrictions"); + return; + end + + -- testing the content + if (room.x_peertubelivechat_emoji_only_compiled_regexp:match(body) ~= nil) then + -- module:log("debug", "Message accepted"); + return; + end + + module:log("debug", "Bouncing message for user %s", actor_nick); + local reply = st.error_reply( + stanza, + -- error_type = 'modify' (see descriptions in RFC 6120 https://xmpp.org/rfcs/rfc6120.html#stanzas-error-syntax) + "modify", + -- error_condition = 'policy-violation' (see RFC 6120 Defined Error Conditions https://xmpp.org/rfcs/rfc6120.html#stanzas-error-conditions) + "policy-violation", + "Emoji only mode enabled" + ); + + origin.send(reply); + return true; -- stoping propagation +end +module:hook("muc-occupant-groupchat", handle_groupchat); diff --git a/server/lib/configuration/channel/init.ts b/server/lib/configuration/channel/init.ts index 87788bd4..96bd1af4 100644 --- a/server/lib/configuration/channel/init.ts +++ b/server/lib/configuration/channel/init.ts @@ -120,6 +120,7 @@ async function initChannelConfiguration (options: RegisterServerOptions): Promis // but will be more efficient to add here, as we already tested hasChat). // Note: no need to await here, would only degrade performances. // FIXME: should also update livechat_muc_terms if channel has changed. + // FIXME: should also update livechat_emoji_only_regexp if channel has changed. updateProsodyRoom(options, video.uuid, { name: video.name }).then( diff --git a/server/lib/emojis/emojis.ts b/server/lib/emojis/emojis.ts index ccd08f24..da6ec9b5 100644 --- a/server/lib/emojis/emojis.ts +++ b/server/lib/emojis/emojis.ts @@ -23,6 +23,7 @@ export class Emojis { protected channelBasePath: string protected channelBaseUri: string protected readonly channelCache = new Map() + protected readonly commonEmojisCodes: string[] protected readonly logger: { debug: (s: string) => void info: (s: string) => void @@ -30,9 +31,10 @@ export class Emojis { error: (s: string) => void } - constructor (options: RegisterServerOptions) { + constructor (options: RegisterServerOptions, commonEmojisCodes: string[]) { const logger = options.peertubeHelpers.logger this.options = options + this.commonEmojisCodes = commonEmojisCodes this.channelBasePath = path.join( options.peertubeHelpers.plugin.getDataDirectoryPath(), 'emojis', @@ -138,9 +140,11 @@ export class Emojis { /** * Test if short name is valid. + * * @param sn short name */ public validShortName (sn: any): boolean { + // Important note: do not change this without checking if it can breaks getChannelEmojisOnlyRegexp. if ((typeof sn !== 'string') || !/^:?[\w-]+:?$/.test(sn)) { this.logger.debug('Short name invalid: ' + (typeof sn === 'string' ? sn : '???')) return false @@ -390,6 +394,40 @@ export class Emojis { } } + /** + * Returns a string representing a regular expression (Perl Compatible RE) that can validate that a message + * contains only emojis (for this channel). + * This is used for the emoji only mode (test are made on the Prosody server). + * + * @param channelId channel id + */ + public async getChannelEmojisOnlyRegexp (channelId: number): Promise { + const parts = [...this.commonEmojisCodes] + if (await this.channelHasCustomEmojis(channelId)) { + const def = await this.channelCustomEmojisDefinition(channelId) + if (def) { + parts.push(...def.customEmojis.map(d => d.sn)) + } + } + + // Note: validShortName should ensure we won't put special chars. + // And for the common emojis, we assume that there is no special regexp chars (other that +, which will be escaped). + const regexp = '^\\s*(?:(?:' + parts.map((s) => s.replace(/[+]/g, '\\$&')).join('|') + ')\\s*)+\\s*$' + + // As a safety net, we check if it is a valid javascript regexp. + try { + const s = new RegExp(regexp) + if (!s) { + throw new Error('Can\'t create the RegExp from ' + regexp) + } + } catch (err) { + this.logger.error('Invalid Emoji Only regexp for channel ' + channelId.toString() + ': ' + regexp) + return undefined + } + + return regexp + } + /** * Returns the singleton, of thrown an exception if it is not initialized yet. * Please note that this singleton won't exist if feature is disabled. @@ -416,10 +454,14 @@ export class Emojis { */ public static async initSingleton (options: RegisterServerOptions): Promise { const disabled = await options.settingsManager.getSetting('disable-channel-configuration') + + // Loading common emojis codes + const commonEmojisCodes = await _getConverseEmojiCodes(options) + if (disabled) { singleton = undefined } else { - singleton = new Emojis(options) + singleton = new Emojis(options, commonEmojisCodes) } } @@ -431,3 +473,68 @@ export class Emojis { singleton = undefined } } + +async function _getConverseEmojiCodes (options: RegisterServerOptions): Promise { + try { + // build-converse.sh copy the file emoji.json to /dist/converse-emoji.json + const converseEmojiDefPath = path.join(__dirname, '..', '..', '..', 'converse-emoji.json') + options.peertubeHelpers.logger.debug('Loading Converse Emojis from file ' + converseEmojiDefPath) + + const converseEmojis: {[key: string]: any} = JSON.parse( + await (await fs.promises.readFile(converseEmojiDefPath)).toString() + ) + + const r = [] + for (const [key, block] of Object.entries(converseEmojis)) { + if (key === 'custom') { continue } // These are not used. + r.push( + ...Object.values(block) + .map((d: any) => d.cp ? _convert(d.cp) : d.sn) + .filter((sn: string) => sn && sn !== '') + ) + } + return r + } catch (err) { + options.peertubeHelpers.logger.error( + 'Failed to load Converse Emojis file, emoji only mode will be buggy. ' + (err as string) + ) + return [] + } +} + +/** + * Converts unicode code points and code pairs to their respective characters. + * See ConverseJS emoji/utils.js for more info. + * @param {string} unicode + */ +function _convert (unicode: string): string { + if (unicode.includes('-')) { + const parts = [] + const s = unicode.split('-') + + for (let i = 0; i < s.length; i++) { + const part = parseInt(s[i], 16) + if (part >= 0x10000 && part <= 0x10FFFF) { + const hi = Math.floor((part - 0x10000) / 0x400) + 0xD800 + const lo = ((part - 0x10000) % 0x400) + 0xDC00 + parts.push(String.fromCharCode(hi) + String.fromCharCode(lo)) + } else { + parts.push(String.fromCharCode(part)) + } + } + return parts.join('') + } + return _fromCodePoint(unicode) +} + +function _fromCodePoint (codepoint: string): string { + let code = typeof codepoint === 'string' ? parseInt(codepoint, 16) : codepoint + if (code < 0x10000) { + return String.fromCharCode(code) + } + code -= 0x10000 + return String.fromCharCode( + 0xD800 + (code >> 10), + 0xDC00 + (code & 0x3FF) + ) +} diff --git a/server/lib/prosody/api/manage-rooms.ts b/server/lib/prosody/api/manage-rooms.ts index 064608d5..f1c5b28b 100644 --- a/server/lib/prosody/api/manage-rooms.ts +++ b/server/lib/prosody/api/manage-rooms.ts @@ -65,6 +65,8 @@ async function updateProsodyRoom ( name?: string slow_mode_duration?: number moderation_delay?: number + livechat_emoji_only?: boolean + livechat_emoji_only_regexp?: string livechat_muc_terms?: string addAffiliations?: Affiliations removeAffiliationsFor?: string[] @@ -100,6 +102,12 @@ async function updateProsodyRoom ( if ('livechat_muc_terms' in data) { apiData.livechat_muc_terms = data.livechat_muc_terms ?? '' } + if ('livechat_emoji_only' in data) { + apiData.livechat_emoji_only = data.livechat_emoji_only ?? false + } + if ('livechat_emoji_only_regexp' in data) { + apiData.livechat_emoji_only_regexp = data.livechat_emoji_only_regexp ?? '' + } if (('addAffiliations' in data) && data.addAffiliations !== undefined) { apiData.addAffiliations = data.addAffiliations } diff --git a/server/lib/prosody/config/content.ts b/server/lib/prosody/config/content.ts index 58e99c19..ce8a4f91 100644 --- a/server/lib/prosody/config/content.ts +++ b/server/lib/prosody/config/content.ts @@ -260,6 +260,8 @@ class ProsodyConfigContent { this.muc.set('anonymize_moderation_actions_form_position', 117) this.muc.add('modules_enabled', 'muc_mam_search') + + this.muc.add('modules_enabled', 'muc_peertubelivechat_restrict_message') } useAnonymous (autoBanIP: boolean): void { diff --git a/server/lib/routers/api/configuration.ts b/server/lib/routers/api/configuration.ts index 43e88be5..7038c4e2 100644 --- a/server/lib/routers/api/configuration.ts +++ b/server/lib/routers/api/configuration.ts @@ -16,6 +16,8 @@ import { import { sanitizeChannelConfigurationOptions } from '../../configuration/channel/sanitize' import { getConverseJSParams } from '../../../lib/conversejs/params' import { Emojis } from '../../../lib/emojis' +import { RoomChannel } from '../../../lib/room-channel' +import { updateProsodyRoom } from '../../../lib/prosody/api/manage-rooms' async function initConfigurationApiRouter (options: RegisterServerOptions, router: Router): Promise { const logger = options.peertubeHelpers.logger @@ -168,6 +170,20 @@ async function initConfigurationApiRouter (options: RegisterServerOptions, route await emojis.saveChannelDefinition(channelInfos.id, emojisDefinitionSanitized, bufferInfos) + // We must update the emoji only regexp on the Prosody server. + const emojisOnlyRegexp = await emojis.getChannelEmojisOnlyRegexp(channelInfos.id) + const roomJIDs = RoomChannel.singleton().getChannelRoomJIDs(channelInfos.id) + for (const roomJID of roomJIDs) { + // No need to await here + logger.info(`Updating room ${roomJID} emoji only regexp...`) + updateProsodyRoom(options, roomJID, { + livechat_emoji_only_regexp: emojisOnlyRegexp + }).then( + () => {}, + (err) => logger.error(err) + ) + } + // Reloading data, to send them back to front: const channelEmojis = (await emojis.channelCustomEmojisDefinition(channelInfos.id)) ?? diff --git a/server/lib/routers/api/room.ts b/server/lib/routers/api/room.ts index a3efbad6..f0d922ca 100644 --- a/server/lib/routers/api/room.ts +++ b/server/lib/routers/api/room.ts @@ -15,6 +15,7 @@ import { getChannelConfigurationOptions, getDefaultChannelConfigurationOptions } from '../../configuration/channel/storage' +import { Emojis } from '../../emojis' // See here for description: https://modules.prosody.im/mod_muc_http_defaults.html interface RoomDefaults { @@ -37,6 +38,8 @@ interface RoomDefaults { // Following fields are specific to livechat (for now), and requires a customized version for mod_muc_http_defaults. slow_mode_duration?: number mute_anonymous?: boolean + livechat_emoji_only?: boolean + livechat_emoji_only_regexp?: string livechat_muc_terms?: string moderation_delay?: number anonymize_moderation_actions?: boolean @@ -51,9 +54,12 @@ async function _getChannelSpecificOptions ( const channelOptions = await getChannelConfigurationOptions(options, channelId) ?? getDefaultChannelConfigurationOptions(options) + const emojiOnlyRegexp = await Emojis.singletonSafe()?.getChannelEmojisOnlyRegexp(channelId) + return { slow_mode_duration: channelOptions.slowMode.duration, mute_anonymous: channelOptions.mute.anonymous, + livechat_emoji_only_regexp: emojiOnlyRegexp, livechat_muc_terms: channelOptions.terms, moderation_delay: channelOptions.moderation.delay, anonymize_moderation_actions: channelOptions.moderation.anonymize