New option for the moderation bot: forbid duplicate messages (#516).
This commit is contained in:
parent
651641f63c
commit
5225257bb5
@ -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
|
||||
|
5
client/@types/global.d.ts
vendored
5
client/@types/global.d.ts
vendored
@ -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
|
||||
|
@ -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)}
|
||||
|
@ -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`] = []
|
||||
|
@ -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
14
package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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')
|
||||
|
@ -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]
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user