This commit is contained in:
2024-12-03 17:03:42 -05:00
231 changed files with 18277 additions and 11174 deletions

View File

@ -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"
}
}

View File

@ -1,3 +0,0 @@
SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
SPDX-License-Identifier: AGPL-3.0-only

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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]

View File

@ -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)

View File

@ -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.

View File

@ -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')

View File

@ -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)

View File

@ -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.
}

View File

@ -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: [],

View File

@ -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

View File

@ -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 + '}'
}

View File

@ -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
}
}

View File

@ -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)
}
})

View File

@ -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'],

View File

@ -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,

View File

@ -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 {

View File

@ -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) {

View File

@ -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) })
}
}

View File

@ -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: {

View File

@ -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 {

View File

@ -39,7 +39,7 @@ function startProsodyCertificatesRenewCheck (options: RegisterServerOptions, con
}, checkInterval)
renew = {
timer: timer
timer
}
}

View File

@ -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
}

View File

@ -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 = {}

View File

@ -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()

View File

@ -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> {

View File

@ -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()
}
}

View File

@ -0,0 +1,76 @@
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
//
// SPDX-License-Identifier: AGPL-3.0-only
import type { RegisterServerOptions } from '@peertube/peertube-types'
import { listProsodyRooms, updateProsodyRoom } from '../api/manage-rooms'
import * as path from 'path'
import * as fs from 'fs'
import { Emojis } from '../../emojis'
/**
* Livechat 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
}

View File

@ -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)

View File

@ -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 {

View File

@ -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)

View File

@ -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))

View File

@ -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()
}
}

View File

@ -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 ' +

View File

@ -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'],

View File

@ -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')
})

View File

@ -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))

View File

@ -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,