New option for the moderation bot: forbid duplicate messages (#516).

This commit is contained in:
John Livingston 2024-09-10 18:59:56 +02:00
parent 651641f63c
commit 5225257bb5
No known key found for this signature in database
GPG Key ID: B17B5640CE66CDBC
11 changed files with 227 additions and 10 deletions

View File

@ -2,9 +2,13 @@
## 11.1.0 (Not Released Yet)
TODO Before releasing:
* update ConverseJS with latest merges.
### New features
* #131: Emoji only mode.
* #516: new option for the moderation bot: forbid duplicate messages.
* #517: new option for the moderation bot: forbid messages with too many special characters.
### Minor changes and fixes

View File

@ -155,3 +155,8 @@ declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_SPECIAL_CHARS_LABEL: string
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_SPECIAL_CHARS_DESC: string
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_SPECIAL_CHARS_TOLERANCE_LABEL: string
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_SPECIAL_CHARS_TOLERANCE_DESC: string
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_LABEL: string
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_DESC: string
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_DELAY_LABEL: string
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_DELAY_DESC: string

View File

@ -460,6 +460,126 @@ export function tplChannelConfiguration (el: ChannelConfigurationElement): Templ
`
}
<livechat-configuration-section-header
.label=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_LABEL)}
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_DESC)}
.helpPage=${'documentation/user/streamers/bot/no_duplicate'}>
</livechat-configuration-section-header>
<div class="form-group">
<label>
<input
type="checkbox"
name="no_duplicate"
id="peertube-livechat-no-duplicate"
@input=${(event: InputEvent) => {
if (event?.target && el.channelConfiguration) {
el.channelConfiguration.configuration.bot.noDuplicate.enabled =
(event.target as HTMLInputElement).checked
}
el.requestUpdate('channelConfiguration')
}
}
value="1"
?checked=${el.channelConfiguration?.configuration.bot.noDuplicate.enabled}
/>
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_LABEL)}
</label>
</div>
${!el.channelConfiguration?.configuration.bot.noDuplicate.enabled
? ''
: html`
<div class="form-group">
<label>
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_DELAY_LABEL)}
<input
type="number"
name="no_duplicate_delay"
class=${classMap(
Object.assign(
{ 'form-control': true },
el.getInputValidationClass('bot.noDuplicate.delay')
)
)}
min="0"
max="10"
id="peertube-livechat-no-duplicate-delay"
aria-describedby="peertube-livechat-no-duplicate-delay-feedback"
@input=${(event: InputEvent) => {
if (event?.target && el.channelConfiguration) {
el.channelConfiguration.configuration.bot.noDuplicate.delay =
Number((event.target as HTMLInputElement).value)
}
el.requestUpdate('channelConfiguration')
}
}
value="${el.channelConfiguration?.configuration.bot.noDuplicate.delay ?? '0'}"
/>
</label>
<small class="form-text text-muted">
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_NO_DUPLICATE_DELAY_DESC)}
</small>
${el.renderFeedback('peertube-livechat-no-duplicate-delay-feedback',
'bot.noDuplicate.delay')
}
</div>
<div class="form-group">
<label>
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_RETRACTATION_REASON_LABEL)}
<input
type="text"
name="no_duplicate_reason"
class=${classMap(
Object.assign(
{ 'form-control': true },
el.getInputValidationClass('bot.noDuplicate.reason')
)
)}
id="peertube-livechat-no-duplicate-reason"
aria-describedby="peertube-livechat-no-duplicate-reason-feedback"
@input=${(event: InputEvent) => {
if (event?.target && el.channelConfiguration) {
el.channelConfiguration.configuration.bot.noDuplicate.reason =
(event.target as HTMLInputElement).value
}
el.requestUpdate('channelConfiguration')
}
}
value="${el.channelConfiguration?.configuration.bot.noDuplicate.reason ?? ''}"
/>
</label>
<small class="form-text text-muted">
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_RETRACTATION_REASON_DESC)}
</small>
${el.renderFeedback('peertube-livechat-no-duplicate-reason-feedback',
'bot.noDuplicate.reason')
}
</div>
<div class="form-group">
<label>
<input
type="checkbox"
name="no_duplicate_applyToModerators"
id="peertube-livechat-no-duplicate-applyToModerators"
@input=${(event: InputEvent) => {
if (event?.target && el.channelConfiguration) {
el.channelConfiguration.configuration.bot.noDuplicate.applyToModerators =
(event.target as HTMLInputElement).checked
}
el.requestUpdate('channelConfiguration')
}
}
value="1"
?checked=${el.channelConfiguration?.configuration.bot.noDuplicate.applyToModerators}
/>
${ptTr(LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_LABEL)}
</label>
<small class="form-text text-muted">
${ptTr(LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_DESC)}
</small>
</div>
`
}
<livechat-configuration-section-header
.label=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL)}
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_DESC)}

View File

@ -68,6 +68,7 @@ export class ChannelDetailsService {
if (botConf.enabled) {
propertiesError['bot.nickname'] = []
propertiesError['bot.forbidSpecialChars.tolerance'] = []
propertiesError['bot.noDuplicate.delay'] = []
if (/[^\p{L}\p{N}\p{Z}_-]/u.test(botConf.nickname ?? '')) {
propertiesError['bot.nickname'].push(ValidationErrorType.WrongFormat)
@ -88,6 +89,21 @@ export class ChannelDetailsService {
}
}
if (botConf.noDuplicate.enabled) {
const noDuplicateDelay = channelConfigurationOptions.bot.noDuplicate.delay
if (
(typeof noDuplicateDelay !== 'number') ||
isNaN(noDuplicateDelay)
) {
propertiesError['bot.noDuplicate.delay'].push(ValidationErrorType.WrongType)
} else if (
noDuplicateDelay < 0 ||
noDuplicateDelay > 24 * 3600
) {
propertiesError['bot.noDuplicate.delay'].push(ValidationErrorType.NotInRange)
}
}
for (const [i, fw] of botConf.forbiddenWords.entries()) {
for (const v of fw.entries) {
propertiesError[`bot.forbiddenWords.${i}.entries`] = []

View File

@ -662,3 +662,11 @@ livechat_configuration_channel_special_chars_tolerance_label: Tolerance
livechat_configuration_channel_special_chars_tolerance_desc: Number of special characters to accept before deleting messages.
feature_comes_with: This feature comes with the livechat plugin version X.X.X.
livechat_configuration_channel_no_duplicate_label: "No duplicate message"
livechat_configuration_channel_no_duplicate_desc: |
By enabling this options, the moderation bot will automatically moderate duplicate messages.
That means if a user send the same message twice within X seconds, the second message will be deleted.
livechat_configuration_channel_no_duplicate_delay_label: Time interval
livechat_configuration_channel_no_duplicate_delay_desc: |
The interval, in seconds, during which a user can't send again the same message.

14
package-lock.json generated
View File

@ -18,7 +18,7 @@
"log-rotate": "^0.2.8",
"openid-client": "^5.7.0",
"validate-color": "^2.2.4",
"xmppjs-chat-bot": "^0.4.0"
"xmppjs-chat-bot": "^0.5.0"
},
"devDependencies": {
"@eslint/js": "^9.10.0",
@ -12932,9 +12932,9 @@
}
},
"node_modules/xmppjs-chat-bot": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/xmppjs-chat-bot/-/xmppjs-chat-bot-0.4.0.tgz",
"integrity": "sha512-vN+hWlrSDKmOK+XDOx3VmBffQkEYtfEhLDiovwy8PqPJnyEGESsIcva33hvzWrBYES8hTz1DX320aFYx5tnnNA==",
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/xmppjs-chat-bot/-/xmppjs-chat-bot-0.5.0.tgz",
"integrity": "sha512-P+C2x00zS3Zd0PvkKeiT+cPYMQOW/pZu2pEG+iNkUjHU5MYBukn48aMgbzzPwJVQpk/9FuZZeK1m/pl3iD2KFA==",
"funding": [
"https://paypal.me/JohnXLivingston",
"https://liberapay.com/JohnLivingston/"
@ -22351,9 +22351,9 @@
}
},
"xmppjs-chat-bot": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/xmppjs-chat-bot/-/xmppjs-chat-bot-0.4.0.tgz",
"integrity": "sha512-vN+hWlrSDKmOK+XDOx3VmBffQkEYtfEhLDiovwy8PqPJnyEGESsIcva33hvzWrBYES8hTz1DX320aFYx5tnnNA==",
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/xmppjs-chat-bot/-/xmppjs-chat-bot-0.5.0.tgz",
"integrity": "sha512-P+C2x00zS3Zd0PvkKeiT+cPYMQOW/pZu2pEG+iNkUjHU5MYBukn48aMgbzzPwJVQpk/9FuZZeK1m/pl3iD2KFA==",
"requires": {
"@xmpp/client": "^0.13.1",
"@xmpp/component": "^0.13.1",

View File

@ -35,7 +35,7 @@
"log-rotate": "^0.2.8",
"openid-client": "^5.7.0",
"validate-color": "^2.2.4",
"xmppjs-chat-bot": "^0.4.0"
"xmppjs-chat-bot": "^0.5.0"
},
"devDependencies": {
"@eslint/js": "^9.10.0",

View File

@ -57,7 +57,18 @@ async function sanitizeChannelConfigurationOptions (
applyToModerators: false
}
if (!_assertObjectType(botData.forbidSpecialChars)) {
throw new Error('Invalid data.forbidSpecialChars data type')
throw new Error('Invalid data.bot.forbidSpecialChars data type')
}
// noDuplicate comes with livechat 11.1.0
botData.noDuplicate ??= {
enabled: false,
reason: '',
delay: 60,
applyToModerators: false
}
if (!_assertObjectType(botData.noDuplicate)) {
throw new Error('Invalid data.bot.noDuplicate data type')
}
// terms not present in livechat <= 10.2.0
@ -76,6 +87,7 @@ async function sanitizeChannelConfigurationOptions (
nickname: _readSimpleInput(botData, 'nickname', true),
forbiddenWords: await _readForbiddenWords(botData),
forbidSpecialChars: await _readForbidSpecialChars(botData),
noDuplicate: await _readNoDuplicate(botData),
quotes: _readQuotes(botData),
commands: _readCommands(botData)
// TODO: bannedJIDs
@ -266,6 +278,21 @@ async function _readForbidSpecialChars (
return result
}
async function _readNoDuplicate (
botData: Record<string, unknown>
): Promise<ChannelConfigurationOptions['bot']['noDuplicate']> {
if (!_assertObjectType(botData.noDuplicate)) {
throw new Error('Invalid forbidSpecialChars data')
}
const result: ChannelConfigurationOptions['bot']['noDuplicate'] = {
enabled: _readBoolean(botData.noDuplicate, 'enabled'),
reason: _readSimpleInput(botData.noDuplicate, 'reason'),
delay: _readInteger(botData.noDuplicate, 'delay', 0, 24 * 3600),
applyToModerators: _readBoolean(botData.noDuplicate, 'applyToModerators')
}
return result
}
function _readQuotes (botData: Record<string, unknown>): ChannelConfigurationOptions['bot']['quotes'] {
if (!Array.isArray(botData.quotes)) {
throw new Error('Invalid quotes data')

View File

@ -50,6 +50,12 @@ function getDefaultChannelConfigurationOptions (_options: RegisterServerOptions)
tolerance: 0,
applyToModerators: false
},
noDuplicate: {
enabled: false,
reason: '',
delay: 60,
applyToModerators: false
},
quotes: [],
commands: []
},
@ -124,6 +130,11 @@ function channelConfigurationOptionsToBotRoomConf (
handlersIds.set(id, true)
handlers.push(_getForbidSpecialCharsHandler(id, channelConfigurationOptions.bot.forbidSpecialChars))
}
if (channelConfigurationOptions.bot.noDuplicate.enabled) {
const id = 'no_duplicate'
handlersIds.set(id, true)
handlers.push(_getNoDuplicateHandler(id, channelConfigurationOptions.bot.noDuplicate))
}
channelConfigurationOptions.bot.quotes.forEach((v, i) => {
const id = 'quote_' + i.toString()
handlersIds.set(id, true)
@ -254,6 +265,23 @@ function _getForbidSpecialCharsHandler (
return handler
}
function _getNoDuplicateHandler (
id: string,
noDuplicate: ChannelConfigurationOptions['bot']['noDuplicate']
): ConfigHandler {
const handler: ConfigHandler = {
type: 'no-duplicate',
id,
enabled: true,
options: {
reason: noDuplicate.reason,
delay: noDuplicate.delay,
applyToModerators: !!noDuplicate.applyToModerators
}
}
return handler
}
function _getQuotesHandler (
id: string,
quotes: ChannelConfigurationOptions['bot']['quotes'][0]

View File

@ -96,7 +96,7 @@ async function initConfigurationApiRouter (options: RegisterServerOptions, route
req.body.bot = channelOptions.bot
req.body.bot.enabled = false
}
// TODO: Same for forbidSpecialChars: if disabled, don't save reason and tolerance
// TODO: Same for forbidSpecialChars/noDuplicate: if disabled, don't save reason and tolerance
// (disabling for now, because it is not acceptable to load twice the channel configuration.
// Must find better way)
// if (req.body.bot?.enabled === true && req.body.bot.forbidSpecialChars?.enabled === false) {
@ -108,6 +108,7 @@ async function initConfigurationApiRouter (options: RegisterServerOptions, route
// req.body.bot.forbidSpecialChars.tolerance = channelOptions.bot.forbidSpecialChars.tolerance
// req.body.bot.forbidSpecialChars.applyToModerators = channelOptions.bot.forbidSpecialChars.applyToModerators
// req.body.bot.forbidSpecialChars.enabled = false
// ... NoDuplicate...
// }
channelOptions = await sanitizeChannelConfigurationOptions(options, channelInfos.id, req.body)
} catch (err) {

View File

@ -97,6 +97,7 @@ interface ChannelConfigurationOptions {
quotes: ChannelQuotes[]
commands: ChannelCommands[]
forbidSpecialChars: ChannelForbidSpecialChars
noDuplicate: ChannelNoDuplicate
// TODO: bannedJIDs: string[]
}
slowMode: {
@ -140,6 +141,13 @@ interface ChannelForbidSpecialChars {
applyToModerators: boolean
}
interface ChannelNoDuplicate {
enabled: boolean
reason: string
delay: number
applyToModerators: boolean
}
interface ChannelConfiguration {
channel: ChannelInfos
configuration: ChannelConfigurationOptions