Emoji only mode WIP:

* refactoring + optimization
* migration
This commit is contained in:
John Livingston 2024-09-06 11:01:48 +02:00
parent b115c28ee7
commit 08017ac2bb
No known key found for this signature in database
GPG Key ID: B17B5640CE66CDBC
12 changed files with 172 additions and 50 deletions

View File

@ -18,7 +18,7 @@ local mod_muc_peertubelivechat_terms = module:depends"muc_peertubelivechat_terms
local set_muc_terms = rawget(mod_muc_peertubelivechat_terms, "set_muc_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 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_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"); local set_peertubelivechat_custom_emoji_regexp = rawget(mod_muc_peertubelivechat_restrict_message, "set_peertubelivechat_custom_emoji_regexp");
function check_auth(routes) function check_auth(routes)
local function check_request_auth(event) local function check_request_auth(event)
@ -110,9 +110,9 @@ local function update_room(event)
set_peertubelivechat_emoji_only_mode(room, config.livechat_emoji_only) set_peertubelivechat_emoji_only_mode(room, config.livechat_emoji_only)
end end
end end
if type(config.livechat_emoji_only_regexp) == "string" then if type(config.livechat_custom_emoji_regexp) == "string" then
if set_peertubelivechat_emoji_only_mode then if set_peertubelivechat_custom_emoji_regexp then
set_peertubelivechat_emoji_only_regexp(room, config.livechat_emoji_only_regexp) set_peertubelivechat_custom_emoji_regexp(room, config.livechat_custom_emoji_regexp)
end end
end end
if (type(config.livechat_muc_terms) == "string") then if (type(config.livechat_muc_terms) == "string") then

View File

@ -10,7 +10,7 @@
-- * "moderation_delay" -- * "moderation_delay"
-- * "anonymize_moderation_actions" -- * "anonymize_moderation_actions"
-- * "livechat_emoji_only" -- * "livechat_emoji_only"
-- * "livechat_emoji_only_regexp" -- * "livechat_custom_emoji_regexp"
-- * "livechat_muc_terms" -- * "livechat_muc_terms"
-- These options are introduced in the Peertube livechat plugin. -- These options are introduced in the Peertube livechat plugin.
-- --
@ -134,8 +134,8 @@ local function apply_config(room, settings)
if (type(config.livechat_emoji_only) == "boolean") then if (type(config.livechat_emoji_only) == "boolean") then
room._data.x_peertubelivechat_emoji_only_mode = config.livechat_emoji_only; room._data.x_peertubelivechat_emoji_only_mode = config.livechat_emoji_only;
end end
if (type(config.livechat_emoji_only_regexp) == "string" and config.livechat_emoji_only_regexp ~= "") then if (type(config.livechat_custom_emoji_regexp) == "string" and config.livechat_custom_emoji_regexp ~= "") then
room._data.x_peertubelivechat_emoji_only_regexp = config.livechat_emoji_only_regexp; room._data.x_peertubelivechat_custom_emoji_regexp = config.livechat_custom_emoji_regexp;
end end
if (type(config.livechat_muc_terms) == "string") then 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 -- we don't need to use set_muc_terms here, as this is called for a newly created room

View File

@ -19,6 +19,7 @@ local mod_muc = module:depends "muc";
local muc_util = module:require "muc/util"; local muc_util = module:require "muc/util";
local valid_roles = muc_util.valid_roles; local valid_roles = muc_util.valid_roles;
local common_emoji_regexp = assert(module:get_option_string('peertubelivechat_restrict_message_common_emoji_regexp'), 'Common emoji regexp is mandatory');
function get_peertubelivechat_emoji_only_mode(room) function get_peertubelivechat_emoji_only_mode(room)
return room._data.x_peertubelivechat_emoji_only_mode; return room._data.x_peertubelivechat_emoji_only_mode;
@ -31,17 +32,17 @@ function set_peertubelivechat_emoji_only_mode(room, emoji_only)
return true; return true;
end end
function get_peertubelivechat_emoji_only_regexp(room) function get_peertubelivechat_custom_emoji_regexp(room)
return room._data.x_peertubelivechat_emoji_only_regexp; return room._data.x_peertubelivechat_custom_emoji_regexp;
end end
function set_peertubelivechat_emoji_only_regexp(room, emoji_only_regexp) function set_peertubelivechat_custom_emoji_regexp(room, emoji_only_regexp)
if (emoji_only_regexp ~= nil and type(emoji_only_regexp) ~= "string") then if (emoji_only_regexp ~= nil and type(emoji_only_regexp) ~= "string") then
return false; return false;
end end
if emoji_only_regexp == "" then emoji_only_regexp = nil; 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 if get_peertubelivechat_custom_emoji_regexp(room) == emoji_only_regexp then return false; end
room._data.x_peertubelivechat_emoji_only_regexp = emoji_only_regexp; room._data.x_peertubelivechat_custom_emoji_regexp = emoji_only_regexp;
-- and we must decache the compile regexp -- and we must decache the compile regexp
room.x_peertubelivechat_emoji_only_compiled_regexp = nil; room.x_peertubelivechat_emoji_only_compiled_regexp = nil;
@ -49,13 +50,12 @@ function set_peertubelivechat_emoji_only_regexp(room, emoji_only_regexp)
end end
module:hook("muc-disco#info", function(event) 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 if get_peertubelivechat_emoji_only_mode(event.room) then
event.reply:tag("feature", {var = "x_peertubelivechat_emoji_only_mode"}):up(); event.reply:tag("feature", {var = "x_peertubelivechat_emoji_only_mode"}):up();
end end
end); end);
module:hook("muc-config-form", function(event) module:hook("muc-config-form", function(event)
if (get_peertubelivechat_emoji_only_regexp(event.room) ~= nil) then
table.insert(event.form, { table.insert(event.form, {
name = "muc#roomconfig_x_peertubelivechat_emoji_only_mode"; name = "muc#roomconfig_x_peertubelivechat_emoji_only_mode";
type = "boolean"; type = "boolean";
@ -63,11 +63,10 @@ module:hook("muc-config-form", function(event)
desc = "Occupants will only be able to send emoji. This does not affect moderators."; desc = "Occupants will only be able to send emoji. This does not affect moderators.";
value = get_peertubelivechat_emoji_only_mode(event.room); value = get_peertubelivechat_emoji_only_mode(event.room);
}); });
end end, 122);
end, 121);
module:hook("muc-config-submitted/muc#roomconfig_x_peertubelivechat_emoji_only_mode", function(event) 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 if set_peertubelivechat_emoji_only_mode(event.room, event.value) then
event.status_codes["104"] = true; event.status_codes["104"] = true;
end end
end); end);
@ -84,10 +83,14 @@ function handle_groupchat(event)
if not room.x_peertubelivechat_emoji_only_compiled_regexp then if not room.x_peertubelivechat_emoji_only_compiled_regexp then
-- compute the regexp on first access -- compute the regexp on first access
local r = get_peertubelivechat_emoji_only_regexp(room); local r = get_peertubelivechat_custom_emoji_regexp(room);
if (r == nil) then if (r == nil or r == "") then
return; r = common_emoji_regexp;
else
r = r .. "|" .. common_emoji_regexp;
end end
r = "^\\s*(?:(?:" .. r .. ")\\s*)+\\s*$"
room.x_peertubelivechat_emoji_only_compiled_regexp = rex.new(r, "i", "UTF8"); room.x_peertubelivechat_emoji_only_compiled_regexp = rex.new(r, "i", "UTF8");
end end

View File

@ -120,9 +120,10 @@ async function initChannelConfiguration (options: RegisterServerOptions): Promis
// but will be more efficient to add here, as we already tested hasChat). // but will be more efficient to add here, as we already tested hasChat).
// Note: no need to await here, would only degrade performances. // 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_muc_terms if channel has changed.
// FIXME: should also update livechat_emoji_only_regexp if channel has changed.
updateProsodyRoom(options, video.uuid, { updateProsodyRoom(options, video.uuid, {
name: video.name name: video.name,
// In case the channel changed:
livechat_custom_emoji_regexp: await Emojis.singletonSafe()?.getChannelCustomEmojisRegexp(video.channelId)
}).then( }).then(
() => {}, () => {},
(err) => logger.error(err) (err) => logger.error(err)

View File

@ -144,7 +144,7 @@ export class Emojis {
* @param sn short name * @param sn short name
*/ */
public validShortName (sn: any): boolean { public validShortName (sn: any): boolean {
// Important note: do not change this without checking if it can breaks getChannelEmojisOnlyRegexp. // Important note: do not change this without checking if it can breaks getChannelCustomEmojisRegexp.
if ((typeof sn !== 'string') || !/^:?[\w-]+:?$/.test(sn)) { if ((typeof sn !== 'string') || !/^:?[\w-]+:?$/.test(sn)) {
this.logger.debug('Short name invalid: ' + (typeof sn === 'string' ? sn : '???')) this.logger.debug('Short name invalid: ' + (typeof sn === 'string' ? sn : '???'))
return false return false
@ -395,14 +395,13 @@ export class Emojis {
} }
/** /**
* Returns a string representing a regular expression (Perl Compatible RE) that can validate that a message * Returns a string representing a regular expression validating channel custom emojis.
* contains only emojis (for this channel).
* This is used for the emoji only mode (test are made on the Prosody server). * This is used for the emoji only mode (test are made on the Prosody server).
* *
* @param channelId channel id * @param channelId channel id
*/ */
public async getChannelEmojisOnlyRegexp (channelId: number): Promise<string | undefined> { public async getChannelCustomEmojisRegexp (channelId: number): Promise<string | undefined> {
const parts = [...this.commonEmojisCodes] const parts = []
if (await this.channelHasCustomEmojis(channelId)) { if (await this.channelHasCustomEmojis(channelId)) {
const def = await this.channelCustomEmojisDefinition(channelId) const def = await this.channelCustomEmojisDefinition(channelId)
@ -411,11 +410,17 @@ export class Emojis {
} }
} }
// Note: validShortName should ensure we won't put special chars. if (parts.length === 0) {
// And for the common emojis, we assume that there is no special regexp chars return undefined
const regexp = '^\\s*(?:(?:' + parts.join('|') + ')\\s*)+\\s*$' }
return regexp // Note: validShortName should ensure we won't put special chars.
return parts.join('|')
}
public getCommonEmojisRegexp (): string {
// We assume that there is no special regexp chars (should only contains unicode emojis)
return this.commonEmojisCodes.join('|')
} }
/** /**

View File

@ -66,7 +66,7 @@ async function updateProsodyRoom (
slow_mode_duration?: number slow_mode_duration?: number
moderation_delay?: number moderation_delay?: number
livechat_emoji_only?: boolean livechat_emoji_only?: boolean
livechat_emoji_only_regexp?: string livechat_custom_emoji_regexp?: string
livechat_muc_terms?: string livechat_muc_terms?: string
addAffiliations?: Affiliations addAffiliations?: Affiliations
removeAffiliationsFor?: string[] removeAffiliationsFor?: string[]
@ -105,8 +105,8 @@ async function updateProsodyRoom (
if ('livechat_emoji_only' in data) { if ('livechat_emoji_only' in data) {
apiData.livechat_emoji_only = data.livechat_emoji_only ?? false apiData.livechat_emoji_only = data.livechat_emoji_only ?? false
} }
if ('livechat_emoji_only_regexp' in data) { if ('livechat_custom_emoji_regexp' in data) {
apiData.livechat_emoji_only_regexp = data.livechat_emoji_only_regexp ?? '' apiData.livechat_custom_emoji_regexp = data.livechat_custom_emoji_regexp ?? ''
} }
if (('addAffiliations' in data) && data.addAffiliations !== undefined) { if (('addAffiliations' in data) && data.addAffiliations !== undefined) {
apiData.addAffiliations = data.addAffiliations apiData.addAffiliations = data.addAffiliations

View File

@ -19,6 +19,7 @@ import { BotConfiguration } from '../configuration/bot'
import { debugMucAdmins } from '../debug' import { debugMucAdmins } from '../debug'
import { ExternalAuthOIDC } from '../external-auth/oidc' import { ExternalAuthOIDC } from '../external-auth/oidc'
import { listModFirewallFiles } from '../firewall/config' import { listModFirewallFiles } from '../firewall/config'
import { Emojis } from '../emojis'
async function getWorkingDir (options: RegisterServerOptions): Promise<string> { async function getWorkingDir (options: RegisterServerOptions): Promise<string> {
const peertubeHelpers = options.peertubeHelpers const peertubeHelpers = options.peertubeHelpers
@ -389,6 +390,13 @@ async function getProsodyConfig (options: RegisterServerOptionsV5): Promise<Pros
config.useModFirewall(modFirewallFiles) config.useModFirewall(modFirewallFiles)
} }
const commonEmojisRegexp = Emojis.singletonSafe()?.getCommonEmojisRegexp()
if (commonEmojisRegexp) {
config.useRestrictMessage(commonEmojisRegexp)
} else {
logger.error('Fail to load common emojis regexp, disabling restrict message module.')
}
config.useTestModule(apikey, testApiUrl) config.useTestModule(apikey, testApiUrl)
const debugMucAdminJids = debugMucAdmins(options) const debugMucAdminJids = debugMucAdmins(options)
@ -500,6 +508,11 @@ function getProsodyConfigContentForDiagnostic (config: ProsodyConfig, content?:
// replaceAll not available, using trick: // replaceAll not available, using trick:
r = r.split(value).join(`***${key}***`) r = r.split(value).join(`***${key}***`)
} }
// We also replace `peertubelivechat_restrict_message_common_emoji_regexp` because it could be a very long line
r = r.replace(
/^(?:(\s*peertubelivechat_restrict_message_common_emoji_regexp\s*=\s*.{0,10}).*)$/gm,
'$1 ***long line truncated***'
)
return r return r
} }

View File

@ -260,8 +260,6 @@ class ProsodyConfigContent {
this.muc.set('anonymize_moderation_actions_form_position', 117) this.muc.set('anonymize_moderation_actions_form_position', 117)
this.muc.add('modules_enabled', 'muc_mam_search') this.muc.add('modules_enabled', 'muc_mam_search')
this.muc.add('modules_enabled', 'muc_peertubelivechat_restrict_message')
} }
useAnonymous (autoBanIP: boolean): void { useAnonymous (autoBanIP: boolean): void {
@ -564,6 +562,18 @@ class ProsodyConfigContent {
this.global.set('firewall_scripts', files) this.global.set('firewall_scripts', files)
} }
/**
* Enable and configure the restrict message module.
* @param commonEmojiRegexp A regexp to match common emojis.
*/
useRestrictMessage (commonEmojiRegexp: string): void {
this.muc.add('modules_enabled', 'muc_peertubelivechat_restrict_message')
this.muc.set(
'peertubelivechat_restrict_message_common_emoji_regexp',
new ConfigEntryValueMultiLineString(commonEmojiRegexp)
)
}
addMucAdmins (jids: string[]): void { addMucAdmins (jids: string[]): void {
for (const jid of jids) { for (const jid of jids) {
this.muc.add('admins', jid) this.muc.add('admins', jid)

View File

@ -0,0 +1,76 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
import type { RegisterServerOptions } from '@peertube/peertube-types'
import { listProsodyRooms, updateProsodyRoom } from '../api/manage-rooms'
import * as path from 'path'
import * as fs from 'fs'
import { Emojis } from '../../emojis'
/**
* Livechat v11.1.0: we must send channel custom emojis regexp to Prosody.
*
* This script will only be launched one time.
*/
async function updateProsodyChannelEmojisRegex (options: RegisterServerOptions): Promise<void> {
const logger = options.peertubeHelpers.logger
// First, detect if we already run this script.
const doneFilePath = path.resolve(options.peertubeHelpers.plugin.getDataDirectoryPath(), 'fix-v11.1-emojis')
if (fs.existsSync(doneFilePath)) {
logger.debug('[migratev11_1_ChannelEmojis] Channel Emojis Regex already updated on Prosody.')
return
}
logger.info('[migratev11_1_ChannelEmojis] Updating Channel custom emojis regexp on Prosody')
const emojis = Emojis.singleton()
const rooms = await listProsodyRooms(options)
logger.debug('[migratev11_1_ChannelEmojis] Found ' + rooms.length.toString() + ' rooms.')
for (const room of rooms) {
try {
let channelId: number
logger.info('[migratev11_1_ChannelEmojis] Must update custom emojis regexp for room ' + room.localpart)
const matches = room.localpart.match(/^channel\.(\d+)$/)
if (matches?.[1]) {
// room associated to a channel
channelId = parseInt(matches[1])
} else {
// room associated to a video
const video = await options.peertubeHelpers.videos.loadByIdOrUUID(room.localpart)
if (!video || video.remote) {
logger.info('[migratev11_1_ChannelEmojis] Video ' + room.localpart + ' not found or remote, skipping')
continue
}
channelId = video.channelId
}
if (!channelId) {
throw new Error('Cant find channelId')
}
const regexp = await emojis.getChannelCustomEmojisRegexp(channelId)
if (regexp === undefined) {
logger.info('[migratev11_1_ChannelEmojis] Room ' + room.localpart + ' channel has no custom emojis, skipping.')
continue
}
await updateProsodyRoom(options, room.jid, {
livechat_custom_emoji_regexp: regexp
})
} catch (err) {
logger.error(
'[migratev11_1_ChannelEmojis] Failed to handle room ' + room.localpart + ', skipping. Error: ' + (err as string)
)
continue
}
}
await fs.promises.writeFile(doneFilePath, '')
}
export {
updateProsodyChannelEmojisRegex
}

View File

@ -171,13 +171,13 @@ async function initConfigurationApiRouter (options: RegisterServerOptions, route
await emojis.saveChannelDefinition(channelInfos.id, emojisDefinitionSanitized, bufferInfos) await emojis.saveChannelDefinition(channelInfos.id, emojisDefinitionSanitized, bufferInfos)
// We must update the emoji only regexp on the Prosody server. // We must update the emoji only regexp on the Prosody server.
const emojisOnlyRegexp = await emojis.getChannelEmojisOnlyRegexp(channelInfos.id) const customEmojisRegexp = await emojis.getChannelCustomEmojisRegexp(channelInfos.id)
const roomJIDs = RoomChannel.singleton().getChannelRoomJIDs(channelInfos.id) const roomJIDs = RoomChannel.singleton().getChannelRoomJIDs(channelInfos.id)
for (const roomJID of roomJIDs) { for (const roomJID of roomJIDs) {
// No need to await here // No need to await here
logger.info(`Updating room ${roomJID} emoji only regexp...`) logger.info(`Updating room ${roomJID} emoji only regexp...`)
updateProsodyRoom(options, roomJID, { updateProsodyRoom(options, roomJID, {
livechat_emoji_only_regexp: emojisOnlyRegexp livechat_custom_emoji_regexp: customEmojisRegexp
}).then( }).then(
() => {}, () => {},
(err) => logger.error(err) (err) => logger.error(err)

View File

@ -39,7 +39,7 @@ interface RoomDefaults {
slow_mode_duration?: number slow_mode_duration?: number
mute_anonymous?: boolean mute_anonymous?: boolean
livechat_emoji_only?: boolean livechat_emoji_only?: boolean
livechat_emoji_only_regexp?: string livechat_custom_emoji_regexp?: string
livechat_muc_terms?: string livechat_muc_terms?: string
moderation_delay?: number moderation_delay?: number
anonymize_moderation_actions?: boolean anonymize_moderation_actions?: boolean
@ -54,12 +54,12 @@ async function _getChannelSpecificOptions (
const channelOptions = await getChannelConfigurationOptions(options, channelId) ?? const channelOptions = await getChannelConfigurationOptions(options, channelId) ??
getDefaultChannelConfigurationOptions(options) getDefaultChannelConfigurationOptions(options)
const emojiOnlyRegexp = await Emojis.singletonSafe()?.getChannelEmojisOnlyRegexp(channelId) const customEmojisRegexp = await Emojis.singletonSafe()?.getChannelCustomEmojisRegexp(channelId)
return { return {
slow_mode_duration: channelOptions.slowMode.duration, slow_mode_duration: channelOptions.slowMode.duration,
mute_anonymous: channelOptions.mute.anonymous, mute_anonymous: channelOptions.mute.anonymous,
livechat_emoji_only_regexp: emojiOnlyRegexp, livechat_custom_emoji_regexp: customEmojisRegexp,
livechat_muc_terms: channelOptions.terms, livechat_muc_terms: channelOptions.terms,
moderation_delay: channelOptions.moderation.delay, moderation_delay: channelOptions.moderation.delay,
anonymize_moderation_actions: channelOptions.moderation.anonymize anonymize_moderation_actions: channelOptions.moderation.anonymize

View File

@ -18,6 +18,7 @@ import { BotConfiguration } from './lib/configuration/bot'
import { BotsCtl } from './lib/bots/ctl' import { BotsCtl } from './lib/bots/ctl'
import { ExternalAuthOIDC } from './lib/external-auth/oidc' import { ExternalAuthOIDC } from './lib/external-auth/oidc'
import { migrateMUCAffiliations } from './lib/prosody/migration/migrateV10' import { migrateMUCAffiliations } from './lib/prosody/migration/migrateV10'
import { updateProsodyChannelEmojisRegex } from './lib/prosody/migration/migrageV11-1'
import { Emojis } from './lib/emojis' import { Emojis } from './lib/emojis'
import { LivechatProsodyAuth } from './lib/prosody/auth' import { LivechatProsodyAuth } from './lib/prosody/auth'
import decache from 'decache' import decache from 'decache'
@ -87,11 +88,24 @@ async function register (options: RegisterServerOptions): Promise<any> {
// livechat v10.0.0: we must migrate MUC affiliations (but we don't have to wait) // livechat v10.0.0: we must migrate MUC affiliations (but we don't have to wait)
// we do this after the preBotPromise, just to avoid doing both at the same time. // we do this after the preBotPromise, just to avoid doing both at the same time.
preBotPromise.then(() => { preBotPromise.then(() => {
migrateMUCAffiliations(options).then( const p = migrateMUCAffiliations(options).then(
() => {}, () => {},
(err) => { (err) => {
logger.error(err) logger.error(err)
}) }
)
// livechat v11.1: we must send channel emojis regexp to Prosody rooms
p.finally(
() => {
updateProsodyChannelEmojisRegex(options).then(
() => {},
(err: any) => {
logger.error(err)
}
)
}
)
}, () => {}) }, () => {})
} catch (error) { } catch (error) {
options.peertubeHelpers.logger.error('Error when launching Prosody: ' + (error as string)) options.peertubeHelpers.logger.error('Error when launching Prosody: ' + (error as string))