Emoji only mode WIP

This commit is contained in:
John Livingston 2024-09-05 18:28:54 +02:00
parent 2f78b901e3
commit 1a75b30c50
No known key found for this signature in database
GPG Key ID: B17B5640CE66CDBC
20 changed files with 375 additions and 6 deletions

View File

@ -1,5 +1,11 @@
# Changelog
## 11.1.0 (Not Released Yet)
### New features
* #131: Emoji only mode.
## 11.0.1
### Minor changes and fixes

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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);

View File

@ -83,6 +83,20 @@ const tplSlowMode = (o) => {
return html`<livechat-slow-mode jid=${o.model.get('jid')}>`
}
const tplEmojiOnly = (o) => {
if (!o.can_post) { return html`` }
if (!o.model.features?.get?.('x_peertubelivechat_emoji_only_mode')) {
return ''
}
return html`<div class="livechat-emoji-only-info-box">
<converse-icon class="fa fa-info-circle" size="1.2em"></converse-icon>
${
// eslint-disable-next-line no-undef
__(LOC_emoji_only_info)
}
</div>`
}
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`<span class="muc-bottom-panel muc-bottom-panel--muted">${mutedAnonymousMessage}</span>`

View File

@ -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...

View File

@ -0,0 +1,23 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// 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<any> {
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)
}
}

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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)

View File

@ -0,0 +1,14 @@
<!--
SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
SPDX-License-Identifier: AGPL-3.0-only
-->
# 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).

View File

@ -0,0 +1,131 @@
-- mod_muc_peertubelivechat_roles
--
-- SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
-- 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);

View File

@ -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(

View File

@ -23,6 +23,7 @@ export class Emojis {
protected channelBasePath: string
protected channelBaseUri: string
protected readonly channelCache = new Map<number, boolean>()
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<string | undefined> {
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<void> {
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<string[]> {
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)
)
}

View File

@ -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
}

View File

@ -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 {

View File

@ -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<void> {
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)) ??

View File

@ -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