peertube-plugin-livechat/server/lib/configuration/channel/sanitize.ts
John Livingston cf179d7f72
Slow mode WIP (#192):
* default delay is configurable in channel's chat rooms options.
2024-02-16 17:35:53 +01:00

227 lines
6.3 KiB
TypeScript

import type { RegisterServerOptions } from '@peertube/peertube-types'
import type { ChannelConfigurationOptions } from '../../../../shared/lib/types'
/**
* Sanitize data so that they can safely be used/stored for channel configuration configuration.
* Throw an error if the format is obviously wrong.
* Cleans data (removing empty values, ...)
* @param options Peertube server options
* @param _channelInfos Channel infos
* @param data Input data
*/
async function sanitizeChannelConfigurationOptions (
_options: RegisterServerOptions,
_channelId: number | string,
data: any
): Promise<ChannelConfigurationOptions> {
if (typeof data !== 'object') {
throw new Error('Invalid data type')
}
const botData = data.bot
if (typeof botData !== 'object') {
throw new Error('Invalid data.bot data type')
}
const slowModeData = data.slowMode ?? { defaultDelay: 0 } // not present in livechat <= 8.2.0
if (typeof slowModeData !== 'object') {
throw new Error('Invalid data.slowMode data type')
}
const result: ChannelConfigurationOptions = {
bot: {
enabled: _readBoolean(botData, 'enabled'),
nickname: _readSimpleInput(botData, 'nickname', true),
forbiddenWords: await _readForbiddenWords(botData),
quotes: _readQuotes(botData),
commands: _readCommands(botData)
// TODO: bannedJIDs
},
slowMode: {
defaultDelay: _readInteger(slowModeData, 'defaultDelay', 0, 1000)
}
}
return result
}
function _readBoolean (data: any, f: string): boolean {
if (!(f in data)) {
return false
}
if (typeof data[f] !== 'boolean') {
throw new Error('Invalid data type for field ' + f)
}
return data[f]
}
function _readInteger (data: any, f: string, min: number, max: number): number {
if (!(f in data)) {
throw new Error('Missing integer value for field ' + f)
}
const v = parseInt(data[f])
if (isNaN(v)) {
throw new Error('Invalid value type for field ' + f)
}
if (v < min) {
throw new Error('Invalid value type (<min) for field ' + f)
}
if (v > max) {
throw new Error('Invalid value type (>max) for field ' + f)
}
return v
}
function _readSimpleInput (data: any, f: string, strict?: boolean, noSpace?: boolean): string {
if (!(f in data)) {
return ''
}
if (typeof data[f] !== 'string') {
throw new Error('Invalid data type for field ' + f)
}
// Removing control characters.
// eslint-disable-next-line no-control-regex
let s = (data[f] as string).replace(/[\u0000-\u001F\u007F-\u009F]/g, '')
if (strict) {
// Replacing all invalid characters, no need to throw an error..
s = s.replace(/[^\p{L}\p{N}\p{Z}_-]$/gu, '')
}
if (noSpace) {
s = s.replace(/\s+/g, '')
}
return s
}
function _readStringArray (data: any, f: string): string[] {
if (!(f in data)) {
return []
}
if (!Array.isArray(data[f])) {
throw new Error('Invalid data type for field ' + f)
}
const result: string[] = []
for (const v of data[f]) {
if (typeof v !== 'string') {
throw new Error('Invalid data type in a value of field ' + f)
}
if (v === '' || /^\s+$/.test(v)) {
// ignore empty values
continue
}
result.push(v)
}
return result
}
function _readMultiLineString (data: any, f: string): string {
if (!(f in data)) {
return ''
}
if (typeof data[f] !== 'string') {
throw new Error('Invalid data type for field ' + f)
}
// Removing control characters (must authorize \u001A: line feed)
// eslint-disable-next-line no-control-regex
const s = (data[f] as string).replace(/[\u0000-\u0009\u001B-\u001F\u007F-\u009F]/g, '')
return s
}
async function _readRegExpArray (data: any, f: string): Promise<string[]> {
// Note: this function can instanciate a lot of RegExp.
// To avoid freezing the server, we make it async, and will validate each regexp in a separate tick.
if (!(f in data)) {
return []
}
if (!Array.isArray(data[f])) {
throw new Error('Invalid data type for field ' + f)
}
const result: string[] = []
for (const v of data[f]) {
if (typeof v !== 'string') {
throw new Error('Invalid data type in a value of field ' + f)
}
if (v === '' || /^\s+$/.test(v)) {
// ignore empty values
continue
}
// value must be a valid regexp
try {
async function _validate (): Promise<void> {
// eslint-disable-next-line no-new
new RegExp(v)
}
await _validate()
} catch (_err) {
throw new Error('Invalid value in field ' + f)
}
result.push(v)
}
return result
}
async function _readForbiddenWords (botData: any): Promise<ChannelConfigurationOptions['bot']['forbiddenWords']> {
if (!Array.isArray(botData.forbiddenWords)) {
throw new Error('Invalid forbiddenWords data')
}
const result: ChannelConfigurationOptions['bot']['forbiddenWords'] = []
for (const fw of botData.forbiddenWords) {
const regexp = !!fw.regexp
let entries
if (regexp) {
entries = await _readRegExpArray(fw, 'entries')
} else {
entries = _readStringArray(fw, 'entries')
}
const applyToModerators = _readBoolean(fw, 'applyToModerators')
const reason = fw.reason ? _readSimpleInput(fw, 'reason') : undefined
const comments = fw.comments ? _readMultiLineString(fw, 'comments') : undefined
result.push({
regexp,
entries,
applyToModerators,
reason,
comments
})
}
return result
}
function _readQuotes (botData: any): ChannelConfigurationOptions['bot']['quotes'] {
if (!Array.isArray(botData.quotes)) {
throw new Error('Invalid quotes data')
}
const result: ChannelConfigurationOptions['bot']['quotes'] = []
for (const qs of botData.quotes) {
const messages = _readStringArray(qs, 'messages')
const delay = _readInteger(qs, 'delay', 1, 6000)
result.push({
messages,
delay
})
}
return result
}
function _readCommands (botData: any): ChannelConfigurationOptions['bot']['commands'] {
if (!Array.isArray(botData.commands)) {
throw new Error('Invalid commands data')
}
const result: ChannelConfigurationOptions['bot']['commands'] = []
for (const cs of botData.commands) {
const message = _readSimpleInput(cs, 'message')
const command = _readSimpleInput(cs, 'command', false, true)
result.push({
message,
command
})
}
return result
}
export {
sanitizeChannelConfigurationOptions
}