Merge branch 'main' of https://github.com/JohnXLivingston/peertube-plugin-livechat
This commit is contained in:
@ -1,40 +0,0 @@
|
||||
{
|
||||
"root": true,
|
||||
"env": {
|
||||
"browser": false,
|
||||
"es6": true
|
||||
},
|
||||
"extends": [
|
||||
"standard-with-typescript"
|
||||
],
|
||||
"globals": {},
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2018,
|
||||
"project": [
|
||||
"./server/tsconfig.json"
|
||||
]
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"ignorePatterns": [],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-unused-vars": [2, {"argsIgnorePattern": "^_"}],
|
||||
"@typescript-eslint/no-floating-promises": "error",
|
||||
"@typescript-eslint/no-misused-promises": "error",
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
"@typescript-eslint/strict-boolean-expressions": "off",
|
||||
"@typescript-eslint/return-await": [2, "in-try-catch"], // FIXME: correct?
|
||||
"@typescript-eslint/no-invalid-void-type": "off",
|
||||
"@typescript-eslint/triple-slash-reference": "off",
|
||||
"max-len": [
|
||||
"error",
|
||||
{
|
||||
"code": 120,
|
||||
"comments": 120
|
||||
}
|
||||
],
|
||||
"no-unused-vars": "off"
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
@ -88,7 +88,7 @@ class BotsCtl {
|
||||
moderationBotProcess.stderr?.on('data', (data) => {
|
||||
// change error level for non-relevant errors:
|
||||
data = data.toString()
|
||||
if (/Warning.*NODE_TLS_REJECT_UNAUTHORIZED.*'0'.*TLS/.test(data)) {
|
||||
if (/Warning.*NODE_TLS_REJECT_UNAUTHORIZED.*'0'.*TLS/.test(data as string)) {
|
||||
this.logger.debug(`ModerationBot stderr: ${data as string}`)
|
||||
return
|
||||
}
|
||||
@ -123,9 +123,11 @@ class BotsCtl {
|
||||
}
|
||||
const p = new Promise<void>((resolve, reject) => {
|
||||
try {
|
||||
if (!this.moderationBotProcess) { resolve() }
|
||||
const moderationBotProcess: ReturnType<typeof child_process.spawn> =
|
||||
this.moderationBotProcess as ReturnType<typeof child_process.spawn>
|
||||
if (!this.moderationBotProcess) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
const moderationBotProcess: ReturnType<typeof child_process.spawn> = this.moderationBotProcess
|
||||
|
||||
let resolved = false
|
||||
// Trying to kill, and force kill if it takes more than X seconds
|
||||
|
@ -13,7 +13,7 @@ let singleton: BotConfiguration | undefined
|
||||
|
||||
type RoomConfCache =
|
||||
null // already loaded, but file does not exist
|
||||
| RoomConf // loaded, and contains the room conf
|
||||
| RoomConf // loaded, and contains the room conf
|
||||
|
||||
type ChannelCommonRoomConf = Omit<RoomConf, 'local' | 'domain'>
|
||||
|
||||
@ -189,7 +189,7 @@ class BotConfiguration {
|
||||
})).toString()
|
||||
|
||||
config = JSON.parse(content)
|
||||
} catch (err) {
|
||||
} catch (_err) {
|
||||
this.logger.info('Error reading the moderation bot global configuration file, assuming it does not exists.')
|
||||
config = undefined
|
||||
}
|
||||
@ -275,7 +275,7 @@ class BotConfiguration {
|
||||
content = (await fs.promises.readFile(filePath, {
|
||||
encoding: 'utf-8'
|
||||
})).toString()
|
||||
} catch (err) {
|
||||
} catch (_err) {
|
||||
this.logger.debug('Failed to read room conf file, assuming it does not exists')
|
||||
this.roomConfCache.set(roomJID, null)
|
||||
return null
|
||||
@ -284,7 +284,7 @@ class BotConfiguration {
|
||||
let json: RoomConf
|
||||
try {
|
||||
json = JSON.parse(content) as RoomConf
|
||||
} catch (err) {
|
||||
} catch (_err) {
|
||||
this.logger.error(`Error parsing JSON file ${filePath}, assuming empty`)
|
||||
this.roomConfCache.set(roomJID, null)
|
||||
return null
|
||||
|
@ -59,7 +59,7 @@ async function initChannelConfiguration (options: RegisterServerOptions): Promis
|
||||
|
||||
logger.info(`Channel ${channelId} deleted, removing 'custom emojis' related stuff.`)
|
||||
try {
|
||||
Emojis.singletonSafe()?.deleteChannelDefinition(channelId)
|
||||
await Emojis.singletonSafe()?.deleteChannelDefinition(channelId)
|
||||
} catch (err) {
|
||||
logger.error(err)
|
||||
}
|
||||
@ -87,7 +87,7 @@ async function initChannelConfiguration (options: RegisterServerOptions): Promis
|
||||
])
|
||||
|
||||
await fillVideoCustomFields(options, video)
|
||||
const hasChat = await videoHasWebchat({
|
||||
const hasChat = videoHasWebchat({
|
||||
'chat-per-live-video': !!settings['chat-per-live-video'],
|
||||
'chat-all-lives': !!settings['chat-all-lives'],
|
||||
'chat-all-non-lives': !!settings['chat-all-non-lives'],
|
||||
@ -121,7 +121,9 @@ async function initChannelConfiguration (options: RegisterServerOptions): Promis
|
||||
// Note: no need to await here, would only degrade performances.
|
||||
// FIXME: should also update livechat_muc_terms if channel has changed.
|
||||
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(
|
||||
() => {},
|
||||
(err) => logger.error(err)
|
||||
|
@ -4,7 +4,13 @@
|
||||
|
||||
import type { RegisterServerOptions } from '@peertube/peertube-types'
|
||||
import type { ChannelConfigurationOptions } from '../../../../shared/lib/types'
|
||||
import { channelTermsMaxLength } from '../../../../shared/lib/constants'
|
||||
import {
|
||||
channelTermsMaxLength,
|
||||
forbidSpecialCharsMaxTolerance,
|
||||
forbidSpecialCharsDefaultTolerance,
|
||||
noDuplicateDefaultDelay,
|
||||
noDuplicateMaxDelay
|
||||
} from '../../../../shared/lib/constants'
|
||||
|
||||
/**
|
||||
* Sanitize data so that they can safely be used/stored for channel configuration configuration.
|
||||
@ -17,39 +23,62 @@ import { channelTermsMaxLength } from '../../../../shared/lib/constants'
|
||||
async function sanitizeChannelConfigurationOptions (
|
||||
_options: RegisterServerOptions,
|
||||
_channelId: number | string,
|
||||
data: any
|
||||
data: unknown
|
||||
): Promise<ChannelConfigurationOptions> {
|
||||
if (typeof data !== 'object') {
|
||||
if (!_assertObjectType(data)) {
|
||||
throw new Error('Invalid data type')
|
||||
}
|
||||
|
||||
const botData = data.bot
|
||||
if (typeof botData !== 'object') {
|
||||
const botData = data.bot ?? {}
|
||||
if (!_assertObjectType(botData)) {
|
||||
throw new Error('Invalid data.bot data type')
|
||||
}
|
||||
|
||||
// slowMode not present in livechat <= 8.2.0:
|
||||
const slowModeData = data.slowMode ?? {}
|
||||
slowModeData.duration ??= slowModeData.defaultDuration ?? 0 // v8.3.0 to 8.3.2: was in defaultDuration
|
||||
|
||||
if (typeof slowModeData !== 'object') {
|
||||
if (!_assertObjectType(slowModeData)) {
|
||||
throw new Error('Invalid data.slowMode data type')
|
||||
}
|
||||
slowModeData.duration ??= slowModeData.defaultDuration ?? 0 // v8.3.0 to 8.3.2: was in defaultDuration
|
||||
|
||||
const moderationData = data.moderation ?? {} // comes with livechat 10.3.0
|
||||
if (!_assertObjectType(moderationData)) {
|
||||
throw new Error('Invalid data.moderation data type')
|
||||
}
|
||||
moderationData.delay ??= 0
|
||||
moderationData.anonymize ??= false // comes with livechat 11.0.0
|
||||
|
||||
// mute not present in livechat <= 10.2.0
|
||||
const mute = data.mute ?? {}
|
||||
if (!_assertObjectType(mute)) {
|
||||
throw new Error('Invalid data.mute data type')
|
||||
}
|
||||
mute.anonymous ??= false
|
||||
|
||||
if (typeof mute !== 'object') {
|
||||
throw new Error('Invalid data.mute data type')
|
||||
// forbidSpecialChars comes with livechat 12.0.0
|
||||
botData.forbidSpecialChars ??= {
|
||||
enabled: false,
|
||||
reason: '',
|
||||
tolerance: forbidSpecialCharsDefaultTolerance,
|
||||
applyToModerators: false
|
||||
}
|
||||
if (!_assertObjectType(botData.forbidSpecialChars)) {
|
||||
throw new Error('Invalid data.bot.forbidSpecialChars data type')
|
||||
}
|
||||
|
||||
// noDuplicate comes with livechat 12.0.0
|
||||
botData.noDuplicate ??= {
|
||||
enabled: false,
|
||||
reason: '',
|
||||
delay: noDuplicateDefaultDelay,
|
||||
applyToModerators: false
|
||||
}
|
||||
if (!_assertObjectType(botData.noDuplicate)) {
|
||||
throw new Error('Invalid data.bot.noDuplicate data type')
|
||||
}
|
||||
|
||||
// terms not present in livechat <= 10.2.0
|
||||
let terms = data.terms
|
||||
let terms = data.terms ?? undefined
|
||||
if (terms !== undefined && (typeof terms !== 'string')) {
|
||||
throw new Error('Invalid data.terms data type')
|
||||
}
|
||||
@ -63,6 +92,8 @@ async function sanitizeChannelConfigurationOptions (
|
||||
enabled: _readBoolean(botData, 'enabled'),
|
||||
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
|
||||
@ -85,7 +116,11 @@ async function sanitizeChannelConfigurationOptions (
|
||||
return result
|
||||
}
|
||||
|
||||
function _readBoolean (data: any, f: string): boolean {
|
||||
function _assertObjectType (data: unknown): data is Record<string, unknown> {
|
||||
return !!data && (typeof data === 'object') && Object.keys(data).every(k => typeof k === 'string')
|
||||
}
|
||||
|
||||
function _readBoolean (data: Record<string, unknown>, f: string): boolean {
|
||||
if (!(f in data)) {
|
||||
return false
|
||||
}
|
||||
@ -95,11 +130,11 @@ function _readBoolean (data: any, f: string): boolean {
|
||||
return data[f]
|
||||
}
|
||||
|
||||
function _readInteger (data: any, f: string, min: number, max: number): number {
|
||||
function _readInteger (data: Record<string, unknown>, 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])
|
||||
const v = typeof data[f] === 'number' ? Math.trunc(data[f]) : parseInt(data[f] as string)
|
||||
if (isNaN(v)) {
|
||||
throw new Error('Invalid value type for field ' + f)
|
||||
}
|
||||
@ -112,7 +147,7 @@ function _readInteger (data: any, f: string, min: number, max: number): number {
|
||||
return v
|
||||
}
|
||||
|
||||
function _readSimpleInput (data: any, f: string, strict?: boolean, noSpace?: boolean): string {
|
||||
function _readSimpleInput (data: Record<string, unknown>, f: string, strict?: boolean, noSpace?: boolean): string {
|
||||
if (!(f in data)) {
|
||||
return ''
|
||||
}
|
||||
@ -121,7 +156,7 @@ function _readSimpleInput (data: any, f: string, strict?: boolean, noSpace?: boo
|
||||
}
|
||||
// Removing control characters.
|
||||
// eslint-disable-next-line no-control-regex
|
||||
let s = (data[f] as string).replace(/[\u0000-\u001F\u007F-\u009F]/g, '')
|
||||
let s = data[f].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, '')
|
||||
@ -132,7 +167,7 @@ function _readSimpleInput (data: any, f: string, strict?: boolean, noSpace?: boo
|
||||
return s
|
||||
}
|
||||
|
||||
function _readStringArray (data: any, f: string): string[] {
|
||||
function _readStringArray (data: Record<string, unknown>, f: string): string[] {
|
||||
if (!(f in data)) {
|
||||
return []
|
||||
}
|
||||
@ -153,7 +188,7 @@ function _readStringArray (data: any, f: string): string[] {
|
||||
return result
|
||||
}
|
||||
|
||||
function _readMultiLineString (data: any, f: string): string {
|
||||
function _readMultiLineString (data: Record<string, unknown>, f: string): string {
|
||||
if (!(f in data)) {
|
||||
return ''
|
||||
}
|
||||
@ -162,11 +197,11 @@ function _readMultiLineString (data: any, f: string): string {
|
||||
}
|
||||
// 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, '')
|
||||
const s = data[f].replace(/[\u0000-\u0009\u001B-\u001F\u007F-\u009F]/g, '')
|
||||
return s
|
||||
}
|
||||
|
||||
async function _readRegExpArray (data: any, f: string): Promise<string[]> {
|
||||
async function _readRegExpArray (data: Record<string, unknown>, 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)) {
|
||||
@ -186,11 +221,11 @@ async function _readRegExpArray (data: any, f: string): Promise<string[]> {
|
||||
}
|
||||
// value must be a valid regexp
|
||||
try {
|
||||
async function _validate (): Promise<void> {
|
||||
async function _validate (v: string): Promise<void> {
|
||||
// eslint-disable-next-line no-new
|
||||
new RegExp(v)
|
||||
}
|
||||
await _validate()
|
||||
await _validate(v)
|
||||
} catch (_err) {
|
||||
throw new Error('Invalid value in field ' + f)
|
||||
}
|
||||
@ -199,12 +234,17 @@ async function _readRegExpArray (data: any, f: string): Promise<string[]> {
|
||||
return result
|
||||
}
|
||||
|
||||
async function _readForbiddenWords (botData: any): Promise<ChannelConfigurationOptions['bot']['forbiddenWords']> {
|
||||
async function _readForbiddenWords (
|
||||
botData: Record<string, unknown>
|
||||
): 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) {
|
||||
if (!_assertObjectType(fw)) {
|
||||
throw new Error('Invalid entry in botData.forbiddenWords')
|
||||
}
|
||||
const regexp = !!fw.regexp
|
||||
let entries
|
||||
if (regexp) {
|
||||
@ -229,12 +269,45 @@ async function _readForbiddenWords (botData: any): Promise<ChannelConfigurationO
|
||||
return result
|
||||
}
|
||||
|
||||
function _readQuotes (botData: any): ChannelConfigurationOptions['bot']['quotes'] {
|
||||
async function _readForbidSpecialChars (
|
||||
botData: Record<string, unknown>
|
||||
): Promise<ChannelConfigurationOptions['bot']['forbidSpecialChars']> {
|
||||
if (!_assertObjectType(botData.forbidSpecialChars)) {
|
||||
throw new Error('Invalid forbidSpecialChars data')
|
||||
}
|
||||
const result: ChannelConfigurationOptions['bot']['forbidSpecialChars'] = {
|
||||
enabled: _readBoolean(botData.forbidSpecialChars, 'enabled'),
|
||||
reason: _readSimpleInput(botData.forbidSpecialChars, 'reason'),
|
||||
tolerance: _readInteger(botData.forbidSpecialChars, 'tolerance', 0, forbidSpecialCharsMaxTolerance),
|
||||
applyToModerators: _readBoolean(botData.forbidSpecialChars, 'applyToModerators')
|
||||
}
|
||||
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, noDuplicateMaxDelay),
|
||||
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')
|
||||
}
|
||||
const result: ChannelConfigurationOptions['bot']['quotes'] = []
|
||||
for (const qs of botData.quotes) {
|
||||
if (!_assertObjectType(qs)) {
|
||||
throw new Error('Invalid entry in botData.quotes')
|
||||
}
|
||||
const messages = _readStringArray(qs, 'messages')
|
||||
const delay = _readInteger(qs, 'delay', 1, 6000)
|
||||
|
||||
@ -246,12 +319,15 @@ function _readQuotes (botData: any): ChannelConfigurationOptions['bot']['quotes'
|
||||
return result
|
||||
}
|
||||
|
||||
function _readCommands (botData: any): ChannelConfigurationOptions['bot']['commands'] {
|
||||
function _readCommands (botData: Record<string, unknown>): 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) {
|
||||
if (!_assertObjectType(cs)) {
|
||||
throw new Error('Invalid entry in botData.commands')
|
||||
}
|
||||
const message = _readSimpleInput(cs, 'message')
|
||||
const command = _readSimpleInput(cs, 'command', false, true)
|
||||
|
||||
|
@ -7,6 +7,10 @@ import type { ChannelConfigurationOptions } from '../../../../shared/lib/types'
|
||||
import type { ChannelCommonRoomConf } from '../../configuration/bot'
|
||||
import { RoomChannel } from '../../room-channel'
|
||||
import { sanitizeChannelConfigurationOptions } from '../../configuration/channel/sanitize'
|
||||
import {
|
||||
forbidSpecialCharsDefaultTolerance,
|
||||
noDuplicateDefaultDelay
|
||||
} from '../../../../shared/lib/constants'
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
|
||||
@ -44,6 +48,18 @@ function getDefaultChannelConfigurationOptions (_options: RegisterServerOptions)
|
||||
enabled: false,
|
||||
nickname: 'Sepia',
|
||||
forbiddenWords: [],
|
||||
forbidSpecialChars: {
|
||||
enabled: false,
|
||||
reason: '',
|
||||
tolerance: forbidSpecialCharsDefaultTolerance,
|
||||
applyToModerators: false
|
||||
},
|
||||
noDuplicate: {
|
||||
enabled: false,
|
||||
reason: '',
|
||||
delay: noDuplicateDefaultDelay,
|
||||
applyToModerators: false
|
||||
},
|
||||
quotes: [],
|
||||
commands: []
|
||||
},
|
||||
@ -113,6 +129,16 @@ function channelConfigurationOptionsToBotRoomConf (
|
||||
handlersIds.set(id, true)
|
||||
handlers.push(_getForbiddenWordsHandler(id, v))
|
||||
})
|
||||
if (channelConfigurationOptions.bot.forbidSpecialChars.enabled) {
|
||||
const id = 'forbid_special_chars'
|
||||
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)
|
||||
@ -129,7 +155,7 @@ function channelConfigurationOptionsToBotRoomConf (
|
||||
for (const handler of previousRoomConf.handlers) {
|
||||
if (!handlersIds.has(handler.id)) {
|
||||
// cloning to avoid issues...
|
||||
const disabledHandler = JSON.parse(JSON.stringify(handler))
|
||||
const disabledHandler = JSON.parse(JSON.stringify(handler)) as typeof handler
|
||||
disabledHandler.enabled = false
|
||||
handlers.push(disabledHandler)
|
||||
}
|
||||
@ -176,8 +202,8 @@ function _getForbiddenWordsHandler (
|
||||
} else {
|
||||
// Here we must add word-breaks and escape entries.
|
||||
// We join all entries in one Regexp (for the same reason as above).
|
||||
rule.regexp = '(?:' +
|
||||
forbiddenWords.entries.map(s => {
|
||||
rule.regexp = '(?:' + forbiddenWords.entries.map(
|
||||
s => {
|
||||
s = _stringToWordRegexp(s)
|
||||
// Must add the \b...
|
||||
// ... but... won't work if the first (or last) char is an emoji.
|
||||
@ -190,7 +216,8 @@ function _getForbiddenWordsHandler (
|
||||
}
|
||||
// FIXME: this solution wont work for non-latin charsets.
|
||||
return s
|
||||
}).join(')|(?:') + ')'
|
||||
}
|
||||
).join(')|(?:') + ')'
|
||||
}
|
||||
|
||||
if (forbiddenWords.reason) {
|
||||
@ -202,6 +229,63 @@ function _getForbiddenWordsHandler (
|
||||
return handler
|
||||
}
|
||||
|
||||
function _getForbidSpecialCharsHandler (
|
||||
id: string,
|
||||
forbidSpecialChars: ChannelConfigurationOptions['bot']['forbidSpecialChars']
|
||||
): ConfigHandler {
|
||||
const handler: ConfigHandler = {
|
||||
type: 'moderate',
|
||||
id,
|
||||
enabled: true,
|
||||
options: {
|
||||
rules: []
|
||||
}
|
||||
}
|
||||
|
||||
// The regexp to find one invalid character:
|
||||
// (Note: Emoji_Modifier and Emoji_Component should not be matched alones, but seems a reasonnable compromise to avoid
|
||||
// complex regex).
|
||||
let regexp = '[^' +
|
||||
'\\s\\p{Letter}\\p{Number}\\p{Punctuation}\\p{Currency_Symbol}\\p{Emoji}\\p{Emoji_Component}\\p{Emoji_Modifier}' +
|
||||
']'
|
||||
|
||||
if (forbidSpecialChars.tolerance > 0) {
|
||||
// we must repeat !
|
||||
const a = []
|
||||
for (let i = 0; i <= forbidSpecialChars.tolerance; i++) { // N+1 values
|
||||
a.push(regexp)
|
||||
}
|
||||
regexp = a.join('.*')
|
||||
}
|
||||
|
||||
const rule: any = {
|
||||
name: id,
|
||||
regexp,
|
||||
modifiers: 'us',
|
||||
reason: forbidSpecialChars.reason
|
||||
}
|
||||
handler.options.rules.push(rule)
|
||||
handler.options.applyToModerators = !!forbidSpecialChars.applyToModerators
|
||||
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]
|
||||
|
@ -100,7 +100,7 @@ async function getConverseJSParams (
|
||||
externalAuthOIDC ??= []
|
||||
externalAuthOIDC.push({
|
||||
type: oidc.type,
|
||||
buttonLabel: buttonLabel,
|
||||
buttonLabel,
|
||||
url: authUrl
|
||||
})
|
||||
}
|
||||
@ -127,7 +127,7 @@ async function getConverseJSParams (
|
||||
localWebsocketServiceUrl: localWsUri,
|
||||
remoteBoshServiceUrl: remoteConnectionInfos?.anonymous?.boshUri ?? null,
|
||||
remoteWebsocketServiceUrl: remoteConnectionInfos?.anonymous?.wsUri ?? null,
|
||||
authenticationUrl: authenticationUrl,
|
||||
authenticationUrl,
|
||||
autoViewerMode,
|
||||
theme: converseJSTheme,
|
||||
forceReadonly,
|
||||
@ -150,7 +150,7 @@ function _interfaceParams (
|
||||
transparent: InitConverseJSParams['transparent']
|
||||
converseJSTheme: InitConverseJSParams['theme']
|
||||
} {
|
||||
let autoViewerMode: boolean = false
|
||||
let autoViewerMode = false
|
||||
const forceReadonly: boolean | 'noscroll' = params.readonly ?? false
|
||||
if (!forceReadonly) {
|
||||
autoViewerMode = true // auto join the chat in viewer mode, if not logged in
|
||||
@ -234,14 +234,14 @@ async function _connectionInfos (
|
||||
params: GetConverseJSParamsParams,
|
||||
roomInfos: RoomInfos
|
||||
): Promise<{
|
||||
prosodyDomain: string
|
||||
localAnonymousJID: string
|
||||
localBoshUri: string
|
||||
localWsUri: string | null
|
||||
remoteConnectionInfos: WCRemoteConnectionInfos | undefined
|
||||
roomJID: string
|
||||
customEmojisUrl?: string
|
||||
} | InitConverseJSParamsError> {
|
||||
prosodyDomain: string
|
||||
localAnonymousJID: string
|
||||
localBoshUri: string
|
||||
localWsUri: string | null
|
||||
remoteConnectionInfos: WCRemoteConnectionInfos | undefined
|
||||
roomJID: string
|
||||
customEmojisUrl?: string
|
||||
} | InitConverseJSParamsError> {
|
||||
const { video, remoteChatInfos, channelId, roomKey } = roomInfos
|
||||
|
||||
const prosodyDomain = await getProsodyDomain(options)
|
||||
|
@ -45,7 +45,7 @@ async function initCustomFields (options: RegisterServerOptions): Promise<void>
|
||||
|
||||
const body: any = params.body
|
||||
const video: Video | undefined = params.video
|
||||
if (!video || !video.id) {
|
||||
if (!video?.id) {
|
||||
return
|
||||
}
|
||||
if (!body.pluginData) return
|
||||
@ -115,7 +115,7 @@ async function fillVideoRemoteLiveChat (
|
||||
const infos = await getVideoLiveChatInfos(options, video)
|
||||
if (!infos) { return }
|
||||
|
||||
let ok: boolean = false
|
||||
let ok = false
|
||||
// We must check if there is a compatible connection protocol...
|
||||
if (anonymousConnectionInfos(infos)) {
|
||||
// Connection ok using a remote anonymous account. That's enought.
|
||||
|
@ -61,7 +61,7 @@ interface ChannelInfos {
|
||||
async function getChannelInfosById (
|
||||
options: RegisterServerOptions,
|
||||
channelId: number,
|
||||
restrictToLocalChannels: boolean = false
|
||||
restrictToLocalChannels = false
|
||||
): Promise<ChannelInfos | null> {
|
||||
if (!channelId) {
|
||||
throw new Error('Missing channelId')
|
||||
|
@ -27,11 +27,11 @@ interface DebugContent {
|
||||
}
|
||||
|
||||
type DebugNumericValue = 'renewCertCheckInterval'
|
||||
| 'renewSelfSignedCertInterval'
|
||||
| 'logRotateEvery'
|
||||
| 'logRotateCheckInterval'
|
||||
| 'remoteServerInfosMaxAge'
|
||||
| 'externalAccountPruneInterval'
|
||||
| 'renewSelfSignedCertInterval'
|
||||
| 'logRotateEvery'
|
||||
| 'logRotateCheckInterval'
|
||||
| 'remoteServerInfosMaxAge'
|
||||
| 'externalAccountPruneInterval'
|
||||
|
||||
type DebugBooleanValue = 'alwaysPublishXMPPRoom' | 'enablePodcastChatTagForNonLive' | 'useOpenSSL'
|
||||
|
||||
@ -87,7 +87,7 @@ function _getProsodyDebuggerOptions (options: RegisterServerOptions, json: any):
|
||||
if (!json.debug_prosody.debugger_path) { return undefined }
|
||||
if (typeof json.debug_prosody.debugger_path !== 'string') { return undefined }
|
||||
|
||||
const mobdebugPath = json.debug_prosody.debugger_path
|
||||
const mobdebugPath = json.debug_prosody.debugger_path as string
|
||||
|
||||
if (!fs.statSync(mobdebugPath).isDirectory()) {
|
||||
options.peertubeHelpers.logger.error('There should be a debugger, but cant find it. Path should be: ', mobdebugPath)
|
||||
|
@ -48,7 +48,8 @@ export async function diagProsody (test: string, options: RegisterServerOptions)
|
||||
if (process.arch !== 'x64' && process.arch !== 'x86_64' && process.arch !== 'arm64') {
|
||||
result.messages.push({
|
||||
level: 'error',
|
||||
message: 'Error: your CPU is a ' +
|
||||
message:
|
||||
'Error: your CPU is a ' +
|
||||
process.arch + ', ' +
|
||||
'which is not compatible with the plugin. ' +
|
||||
'Please read the plugin installation documentation for a workaround.'
|
||||
@ -235,7 +236,7 @@ export async function diagProsody (test: string, options: RegisterServerOptions)
|
||||
title: 'Prosody error log (last 50 lines)',
|
||||
message: logLines.join('\n')
|
||||
})
|
||||
} catch (error) {
|
||||
} catch (_err) {
|
||||
// Error should be because file does not exists. This is not an error case, just ignoring.
|
||||
}
|
||||
|
||||
|
@ -3,8 +3,8 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
type NextValue = 'backend' | 'debug' | 'webchat-video' | 'prosody'
|
||||
| 'external-auth-custom-oidc' | 'external-auth-google-oidc' | 'external-auth-facebook-oidc'
|
||||
| 'everything-ok'
|
||||
| 'external-auth-custom-oidc' | 'external-auth-google-oidc' | 'external-auth-facebook-oidc'
|
||||
| 'everything-ok'
|
||||
|
||||
interface MessageWithLevel {
|
||||
level: 'info' | 'warning' | 'error'
|
||||
@ -28,7 +28,7 @@ export interface TestResult {
|
||||
|
||||
export function newResult (test: string): TestResult {
|
||||
return {
|
||||
test: test,
|
||||
test,
|
||||
ok: false,
|
||||
messages: [],
|
||||
debug: [],
|
||||
|
@ -26,7 +26,7 @@ export async function diagVideo (test: string, { settingsManager }: RegisterServ
|
||||
result.messages.push('Displaying «open in new window» button')
|
||||
}
|
||||
|
||||
let atLeastOne: boolean = false
|
||||
let atLeastOne = false
|
||||
if (videoSettings['chat-per-live-video']) {
|
||||
result.messages.push('Chat can be enabled on live videos.')
|
||||
atLeastOne = true
|
||||
|
@ -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',
|
||||
@ -76,12 +78,13 @@ export class Emojis {
|
||||
if (!await this.channelHasCustomEmojis(channelId)) {
|
||||
return undefined
|
||||
}
|
||||
const route = getBaseRouterRoute(this.options) +
|
||||
'emojis/channel/' +
|
||||
encodeURIComponent(channelId) +
|
||||
'/definition'
|
||||
return canonicalizePluginUri(
|
||||
this.options,
|
||||
getBaseRouterRoute(this.options) +
|
||||
'emojis/channel/' +
|
||||
encodeURIComponent(channelId) +
|
||||
'/definition',
|
||||
route,
|
||||
{
|
||||
removePluginVersion: true
|
||||
}
|
||||
@ -112,7 +115,7 @@ export class Emojis {
|
||||
// File does not exist, this is normal.
|
||||
return undefined
|
||||
}
|
||||
throw err
|
||||
throw err as Error
|
||||
}
|
||||
return JSON.parse(content.toString())
|
||||
}
|
||||
@ -138,9 +141,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 getChannelCustomEmojisRegexp.
|
||||
if ((typeof sn !== 'string') || !/^:?[\w-]+:?$/.test(sn)) {
|
||||
this.logger.debug('Short name invalid: ' + (typeof sn === 'string' ? sn : '???'))
|
||||
return false
|
||||
@ -312,7 +317,7 @@ export class Emojis {
|
||||
}
|
||||
|
||||
const result: ChannelEmojis = {
|
||||
customEmojis: customEmojis
|
||||
customEmojis
|
||||
}
|
||||
return [result, buffersInfos]
|
||||
}
|
||||
@ -383,13 +388,42 @@ export class Emojis {
|
||||
})
|
||||
} catch (err: any) {
|
||||
if (!(('code' in err) && err.code === 'ENOENT')) {
|
||||
this.logger.error(err)
|
||||
this.logger.error(err as string)
|
||||
}
|
||||
} finally {
|
||||
this.channelCache.delete(channelId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representing a regular expression validating channel custom emojis.
|
||||
* This is used for the emoji only mode (test are made on the Prosody server).
|
||||
*
|
||||
* @param channelId channel id
|
||||
*/
|
||||
public async getChannelCustomEmojisRegexp (channelId: number): Promise<string | undefined> {
|
||||
const parts = []
|
||||
|
||||
if (await this.channelHasCustomEmojis(channelId)) {
|
||||
const def = await this.channelCustomEmojisDefinition(channelId)
|
||||
if (def) {
|
||||
parts.push(...def.customEmojis.map(d => d.sn))
|
||||
}
|
||||
}
|
||||
|
||||
if (parts.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// 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('|')
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 +450,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 +469,51 @@ 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: Record<string, any> = JSON.parse(
|
||||
(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(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
...Object.values(block)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
.map((d: any) => d.cp ? _emojiCpToRegexp(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 the corresponding Regexp class.
|
||||
* See ConverseJS emoji/utils.js for more info.
|
||||
* @param {string} unicode
|
||||
*/
|
||||
function _emojiCpToRegexp (unicode: string): string {
|
||||
if (unicode.includes('-')) {
|
||||
const parts = []
|
||||
const s = unicode.split('-')
|
||||
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
parts.push('\\x{' + s[i] + '}')
|
||||
}
|
||||
return parts.join('')
|
||||
}
|
||||
return '\\x{' + unicode + '}'
|
||||
}
|
||||
|
@ -238,7 +238,7 @@ class ExternalAuthOIDC {
|
||||
this.logger.debug('OIDC Discovery url is valid: ' + uri.toString())
|
||||
|
||||
this.providerHostName = uri.hostname
|
||||
} catch (err) {
|
||||
} catch (_err) {
|
||||
errors.push('Invalid discovery url')
|
||||
}
|
||||
}
|
||||
@ -349,11 +349,17 @@ class ExternalAuthOIDC {
|
||||
if (!encryptedCodeVerifier) {
|
||||
throw new Error('Received callback but code verifier not found in request cookies.')
|
||||
}
|
||||
if (typeof encryptedCodeVerifier !== 'string') {
|
||||
throw new Error('Invalid code-verifier type.')
|
||||
}
|
||||
|
||||
const encryptedState = req.cookies[this.cookieNamePrefix + 'state']
|
||||
if (!encryptedState) {
|
||||
throw new Error('Received callback but state not found in request cookies.')
|
||||
}
|
||||
if (typeof encryptedState !== 'string') {
|
||||
throw new Error('Invalid state data type')
|
||||
}
|
||||
|
||||
const codeVerifier = await this.decrypt(encryptedCodeVerifier)
|
||||
const state = await this.decrypt(encryptedState)
|
||||
@ -447,11 +453,11 @@ class ExternalAuthOIDC {
|
||||
|
||||
const encryptedArray = data.split(':')
|
||||
const iv = Buffer.from(encryptedArray[0], outputEncoding)
|
||||
const encrypted = Buffer.from(encryptedArray[1], outputEncoding)
|
||||
const encrypted = encryptedArray[1]
|
||||
const decipher = createDecipheriv(algorithm, this.secretKey, iv)
|
||||
|
||||
// FIXME: dismiss the "as any" below (dont understand why Typescript is not happy without)
|
||||
return decipher.update(encrypted as any, outputEncoding, inputEncoding) + decipher.final(inputEncoding)
|
||||
// here we must revert outputEncoding and inputEncoding, as were are decrypting.
|
||||
return decipher.update(encrypted, outputEncoding, inputEncoding) + decipher.final(inputEncoding)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -491,8 +497,11 @@ class ExternalAuthOIDC {
|
||||
if (typeof o.nickname !== 'string' || o.nickname === '') {
|
||||
throw new Error('No nickname')
|
||||
}
|
||||
if (typeof o.expire !== 'string' || o.expire === '') {
|
||||
throw new Error('Invalid expire data type')
|
||||
}
|
||||
|
||||
const expire = new Date(Date.parse(o.expire))
|
||||
const expire = new Date(Date.parse(o.expire as string))
|
||||
if (!(expire instanceof Date) || isNaN(expire.getTime())) {
|
||||
throw new Error('Invalid expire date')
|
||||
}
|
||||
@ -548,7 +557,7 @@ class ExternalAuthOIDC {
|
||||
if (!(field in userInfos)) { continue }
|
||||
if (typeof userInfos[field] !== 'string') { continue }
|
||||
if (userInfos[field] === '') { continue }
|
||||
return userInfos[field] as string
|
||||
return userInfos[field]
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
@ -571,7 +580,7 @@ class ExternalAuthOIDC {
|
||||
responseType: 'buffer'
|
||||
}).buffer()
|
||||
|
||||
const mimeType = await getMimeTypeFromArrayBuffer(buf)
|
||||
const mimeType = getMimeTypeFromArrayBuffer(buf as ArrayBuffer)
|
||||
if (!mimeType) {
|
||||
throw new Error('Failed to get the avatar file type')
|
||||
}
|
||||
@ -603,7 +612,7 @@ class ExternalAuthOIDC {
|
||||
const filePath = path.resolve(this.avatarsDir, file)
|
||||
const buf = await fs.promises.readFile(filePath)
|
||||
|
||||
const mimeType = await getMimeTypeFromArrayBuffer(buf)
|
||||
const mimeType = getMimeTypeFromArrayBuffer(buf)
|
||||
if (!mimeType) {
|
||||
throw new Error('Failed to get the default avatar file type')
|
||||
}
|
||||
@ -748,6 +757,7 @@ class ExternalAuthOIDC {
|
||||
* @throws Error
|
||||
* @returns the singleton
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
|
||||
public static singleton (singletonType: ExternalAuthOIDCType | string): ExternalAuthOIDC {
|
||||
if (!singletons) {
|
||||
throw new Error('ExternalAuthOIDC singletons are not initialized yet')
|
||||
@ -778,7 +788,7 @@ class ExternalAuthOIDC {
|
||||
const m = token.match(/^(\w+)-/)
|
||||
if (!m) { return null }
|
||||
return ExternalAuthOIDC.singleton(m[1])
|
||||
} catch (err) {
|
||||
} catch (_err) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ export async function initFederation (options: RegisterServerOptions): Promise<v
|
||||
|
||||
registerHook({
|
||||
target: 'filter:activity-pub.activity.context.build.result',
|
||||
handler: async (jsonld: any) => {
|
||||
handler: async (jsonld: any[]) => {
|
||||
return videoContextBuildJSONLD(options, jsonld)
|
||||
}
|
||||
})
|
||||
|
@ -56,7 +56,7 @@ async function videoBuildJSONLD (
|
||||
}
|
||||
|
||||
await fillVideoCustomFields(options, video)
|
||||
const hasChat = await videoHasWebchat({
|
||||
const hasChat = videoHasWebchat({
|
||||
'chat-per-live-video': !!settings['chat-per-live-video'],
|
||||
'chat-all-lives': !!settings['chat-all-lives'],
|
||||
'chat-all-non-lives': !!settings['chat-all-non-lives'],
|
||||
|
@ -169,7 +169,7 @@ function sanitizePeertubeLiveChatServerInfos (
|
||||
*/
|
||||
function sanitizeCustomEmojisUrl (
|
||||
options: RegisterServerOptions,
|
||||
customEmojisUrl: any,
|
||||
customEmojisUrl: unknown,
|
||||
referenceUrl?: string
|
||||
): string | undefined {
|
||||
let checkHost: undefined | string
|
||||
@ -206,8 +206,8 @@ interface URLConstraints {
|
||||
domain?: string
|
||||
}
|
||||
|
||||
function _validUrl (s: string, constraints: URLConstraints): boolean {
|
||||
if ((typeof s) !== 'string') { return false }
|
||||
function _validUrl (s: unknown, constraints: URLConstraints): s is string {
|
||||
if (typeof s !== 'string') { return false }
|
||||
if (s === '') { return false }
|
||||
let url: URL
|
||||
try {
|
||||
@ -265,13 +265,13 @@ function _validateHost (s: any, mustBeSubDomainOf?: string): false | string {
|
||||
}
|
||||
}
|
||||
|
||||
function _readReferenceUrl (s: any): undefined | string {
|
||||
function _readReferenceUrl (s: unknown): undefined | string {
|
||||
try {
|
||||
if (typeof s !== 'string') { return undefined }
|
||||
if (!s.startsWith('https://') && !s.startsWith('http://')) {
|
||||
s = 'http://' + s
|
||||
}
|
||||
const url = new URL(s)
|
||||
const url = new URL(s as string)
|
||||
const host = url.hostname
|
||||
// Just to avoid some basic spoofing, we must check that host is not simply something like "com".
|
||||
// We will check if there is at least one dot. This test is not perfect, but can limit spoofing cases.
|
||||
@ -283,16 +283,16 @@ function _readReferenceUrl (s: any): undefined | string {
|
||||
}
|
||||
|
||||
function _sanitizePeertubeLiveChatInfosV0 (
|
||||
options: RegisterServerOptions, chatInfos: any, referenceUrl?: string
|
||||
options: RegisterServerOptions, chatInfos: unknown, referenceUrl?: string
|
||||
): LiveChatJSONLDAttributeV1 {
|
||||
const logger = options.peertubeHelpers.logger
|
||||
logger.debug('We are have to migrate data from the old JSONLD format')
|
||||
|
||||
if (chatInfos === false) { return false }
|
||||
if (typeof chatInfos !== 'object') { return false }
|
||||
if (!_assertObjectType(chatInfos)) { return false }
|
||||
|
||||
if (chatInfos.type !== 'xmpp') { return false }
|
||||
if ((typeof chatInfos.jid) !== 'string') { return false }
|
||||
if (typeof chatInfos.jid !== 'string') { return false }
|
||||
|
||||
// no link? invalid! dropping all.
|
||||
if (!Array.isArray(chatInfos.links)) { return false }
|
||||
@ -327,10 +327,10 @@ function _sanitizePeertubeLiveChatInfosV0 (
|
||||
}
|
||||
|
||||
for (const link of chatInfos.links) {
|
||||
if ((typeof link) !== 'object') { continue }
|
||||
if (!_assertObjectType(link) || (typeof link.type !== 'string')) { continue }
|
||||
if (['xmpp-bosh-anonymous', 'xmpp-websocket-anonymous'].includes(link.type)) {
|
||||
if ((typeof link.jid) !== 'string') { continue }
|
||||
if ((typeof link.url) !== 'string') { continue }
|
||||
if (typeof link.jid !== 'string') { continue }
|
||||
if (typeof link.url !== 'string') { continue }
|
||||
|
||||
if (
|
||||
!_validUrl(link.url, {
|
||||
@ -372,6 +372,10 @@ function sanitizeXMPPHostFromInstanceUrl (_options: RegisterServerOptions, s: an
|
||||
}
|
||||
}
|
||||
|
||||
function _assertObjectType (data: unknown): data is Record<string, unknown> {
|
||||
return !!data && (typeof data === 'object') && Object.keys(data).every(k => typeof k === 'string')
|
||||
}
|
||||
|
||||
export {
|
||||
sanitizePeertubeLiveChatInfos,
|
||||
sanitizePeertubeLiveChatServerInfos,
|
||||
|
@ -307,6 +307,7 @@ async function _store (options: RegisterServerOptions, filePath: string, content
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
|
||||
async function _get (options: RegisterServerOptions, filePath: string): Promise<any | null> {
|
||||
const logger = options.peertubeHelpers.logger
|
||||
try {
|
||||
|
@ -75,7 +75,7 @@ export async function listModFirewallFiles (
|
||||
})
|
||||
|
||||
return files.map(f => path.join(dir, f.name)).sort()
|
||||
} catch (err) {
|
||||
} catch (_err) {
|
||||
// should be that the directory does not exists
|
||||
return []
|
||||
}
|
||||
@ -148,7 +148,7 @@ export async function sanitizeModFirewallConfig (
|
||||
throw new Error('Invalid data in data.files (content)')
|
||||
}
|
||||
|
||||
if (entry.name.length > maxFirewallNameLength || !firewallNameRegexp.test(entry.name)) {
|
||||
if (entry.name.length > maxFirewallNameLength || !firewallNameRegexp.test(entry.name as string)) {
|
||||
throw new Error('Invalid name in data.files')
|
||||
}
|
||||
if (entry.content.length > maxFirewallFileSize) {
|
||||
|
@ -6,22 +6,57 @@ import { eachSeries } from 'async'
|
||||
import type { NextFunction, Request, RequestHandler, Response } from 'express'
|
||||
|
||||
// Syntactic sugar to avoid try/catch in express controllers
|
||||
// Thanks: https://medium.com/@Abazhenov/using-async-await-in-express-with-node-8-b8af872c0016
|
||||
|
||||
export type RequestPromiseHandler = ((req: Request, res: Response, next: NextFunction) => Promise<any>)
|
||||
|
||||
function asyncMiddleware (fun: RequestPromiseHandler | RequestPromiseHandler[]): RequestHandler {
|
||||
// type asyncMiddleWareFunction = (req: Request, res: Response, next: NextFunction) => Promise<void>
|
||||
// function asyncMiddleware (fun: RequestPromiseHandler | RequestPromiseHandler[]): asyncMiddleWareFunction {
|
||||
// return async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||
// if (!Array.isArray(fun)) {
|
||||
// // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression
|
||||
// Promise.resolve((fun as RequestHandler)(req, res, next))
|
||||
// .catch(err => { next(err) })
|
||||
// return
|
||||
// }
|
||||
|
||||
// try {
|
||||
// for (const f of fun) {
|
||||
// await new Promise<void>((resolve, reject) => {
|
||||
// // eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
// asyncMiddleware(f)(req, res, err => {
|
||||
// if (err) {
|
||||
// reject(err)
|
||||
// return
|
||||
// }
|
||||
// resolve()
|
||||
// })
|
||||
// })
|
||||
// }
|
||||
|
||||
// next()
|
||||
// } catch (err) {
|
||||
// next(err)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
function asyncMiddleware (fun: RequestPromiseHandler | RequestPromiseHandler[]) {
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
if (Array.isArray(fun)) {
|
||||
eachSeries(fun as RequestHandler[], (f, cb) => {
|
||||
Promise.resolve(f(req, res, (err: any) => cb(err)))
|
||||
.catch(err => next(err))
|
||||
// eslint-disable-next-line @typescript-eslint/no-confusing-void-expression
|
||||
Promise.resolve(f(req, res, (err: any) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
cb(err)
|
||||
}))
|
||||
.catch(err => { next(err) })
|
||||
}, next)
|
||||
return
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-confusing-void-expression
|
||||
Promise.resolve((fun as RequestHandler)(req, res, next))
|
||||
.catch(err => next(err))
|
||||
.catch(err => { next(err) })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -65,6 +65,8 @@ async function updateProsodyRoom (
|
||||
name?: string
|
||||
slow_mode_duration?: number
|
||||
moderation_delay?: number
|
||||
livechat_emoji_only?: boolean
|
||||
livechat_custom_emoji_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_custom_emoji_regexp' in data) {
|
||||
apiData.livechat_custom_emoji_regexp = data.livechat_custom_emoji_regexp ?? ''
|
||||
}
|
||||
if (('addAffiliations' in data) && data.addAffiliations !== undefined) {
|
||||
apiData.addAffiliations = data.addAffiliations
|
||||
}
|
||||
@ -107,7 +115,7 @@ async function updateProsodyRoom (
|
||||
apiData.removeAffiliationsFor = data.removeAffiliationsFor
|
||||
}
|
||||
try {
|
||||
logger.debug('Calling update room API on url: ' + apiUrl + ', with data: ' + JSON.stringify(apiData))
|
||||
logger.debug('Calling update room API on url: ' + apiUrl)
|
||||
const result = await got(apiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
@ -65,8 +65,8 @@ export class LivechatProsodyAuth {
|
||||
private readonly _prosodyDomain: string
|
||||
private _userTokensEnabled: boolean
|
||||
private readonly _tokensPath: string
|
||||
private readonly _passwords: Map<string, Password> = new Map()
|
||||
private readonly _tokensInfoByJID: Map<string, LivechatTokenInfos | undefined> = new Map()
|
||||
private readonly _passwords = new Map<string, Password>()
|
||||
private readonly _tokensInfoByJID = new Map<string, LivechatTokenInfos | undefined>()
|
||||
private readonly _secretKey: string
|
||||
protected readonly _logger: {
|
||||
debug: (s: string) => void
|
||||
@ -122,8 +122,8 @@ export class LivechatProsodyAuth {
|
||||
const nickname: string | undefined = await getUserNickname(this._options, user)
|
||||
return {
|
||||
jid: normalizedUsername + '@' + this._prosodyDomain,
|
||||
password: password,
|
||||
nickname: nickname,
|
||||
password,
|
||||
nickname,
|
||||
type: 'peertube'
|
||||
}
|
||||
}
|
||||
@ -136,7 +136,7 @@ export class LivechatProsodyAuth {
|
||||
if (this._userTokensEnabled) {
|
||||
try {
|
||||
const tokensInfo = await this._getTokensInfoForJID(normalizedUsername + '@' + this._prosodyDomain)
|
||||
if (!tokensInfo || !tokensInfo.tokens.length) {
|
||||
if (!tokensInfo?.tokens.length) {
|
||||
return false
|
||||
}
|
||||
// Checking that the user is valid:
|
||||
@ -159,7 +159,7 @@ export class LivechatProsodyAuth {
|
||||
if (this._userTokensEnabled) {
|
||||
try {
|
||||
const tokensInfo = await this._getTokensInfoForJID(normalizedUsername + '@' + this._prosodyDomain)
|
||||
if (!tokensInfo || !tokensInfo.tokens.length) {
|
||||
if (!tokensInfo?.tokens.length) {
|
||||
return false
|
||||
}
|
||||
// Checking that the user is valid:
|
||||
@ -247,7 +247,7 @@ export class LivechatProsodyAuth {
|
||||
}
|
||||
const nickname: string | undefined = await getUserNickname(this._options, user)
|
||||
const jid = normalizedUsername + '@' + this._prosodyDomain
|
||||
const token = await this._createToken(user.id, jid, label)
|
||||
const token = await this._createToken(user.id as number, jid, label)
|
||||
|
||||
token.nickname = nickname
|
||||
return token
|
||||
@ -279,7 +279,7 @@ export class LivechatProsodyAuth {
|
||||
return false
|
||||
}
|
||||
|
||||
await this._saveTokens(user.id, jid, tokensInfo.tokens.filter(t => t.id !== id))
|
||||
await this._saveTokens(user.id as number, jid, tokensInfo.tokens.filter(t => t.id !== id))
|
||||
return true
|
||||
}
|
||||
|
||||
@ -293,8 +293,8 @@ export class LivechatProsodyAuth {
|
||||
|
||||
const password = generatePassword(20)
|
||||
this._passwords.set(normalizedUsername, {
|
||||
password: password,
|
||||
validity: validity
|
||||
password,
|
||||
validity
|
||||
})
|
||||
return password
|
||||
}
|
||||
@ -330,7 +330,7 @@ export class LivechatProsodyAuth {
|
||||
const user = await this._options.peertubeHelpers.user.loadById(userId)
|
||||
if (!user || user.blocked) { return false }
|
||||
return true
|
||||
} catch (err) {
|
||||
} catch (_err) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@ -381,7 +381,7 @@ export class LivechatProsodyAuth {
|
||||
this._tokensInfoByJID.set(jid, undefined)
|
||||
return undefined
|
||||
}
|
||||
throw err
|
||||
throw err as Error
|
||||
}
|
||||
}
|
||||
|
||||
@ -452,11 +452,15 @@ export class LivechatProsodyAuth {
|
||||
|
||||
const encryptedArray = data.split(':')
|
||||
const iv = Buffer.from(encryptedArray[0], outputEncoding)
|
||||
const encrypted = Buffer.from(encryptedArray[1], outputEncoding)
|
||||
const encrypted = encryptedArray[1]
|
||||
const decipher = createDecipheriv(algorithm, this._secretKey, iv)
|
||||
|
||||
// FIXME: dismiss the "as any" below (dont understand why Typescript is not happy without)
|
||||
return decipher.update(encrypted as any, outputEncoding, inputEncoding) + decipher.final(inputEncoding)
|
||||
return decipher.update(
|
||||
encrypted,
|
||||
// here we must revert outputEncoding and inputEncoding, as were are decrypting.
|
||||
outputEncoding,
|
||||
inputEncoding
|
||||
) + decipher.final(inputEncoding)
|
||||
}
|
||||
|
||||
public static singleton (): LivechatProsodyAuth {
|
||||
|
@ -39,7 +39,7 @@ function startProsodyCertificatesRenewCheck (options: RegisterServerOptions, con
|
||||
}, checkInterval)
|
||||
|
||||
renew = {
|
||||
timer: timer
|
||||
timer
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,7 @@ import { BotConfiguration } from '../configuration/bot'
|
||||
import { debugMucAdmins } from '../debug'
|
||||
import { ExternalAuthOIDC } from '../external-auth/oidc'
|
||||
import { listModFirewallFiles } from '../firewall/config'
|
||||
import { Emojis } from '../emojis'
|
||||
|
||||
async function getWorkingDir (options: RegisterServerOptions): Promise<string> {
|
||||
const peertubeHelpers = options.peertubeHelpers
|
||||
@ -122,7 +123,7 @@ async function getProsodyFilePaths (options: RegisterServerOptions): Promise<Pro
|
||||
}
|
||||
|
||||
return {
|
||||
dir: dir,
|
||||
dir,
|
||||
pid: path.resolve(dir, 'prosody.pid'),
|
||||
error: path.resolve(dir, 'prosody.err'),
|
||||
log: path.resolve(dir, 'prosody.log'),
|
||||
@ -216,7 +217,7 @@ async function getProsodyConfig (options: RegisterServerOptionsV5): Promise<Pros
|
||||
? settings['chat-terms']
|
||||
: undefined
|
||||
|
||||
let useExternal: boolean = false
|
||||
let useExternal = false
|
||||
try {
|
||||
const oidcs = ExternalAuthOIDC.allSingletons()
|
||||
for (const oidc of oidcs) {
|
||||
@ -376,7 +377,7 @@ async function getProsodyConfig (options: RegisterServerOptionsV5): Promise<Pros
|
||||
config.useBotsVirtualHost(paths.botAvatars, paths.botAvatarsFiles)
|
||||
bots.moderation = await BotConfiguration.singleton().getModerationBotGlobalConf()
|
||||
if (bots.moderation?.connection?.password && typeof bots.moderation.connection.password === 'string') {
|
||||
valuesToHideInDiagnostic.set('BotPassword', bots.moderation.connection.password)
|
||||
valuesToHideInDiagnostic.set('BotPassword', bots.moderation.connection.password as string)
|
||||
}
|
||||
}
|
||||
|
||||
@ -389,6 +390,13 @@ async function getProsodyConfig (options: RegisterServerOptionsV5): Promise<Pros
|
||||
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)
|
||||
|
||||
const debugMucAdminJids = debugMucAdmins(options)
|
||||
@ -500,6 +508,11 @@ function getProsodyConfigContentForDiagnostic (config: ProsodyConfig, content?:
|
||||
// replaceAll not available, using trick:
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,7 @@ import { getProsodyDomain } from './domain'
|
||||
import { getUserNameByChannelId } from '../../database/channel'
|
||||
import { BotConfiguration } from '../../configuration/bot'
|
||||
|
||||
interface Affiliations { [jid: string]: 'outcast' | 'none' | 'member' | 'admin' | 'owner' }
|
||||
type Affiliations = Record<string, 'outcast' | 'none' | 'member' | 'admin' | 'owner'>
|
||||
|
||||
async function _getCommonAffiliations (options: RegisterServerOptions, _prosodyDomain: string): Promise<Affiliations> {
|
||||
const r: Affiliations = {}
|
||||
|
@ -97,7 +97,7 @@ abstract class ProsodyConfigBlock {
|
||||
if (!this.entries.has(name)) {
|
||||
this.entries.set(name, [])
|
||||
}
|
||||
let entry = this.entries.get(name) as ConfigEntryValue
|
||||
let entry = this.entries.get(name) ?? []
|
||||
if (!Array.isArray(entry)) {
|
||||
entry = [entry]
|
||||
}
|
||||
@ -112,7 +112,7 @@ abstract class ProsodyConfigBlock {
|
||||
if (!this.entries.has(name)) {
|
||||
return
|
||||
}
|
||||
let entry = this.entries.get(name) as ConfigEntryValue
|
||||
let entry = this.entries.get(name) ?? []
|
||||
if (!Array.isArray(entry)) {
|
||||
entry = [entry]
|
||||
}
|
||||
@ -246,6 +246,7 @@ class ProsodyConfigContent {
|
||||
|
||||
this.muc.add('modules_enabled', 'pubsub_peertubelivechat')
|
||||
this.muc.add('modules_enabled', 'muc_peertubelivechat_roles')
|
||||
this.muc.add('modules_enabled', 'muc_peertubelivechat_announcements')
|
||||
|
||||
this.muc.add('modules_enabled', 'muc_peertubelivechat_terms')
|
||||
this.muc.set('muc_terms_service_nickname', 'Peertube')
|
||||
@ -562,6 +563,18 @@ class ProsodyConfigContent {
|
||||
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 {
|
||||
for (const jid of jids) {
|
||||
this.muc.add('admins', jid)
|
||||
@ -587,6 +600,17 @@ class ProsodyConfigContent {
|
||||
let content = ''
|
||||
content += this.global.write()
|
||||
content += this.log + '\n'
|
||||
|
||||
// Add some performance tweaks for Prosody 0.12.4+lua5.4.
|
||||
// See https://github.com/JohnXLivingston/livechat-perf-test/tree/main/tests/33-prosody-gc
|
||||
content += `
|
||||
gc = {
|
||||
mode = "generational";
|
||||
minor_threshold = 5;
|
||||
major_threshold = 50;
|
||||
};
|
||||
`
|
||||
|
||||
content += '\n\n'
|
||||
if (this.authenticated) {
|
||||
content += this.authenticated.write()
|
||||
|
@ -139,9 +139,9 @@ async function prosodyCtl (
|
||||
reject(new Error('Missing prosodyctl command executable'))
|
||||
return
|
||||
}
|
||||
let d: string = ''
|
||||
let e: string = ''
|
||||
let m: string = ''
|
||||
let d = ''
|
||||
let e = ''
|
||||
let m = ''
|
||||
const cmdArgs = [
|
||||
...filePaths.execCtlArgs,
|
||||
'--config',
|
||||
@ -196,7 +196,7 @@ async function prosodyCtl (
|
||||
// (else it can cause trouble by cleaning AppImage extract too soon)
|
||||
spawned.on('close', (code) => {
|
||||
resolve({
|
||||
code: code,
|
||||
code,
|
||||
stdout: d,
|
||||
sterr: e,
|
||||
message: m
|
||||
@ -399,8 +399,8 @@ async function ensureProsodyRunning (
|
||||
})
|
||||
}
|
||||
logger.info('Waiting for the prosody process to launch')
|
||||
let count: number = 0
|
||||
let processStarted: boolean = false
|
||||
let count = 0
|
||||
let processStarted = false
|
||||
while (!processStarted && count < 5) {
|
||||
count++
|
||||
await sleep(500)
|
||||
@ -418,8 +418,8 @@ async function ensureProsodyRunning (
|
||||
return
|
||||
}
|
||||
logger.info('Prosody is running')
|
||||
await startProsodyLogRotate(options, filePaths)
|
||||
await startProsodyCertificatesRenewCheck(options, config)
|
||||
startProsodyLogRotate(options, filePaths)
|
||||
startProsodyCertificatesRenewCheck(options, config)
|
||||
}
|
||||
|
||||
async function ensureProsodyNotRunning (options: RegisterServerOptions): Promise<void> {
|
||||
|
@ -10,7 +10,7 @@ import { reloadProsody } from './ctl'
|
||||
type Rotate = (file: string, options: {
|
||||
count?: number
|
||||
compress?: boolean
|
||||
}, cb: Function) => void
|
||||
}, cb: (err: any) => void) => void
|
||||
const rotate: Rotate = require('log-rotate')
|
||||
|
||||
interface ProsodyLogRotate {
|
||||
@ -27,9 +27,10 @@ async function _rotate (options: RegisterServerOptions, path: string): Promise<v
|
||||
rotate(path, { count: 14, compress: false }, (err: any) => {
|
||||
if (err) {
|
||||
options.peertubeHelpers.logger.error('Failed to rotate file ' + path, err)
|
||||
return resolve()
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
return resolve()
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
return p
|
||||
@ -79,7 +80,7 @@ function startProsodyLogRotate (options: RegisterServerOptions, paths: ProsodyFi
|
||||
}, checkInterval)
|
||||
|
||||
logRotate = {
|
||||
timer: timer,
|
||||
timer,
|
||||
lastRotation: Date.now()
|
||||
}
|
||||
}
|
||||
|
76
server/lib/prosody/migration/migrateV12.ts
Normal file
76
server/lib/prosody/migration/migrateV12.ts
Normal 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 v12.0.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
|
||||
}
|
@ -36,11 +36,11 @@ class RoomChannel {
|
||||
|
||||
protected room2Channel: Map<string, number> = new Map<string, number>()
|
||||
protected channel2Rooms: Map<number, Map<string, true>> = new Map<number, Map<string, true>>()
|
||||
protected needSync: boolean = false
|
||||
protected needSync = false
|
||||
protected roomConfToUpdate: Map<string, true> = new Map<string, true>()
|
||||
|
||||
protected syncTimeout: ReturnType<typeof setTimeout> | undefined
|
||||
protected isWriting: boolean = false
|
||||
protected isWriting = false
|
||||
|
||||
constructor (params: {
|
||||
options: RegisterServerOptions
|
||||
@ -113,7 +113,7 @@ class RoomChannel {
|
||||
let content: string
|
||||
try {
|
||||
content = (await fs.promises.readFile(this.dataFilePath)).toString()
|
||||
} catch (err) {
|
||||
} catch (_err) {
|
||||
this.logger.info('Failed reading room-channel data file (' + this.dataFilePath + '), assuming it does not exists')
|
||||
return false
|
||||
}
|
||||
@ -122,7 +122,7 @@ class RoomChannel {
|
||||
let data: any
|
||||
try {
|
||||
data = JSON.parse(content)
|
||||
} catch (err) {
|
||||
} catch (_err) {
|
||||
this.logger.error('Unable to parse the content of the room-channel data file, will start with an empty database.')
|
||||
return false
|
||||
}
|
||||
@ -233,7 +233,7 @@ class RoomChannel {
|
||||
}
|
||||
|
||||
await fillVideoCustomFields(this.options, video)
|
||||
const hasChat = await videoHasWebchat({
|
||||
const hasChat = videoHasWebchat({
|
||||
'chat-per-live-video': !!settings['chat-per-live-video'],
|
||||
'chat-all-lives': !!settings['chat-all-lives'],
|
||||
'chat-all-non-lives': !!settings['chat-all-non-lives'],
|
||||
@ -405,7 +405,7 @@ class RoomChannel {
|
||||
this.syncTimeout = undefined
|
||||
this.logger.info('Running scheduled sync')
|
||||
this.sync().then(() => {}, (err) => {
|
||||
this.logger.error(err)
|
||||
this.logger.error(err as string)
|
||||
// We will not re-schedule the sync, to avoid flooding error log if there is an issue with the server
|
||||
})
|
||||
}, 100)
|
||||
|
@ -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
|
||||
@ -94,6 +96,20 @@ async function initConfigurationApiRouter (options: RegisterServerOptions, route
|
||||
req.body.bot = channelOptions.bot
|
||||
req.body.bot.enabled = false
|
||||
}
|
||||
// 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) {
|
||||
// logger.debug('Bot disabled, loading the previous bot conf to not override hidden fields')
|
||||
// const channelOptions =
|
||||
// await getChannelConfigurationOptions(options, channelInfos.id) ??
|
||||
// getDefaultChannelConfigurationOptions(options)
|
||||
// req.body.bot.forbidSpecialChars.reason = channelOptions.bot.forbidSpecialChars.reason
|
||||
// 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) {
|
||||
logger.warn(err)
|
||||
@ -168,6 +184,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 customEmojisRegexp = await emojis.getChannelCustomEmojisRegexp(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_custom_emoji_regexp: customEmojisRegexp
|
||||
}).then(
|
||||
() => {},
|
||||
(err) => logger.error(err)
|
||||
)
|
||||
}
|
||||
|
||||
// Reloading data, to send them back to front:
|
||||
const channelEmojis =
|
||||
(await emojis.channelCustomEmojisDefinition(channelInfos.id)) ??
|
||||
@ -184,6 +214,44 @@ async function initConfigurationApiRouter (options: RegisterServerOptions, route
|
||||
}
|
||||
}
|
||||
]))
|
||||
|
||||
router.post('/configuration/channel/emojis/:channelId/enable_emoji_only', asyncMiddleware([
|
||||
checkConfigurationEnabledMiddleware(options),
|
||||
getCheckConfigurationChannelMiddleware(options),
|
||||
async (req: Request, res: Response, _next: NextFunction): Promise<void> => {
|
||||
try {
|
||||
if (!res.locals.channelInfos) {
|
||||
throw new Error('Missing channelInfos in res.locals, should not happen')
|
||||
}
|
||||
|
||||
const emojis = Emojis.singleton()
|
||||
const channelInfos = res.locals.channelInfos as ChannelInfos
|
||||
|
||||
logger.info(`Enabling emoji only mode on each channel ${channelInfos.id} rooms ...`)
|
||||
|
||||
// We can also update the EmojisRegexp, just in case.
|
||||
const customEmojisRegexp = await emojis.getChannelCustomEmojisRegexp(channelInfos.id)
|
||||
const roomJIDs = RoomChannel.singleton().getChannelRoomJIDs(channelInfos.id)
|
||||
for (const roomJID of roomJIDs) {
|
||||
// No need to await here
|
||||
logger.info(`Enabling emoji only mode on room ${roomJID} ...`)
|
||||
updateProsodyRoom(options, roomJID, {
|
||||
livechat_emoji_only: true,
|
||||
livechat_custom_emoji_regexp: customEmojisRegexp
|
||||
}).then(
|
||||
() => {},
|
||||
(err) => logger.error(err)
|
||||
)
|
||||
}
|
||||
|
||||
res.status(200)
|
||||
res.json({ ok: true })
|
||||
} catch (err) {
|
||||
logger.error(err)
|
||||
res.sendStatus(500)
|
||||
}
|
||||
}
|
||||
]))
|
||||
}
|
||||
|
||||
export {
|
||||
|
@ -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_custom_emoji_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 customEmojisRegexp = await Emojis.singletonSafe()?.getChannelCustomEmojisRegexp(channelId)
|
||||
|
||||
return {
|
||||
slow_mode_duration: channelOptions.slowMode.duration,
|
||||
mute_anonymous: channelOptions.mute.anonymous,
|
||||
livechat_custom_emoji_regexp: customEmojisRegexp,
|
||||
livechat_muc_terms: channelOptions.terms,
|
||||
moderation_delay: channelOptions.moderation.delay,
|
||||
anonymize_moderation_actions: channelOptions.moderation.anonymize
|
||||
@ -80,7 +86,7 @@ async function initRoomApiRouter (options: RegisterServerOptions, router: Router
|
||||
// Now, we have two different room type: per video or per channel.
|
||||
if (settings['prosody-room-type'] === 'channel') {
|
||||
const matches = jid.match(/^channel\.(\d+)$/)
|
||||
if (!matches || !matches[1]) {
|
||||
if (!matches?.[1]) {
|
||||
logger.warn(`Invalid channel room jid '${jid}'.`)
|
||||
res.sendStatus(403)
|
||||
return
|
||||
@ -111,7 +117,7 @@ async function initRoomApiRouter (options: RegisterServerOptions, router: Router
|
||||
},
|
||||
await _getChannelSpecificOptions(options, channelId)
|
||||
),
|
||||
affiliations: affiliations
|
||||
affiliations
|
||||
}
|
||||
|
||||
RoomChannel.singleton().link(channelId, jid)
|
||||
@ -166,7 +172,7 @@ async function initRoomApiRouter (options: RegisterServerOptions, router: Router
|
||||
},
|
||||
await _getChannelSpecificOptions(options, video.channelId)
|
||||
),
|
||||
affiliations: affiliations
|
||||
affiliations
|
||||
}
|
||||
|
||||
RoomChannel.singleton().link(video.channelId, jid)
|
||||
|
@ -13,7 +13,12 @@ async function initRouters (options: RegisterServerOptions): Promise<void> {
|
||||
const { getRouter } = options
|
||||
|
||||
const router = getRouter()
|
||||
router.get('/ping', (req: Request, res: Response, _next: NextFunction) => res.json({ message: 'pong' }))
|
||||
router.get(
|
||||
'/ping',
|
||||
(req: Request, res: Response, _next: NextFunction) => {
|
||||
res.json({ message: 'pong' })
|
||||
}
|
||||
)
|
||||
|
||||
router.use('/webchat', await initWebchatRouter(options))
|
||||
router.use('/settings', await initSettingsRouter(options))
|
||||
|
@ -54,7 +54,7 @@ async function initOIDCRouter (options: RegisterServerOptions): Promise<Router>
|
||||
const redirectUrl = await oidc.initAuthenticationProcess(req, res)
|
||||
res.redirect(redirectUrl)
|
||||
} catch (err) {
|
||||
logger.error('[oidc router] Failed to process the OIDC callback: ' + (err as string))
|
||||
logger.error('[oidc router] Failed to process the OIDC connect call: ' + (err as string))
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
@ -142,12 +142,13 @@ async function initWebchatRouter (options: RegisterServerOptionsV5): Promise<Rou
|
||||
res.status(200)
|
||||
res.type('html')
|
||||
res.send(page)
|
||||
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
|
||||
} catch (err: LivechatError | any) {
|
||||
const code = err.livechatError?.code ?? 500
|
||||
const additionnalMessage: string = escapeHTML(err.livechatError?.message as string ?? '')
|
||||
const message: string = escapeHTML(loc('chatroom_not_accessible'))
|
||||
|
||||
res.status(code)
|
||||
res.status(typeof code === 'number' ? code : 500)
|
||||
res.send(`<!DOCTYPE html PUBLIC "-//IETF//DTD HTML 2.0//EN"><html>
|
||||
<head><title>${message}</title></head>
|
||||
<body>
|
||||
@ -272,7 +273,7 @@ async function initWebchatRouter (options: RegisterServerOptionsV5): Promise<Rou
|
||||
res.status(200)
|
||||
const r: ProsodyListRoomsResult = {
|
||||
ok: true,
|
||||
rooms: rooms
|
||||
rooms
|
||||
}
|
||||
res.json(r)
|
||||
}
|
||||
@ -324,7 +325,7 @@ async function enableProxyRoute (
|
||||
target: 'http://localhost:' + prosodyProxyInfo.port + '/http-bind',
|
||||
ignorePath: true
|
||||
})
|
||||
currentHttpBindProxy.on('error', (err, req, res) => {
|
||||
currentHttpBindProxy.on('error', (err, req, res: any) => { // FIXME: remove the `any`.
|
||||
// We must handle errors, otherwise Peertube server crashes!
|
||||
logger.error(
|
||||
'The http bind proxy got an error ' +
|
||||
@ -346,7 +347,7 @@ async function enableProxyRoute (
|
||||
ignorePath: true,
|
||||
ws: true
|
||||
})
|
||||
currentWebsocketProxy.on('error', (err, req, res) => {
|
||||
currentWebsocketProxy.on('error', (err, req, res: any) => { // FIXME: remove the `any`.
|
||||
// We must handle errors, otherwise Peertube server crashes!
|
||||
logger.error(
|
||||
'The websocket proxy got an error ' +
|
||||
@ -368,7 +369,7 @@ async function enableProxyRoute (
|
||||
ignorePath: true,
|
||||
ws: true
|
||||
})
|
||||
currentS2SWebsocketProxy.on('error', (err, req, res) => {
|
||||
currentS2SWebsocketProxy.on('error', (err, req, res: any) => { // FIXME: remove the `any`.
|
||||
// We must handle errors, otherwise Peertube server crashes!
|
||||
logger.error(
|
||||
'The s2s websocket proxy got an error ' +
|
||||
|
@ -46,7 +46,7 @@ async function initRSS (options: RegisterServerOptions): Promise<void> {
|
||||
}
|
||||
|
||||
await fillVideoCustomFields(options, video)
|
||||
const hasChat = await videoHasWebchat({
|
||||
const hasChat = videoHasWebchat({
|
||||
'chat-per-live-video': !!settings['chat-per-live-video'],
|
||||
'chat-all-lives': !!settings['chat-all-lives'],
|
||||
'chat-all-non-lives': !!settings['chat-all-non-lives'],
|
||||
|
@ -125,7 +125,7 @@ function initImportantNotesSettings ({ registerSetting }: RegisterServerOptions)
|
||||
// Note: the following text as a variable in it.
|
||||
// Not translating it: it should be very rare.
|
||||
descriptionHTML: `<span class="peertube-plugin-livechat-warning">
|
||||
It seems that your are using a ${process.arch} CPU,
|
||||
It seems that your are using a ${process.arch} CPU,
|
||||
which is not compatible with the plugin.
|
||||
Please read
|
||||
<a
|
||||
@ -530,7 +530,7 @@ function initThemingSettings ({ registerSetting }: RegisterServerOptions): void
|
||||
{ value: 'peertube', label: loc('converse_theme_option_peertube') },
|
||||
{ value: 'default', label: loc('converse_theme_option_default') },
|
||||
{ value: 'cyberpunk', label: loc('converse_theme_option_cyberpunk') }
|
||||
] as Array<{value: ConverseJSTheme, label: string}>,
|
||||
] as Array<{ value: ConverseJSTheme, label: string }>,
|
||||
descriptionHTML: loc('converse_theme_description')
|
||||
})
|
||||
|
||||
|
@ -18,6 +18,7 @@ import { BotConfiguration } from './lib/configuration/bot'
|
||||
import { BotsCtl } from './lib/bots/ctl'
|
||||
import { ExternalAuthOIDC } from './lib/external-auth/oidc'
|
||||
import { migrateMUCAffiliations } from './lib/prosody/migration/migrateV10'
|
||||
import { updateProsodyChannelEmojisRegex } from './lib/prosody/migration/migrateV12'
|
||||
import { Emojis } from './lib/emojis'
|
||||
import { LivechatProsodyAuth } from './lib/prosody/auth'
|
||||
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)
|
||||
// we do this after the preBotPromise, just to avoid doing both at the same time.
|
||||
preBotPromise.then(() => {
|
||||
migrateMUCAffiliations(options).then(
|
||||
const p = migrateMUCAffiliations(options).then(
|
||||
() => {},
|
||||
(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(() => {})
|
||||
}, () => {})
|
||||
} catch (error) {
|
||||
options.peertubeHelpers.logger.error('Error when launching Prosody: ' + (error as string))
|
||||
|
@ -1,16 +1,9 @@
|
||||
{
|
||||
"extends": "@tsconfig/node12/tsconfig.json",
|
||||
"extends": "@tsconfig/node16/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "node", // Tell tsc to look in node_modules for modules
|
||||
"strict": true, // That implies alwaysStrict, noImplicitAny, noImplicitThis
|
||||
|
||||
"alwaysStrict": true, // should already be true because of strict:true
|
||||
"noImplicitAny": true, // should already be true because of strict:true
|
||||
"noImplicitThis": true, // should already be true because of strict:true
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitOverride": true,
|
||||
"strictBindCallApply": true, // should already be true because of strict:true
|
||||
"noUnusedLocals": false, // works better as a linter error/warning
|
||||
"noUnusedLocals": true,
|
||||
"removeComments": true,
|
||||
"sourceMap": true,
|
||||
|
||||
|
Reference in New Issue
Block a user