Moderation Bot integration WIP:

* Start and stop the bot WIP
* Prosody: removing the BOSH module from the global scope (must only be present on relevant virtualhosts)
* Some refactoring
This commit is contained in:
John Livingston 2023-09-18 18:53:07 +02:00
parent 65fd49a81c
commit f97e54d499
No known key found for this signature in database
GPG Key ID: B17B5640CE66CDBC
15 changed files with 348 additions and 26 deletions

View File

@ -1,6 +1,6 @@
# Changelog # Changelog
## XXX (Unreleased Yet) ## 8.0.0 (Unreleased Yet)
### New features ### New features
@ -13,6 +13,7 @@
* Links to documentation are now using the front-end language to point to the translated documentation page (except for some links generated from the backend, in the diagnostic tool for example). * Links to documentation are now using the front-end language to point to the translated documentation page (except for some links generated from the backend, in the diagnostic tool for example).
* Some code refactoring. * Some code refactoring.
* You can now configure on which network interfaces Prosody will listen for external components. * You can now configure on which network interfaces Prosody will listen for external components.
* Prosody: removing the BOSH module from the global scope (must only be present on relevant virtualhosts).
## 7.2.2 ## 7.2.2

View File

@ -11,7 +11,7 @@ async function registerConfiguration (clientOptions: RegisterClientOptions): Pro
const { peertubeHelpers, registerClientRoute, registerHook } = clientOptions const { peertubeHelpers, registerClientRoute, registerHook } = clientOptions
const settings = await peertubeHelpers.getSettings() const settings = await peertubeHelpers.getSettings()
if (settings['disable-configuration']) { return } if (settings['disable-channel-configuration']) { return }
registerClientRoute({ registerClientRoute({
route: 'livechat/configuration', route: 'livechat/configuration',

View File

@ -302,7 +302,7 @@ configuration_description: |
users will be able to add some customization on their channels, users will be able to add some customization on their channels,
activate the moderation bot, ... activate the moderation bot, ...
disable_configuration_label: "Disable the advanced channel configuration and the chatbot" disable_channel_configuration_label: "Disable the advanced channel configuration and the chatbot"
save: "Save" save: "Save"
cancel: "Cancel" cancel: "Cancel"

14
package-lock.json generated
View File

@ -15,7 +15,7 @@
"http-proxy": "^1.18.1", "http-proxy": "^1.18.1",
"log-rotate": "^0.2.8", "log-rotate": "^0.2.8",
"validate-color": "^2.2.1", "validate-color": "^2.2.1",
"xmppjs-chat-bot": "^0.2.1" "xmppjs-chat-bot": "^0.2.2"
}, },
"devDependencies": { "devDependencies": {
"@peertube/feed": "^5.1.0", "@peertube/feed": "^5.1.0",
@ -12069,9 +12069,9 @@
} }
}, },
"node_modules/xmppjs-chat-bot": { "node_modules/xmppjs-chat-bot": {
"version": "0.2.1", "version": "0.2.2",
"resolved": "https://registry.npmjs.org/xmppjs-chat-bot/-/xmppjs-chat-bot-0.2.1.tgz", "resolved": "https://registry.npmjs.org/xmppjs-chat-bot/-/xmppjs-chat-bot-0.2.2.tgz",
"integrity": "sha512-O2uiVBlgio/5psb+DhCYjBU2HfW0pnQwTuKjutBTLoaTtfG+A+oxFmwarZKFDUjiQK6DrytW+LL+M8792rBaxw==", "integrity": "sha512-/t1L2fSW04M/5zEGYQzXqgTa7CVaE1dAT9kO1C5iSOrd4HzksQ6vVsk0PlAbEZere08pbea8Kw8uxBSwGlTiXw==",
"funding": [ "funding": [
"https://paypal.me/JohnXLivingston", "https://paypal.me/JohnXLivingston",
"https://liberapay.com/JohnLivingston/" "https://liberapay.com/JohnLivingston/"
@ -21291,9 +21291,9 @@
} }
}, },
"xmppjs-chat-bot": { "xmppjs-chat-bot": {
"version": "0.2.1", "version": "0.2.2",
"resolved": "https://registry.npmjs.org/xmppjs-chat-bot/-/xmppjs-chat-bot-0.2.1.tgz", "resolved": "https://registry.npmjs.org/xmppjs-chat-bot/-/xmppjs-chat-bot-0.2.2.tgz",
"integrity": "sha512-O2uiVBlgio/5psb+DhCYjBU2HfW0pnQwTuKjutBTLoaTtfG+A+oxFmwarZKFDUjiQK6DrytW+LL+M8792rBaxw==", "integrity": "sha512-/t1L2fSW04M/5zEGYQzXqgTa7CVaE1dAT9kO1C5iSOrd4HzksQ6vVsk0PlAbEZere08pbea8Kw8uxBSwGlTiXw==",
"requires": { "requires": {
"@xmpp/client": "^0.13.1", "@xmpp/client": "^0.13.1",
"@xmpp/component": "^0.13.1", "@xmpp/component": "^0.13.1",

View File

@ -38,7 +38,7 @@
"http-proxy": "^1.18.1", "http-proxy": "^1.18.1",
"log-rotate": "^0.2.8", "log-rotate": "^0.2.8",
"validate-color": "^2.2.1", "validate-color": "^2.2.1",
"xmppjs-chat-bot": "^0.2.1" "xmppjs-chat-bot": "^0.2.2"
}, },
"devDependencies": { "devDependencies": {
"@peertube/feed": "^5.1.0", "@peertube/feed": "^5.1.0",

182
server/lib/bots/ctl.ts Normal file
View File

@ -0,0 +1,182 @@
import type { RegisterServerOptions } from '@peertube/peertube-types'
import type { Config as XMPPChatBotConfig } from 'xmppjs-chat-bot'
import { BotConfiguration } from '../configuration/bot'
import * as child_process from 'child_process'
let singleton: BotsCtl | undefined
/**
* This class is to control the plugin bots.
* For now there is only one, the Moderation bot.
* But all public methods are made as if there was several bots, so it will be easier to add bots.
*/
class BotsCtl {
protected readonly options: RegisterServerOptions
protected readonly moderationGlobalConf: XMPPChatBotConfig
protected readonly logger: {
debug: (s: string) => void
info: (s: string) => void
warn: (s: string) => void
error: (s: string) => void
}
protected moderationBotProcess: ReturnType<typeof child_process.spawn> | undefined
constructor (params: {
options: RegisterServerOptions
moderationGlobalConf: XMPPChatBotConfig
}) {
this.options = params.options
this.moderationGlobalConf = params.moderationGlobalConf
const logger = params.options.peertubeHelpers.logger
this.logger = {
debug: (s) => logger.debug('[Bots] ' + s),
info: (s) => logger.info('[Bots] ' + s),
warn: (s) => logger.warn('[Bots] ' + s),
error: (s) => logger.error('[Bots] ' + s)
}
}
/**
* Starts all the required bots.
* If bots are already running, does nothing.
*/
public async start (): Promise<void> {
if (await this.options.settingsManager.getSetting('disable-channel-configuration')) {
this.logger.info('Advanced channel configuration is disabled, no bot to start')
return
}
this.logger.info('Starting moderation bot...')
if (this.moderationBotProcess?.exitCode === null) {
this.logger.info('Moderation bot still running, nothing to do')
return
}
const paths = BotConfiguration.singleton().configurationPaths()
// We will run: npm exec -- xmppjs-chat-bot [...]
const execArgs = [
'exec',
'--',
'xmppjs-chat-bot',
'run',
'-f',
paths.moderation.globalFile,
'--room-conf-dir',
paths.moderation.roomConfDir
]
const moderationBotProcess = child_process.spawn('npm', execArgs, {
cwd: __dirname, // must be in the livechat plugin tree, so that npm can found the package.
env: {
...process.env // will include NODE_ENV and co
}
})
moderationBotProcess.stdout?.on('data', (data) => {
this.logger.debug(`ModerationBot stdout: ${data as string}`)
})
moderationBotProcess.stderr?.on('data', (data) => {
this.logger.error(`ModerationBot stderr: ${data as string}`)
})
moderationBotProcess.on('error', (error) => {
this.logger.error(`ModerationBot exec error: ${JSON.stringify(error)}`)
})
moderationBotProcess.on('exit', (code) => {
this.logger.info(`ModerationBot process exited with code ${code ?? 'null'}`)
})
moderationBotProcess.on('close', (code) => {
this.logger.info(`ModerationBot process closed all stdio with code ${code ?? 'null'}`)
})
this.moderationBotProcess = moderationBotProcess
}
/**
* Stops all the bots
*/
public async stop (): Promise<void> {
this.logger.info('Stopping bots...')
if (!this.moderationBotProcess) {
this.logger.info('moderationBot was never running, everything is fine.')
return
}
if (this.moderationBotProcess.exitCode !== null) {
this.logger.info('The moderation bot has an exitCode, already stopped.')
return
}
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>
let resolved = false
// Trying to kill, and force kill if it takes more than 2 seconds
const timeout = setTimeout(() => {
this.logger.error('Moderation bot was not killed within 2 seconds, force killing')
moderationBotProcess.kill('SIGKILL')
resolved = true
resolve()
}, 2000)
moderationBotProcess.on('exit', () => {
if (resolved) { return }
resolved = true
if (timeout) { clearTimeout(timeout) }
resolve()
})
moderationBotProcess.on('close', () => {
if (resolved) { return }
resolved = true
if (timeout) { clearTimeout(timeout) }
resolve()
})
moderationBotProcess.kill()
} catch (err) {
this.logger.error(err as string)
reject(err)
}
})
return p
}
/**
* Instanciate a new singleton
* @param options server options
*/
public static async initSingleton (options: RegisterServerOptions): Promise<BotsCtl> {
// forceRefresh the bot global configuration file:
const moderationGlobalConf = await BotConfiguration.singleton().getModerationBotGlobalConf(true)
singleton = new BotsCtl({
options,
moderationGlobalConf
})
return singleton
}
/**
* Returns the singleton, of thrown an exception if it is not initialized yet.
* @returns the singleton
*/
public static singleton (): BotsCtl {
if (!singleton) {
throw new Error('Bots singleton not initialized yet')
}
return singleton
}
public static async destroySingleton (): Promise<void> {
if (!singleton) { return }
await singleton.stop()
singleton = undefined
}
}
export {
BotsCtl
}

View File

@ -1,6 +1,7 @@
import type { RegisterServerOptions } from '@peertube/peertube-types' import type { RegisterServerOptions } from '@peertube/peertube-types'
import type { RoomConf } from 'xmppjs-chat-bot' import type { RoomConf, Config } from 'xmppjs-chat-bot'
import { getProsodyDomain } from '../prosody/config/domain' import { getProsodyDomain } from '../prosody/config/domain'
import { isDebugMode } from '../debug'
import * as path from 'path' import * as path from 'path'
import * as fs from 'fs' import * as fs from 'fs'
@ -18,8 +19,10 @@ type ChannelCommonRoomConf = Omit<RoomConf, 'local' | 'domain'>
class BotConfiguration { class BotConfiguration {
protected readonly options: RegisterServerOptions protected readonly options: RegisterServerOptions
protected readonly mucDomain: string protected readonly mucDomain: string
protected readonly botsDomain: string
protected readonly confDir: string protected readonly confDir: string
protected readonly roomConfDir: string protected readonly roomConfDir: string
protected readonly moderationBotGlobalConf: string
protected readonly logger: { protected readonly logger: {
debug: (s: string) => void debug: (s: string) => void
info: (s: string) => void info: (s: string) => void
@ -32,13 +35,17 @@ class BotConfiguration {
constructor (params: { constructor (params: {
options: RegisterServerOptions options: RegisterServerOptions
mucDomain: string mucDomain: string
botsDomain: string
confDir: string confDir: string
roomConfDir: string roomConfDir: string
moderationBotGlobalConf: string
}) { }) {
this.options = params.options this.options = params.options
this.mucDomain = params.mucDomain this.mucDomain = params.mucDomain
this.botsDomain = params.botsDomain
this.confDir = params.confDir this.confDir = params.confDir
this.roomConfDir = params.roomConfDir this.roomConfDir = params.roomConfDir
this.moderationBotGlobalConf = params.moderationBotGlobalConf
const logger = params.options.peertubeHelpers.logger const logger = params.options.peertubeHelpers.logger
this.logger = { this.logger = {
@ -55,15 +62,21 @@ class BotConfiguration {
public static async initSingleton (options: RegisterServerOptions): Promise<BotConfiguration> { public static async initSingleton (options: RegisterServerOptions): Promise<BotConfiguration> {
const prosodyDomain = await getProsodyDomain(options) const prosodyDomain = await getProsodyDomain(options)
const mucDomain = 'room.' + prosodyDomain const mucDomain = 'room.' + prosodyDomain
const botsDomain = 'bot.' + prosodyDomain
const confDir = path.resolve( const confDir = path.resolve(
options.peertubeHelpers.plugin.getDataDirectoryPath(), options.peertubeHelpers.plugin.getDataDirectoryPath(),
'bot', 'bot',
mucDomain mucDomain
) )
const roomConfDir = path.resolve( const roomConfDir = path.resolve(
confDir, confDir,
'rooms' 'rooms'
) )
const moderationBotGlobalConf = path.resolve(
confDir,
'moderation.json'
)
await fs.promises.mkdir(confDir, { recursive: true }) await fs.promises.mkdir(confDir, { recursive: true })
await fs.promises.mkdir(roomConfDir, { recursive: true }) await fs.promises.mkdir(roomConfDir, { recursive: true })
@ -71,8 +84,10 @@ class BotConfiguration {
singleton = new BotConfiguration({ singleton = new BotConfiguration({
options, options,
mucDomain, mucDomain,
botsDomain,
confDir, confDir,
roomConfDir roomConfDir,
moderationBotGlobalConf
}) })
return singleton return singleton
@ -93,7 +108,7 @@ class BotConfiguration {
* @param roomJIDParam Room full or local JID * @param roomJIDParam Room full or local JID
* @param conf Configuration to write * @param conf Configuration to write
*/ */
public async update (roomJIDParam: string, conf: ChannelCommonRoomConf): Promise<void> { public async updateRoom (roomJIDParam: string, conf: ChannelCommonRoomConf): Promise<void> {
const roomJID = this._canonicJID(roomJIDParam) const roomJID = this._canonicJID(roomJIDParam)
if (!roomJID) { if (!roomJID) {
this.logger.error('Invalid room JID') this.logger.error('Invalid room JID')
@ -141,6 +156,65 @@ class BotConfiguration {
await this._writeRoomConf(roomJID) await this._writeRoomConf(roomJID)
} }
/**
* Returns the moderation bot global configuration.
* It it does not exists, creates it.
* @param forceRefresh if true, regenerates the configuration file, even if exists.
*/
public async getModerationBotGlobalConf (forceRefresh?: boolean): Promise<Config> {
let config: Config | undefined
if (!forceRefresh) {
try {
const content = (await fs.promises.readFile(this.moderationBotGlobalConf, {
encoding: 'utf-8'
})).toString()
config = JSON.parse(content)
} catch (err) {
this.logger.info('Error reading the moderation bot global configuration file, assuming it does not exists.')
config = undefined
}
}
if (!config) {
// FIXME: use existing lib to get the port, dont hardcode default value here.
const portSetting = await this.options.settingsManager.getSetting('prosody-port')
const port = (portSetting as string) || '52800'
config = {
type: 'client',
connection: {
username: 'moderator',
password: Math.random().toString(36).slice(2, 12),
domain: this.botsDomain,
// Note: using localhost, and not currentProsody.host, because it does not always resolve correctly
service: 'xmpp://localhost:' + port
},
name: 'Moderator',
logger: 'ConsoleLogger',
log_level: isDebugMode(this.options) ? 'debug' : 'info'
}
await fs.promises.writeFile(this.moderationBotGlobalConf, JSON.stringify(config), {
encoding: 'utf-8'
})
}
return config
}
public configurationPaths (): {
moderation: {
globalFile: string
roomConfDir: string
}
} {
return {
moderation: {
globalFile: this.moderationBotGlobalConf,
roomConfDir: this.roomConfDir
}
}
}
/** /**
* frees the singleton * frees the singleton
*/ */

View File

@ -10,9 +10,9 @@ import type { RequestPromiseHandler } from '../async'
function checkConfigurationEnabledMiddleware (options: RegisterServerOptions): RequestPromiseHandler { function checkConfigurationEnabledMiddleware (options: RegisterServerOptions): RequestPromiseHandler {
return async (req: Request, res: Response, next: NextFunction) => { return async (req: Request, res: Response, next: NextFunction) => {
const settings = await options.settingsManager.getSettings([ const settings = await options.settingsManager.getSettings([
'disable-configuration' 'disable-channel-configuration'
]) ])
if (!settings['disable-configuration']) { if (!settings['disable-channel-configuration']) {
next() next()
return return
} }

View File

@ -1,4 +1,5 @@
import type { RegisterServerOptions } from '@peertube/peertube-types' import type { RegisterServerOptions } from '@peertube/peertube-types'
import type { Config as XMPPBotConfig } from 'xmppjs-chat-bot'
import type { ProsodyLogLevel } from './config/content' import type { ProsodyLogLevel } from './config/content'
import * as fs from 'fs' import * as fs from 'fs'
import * as path from 'path' import * as path from 'path'
@ -9,6 +10,7 @@ import { getProsodyDomain } from './config/domain'
import { getAPIKey } from '../apikey' import { getAPIKey } from '../apikey'
import { parseExternalComponents } from './config/components' import { parseExternalComponents } from './config/components'
import { getRemoteServerInfosDir } from '../federation/storage' import { getRemoteServerInfosDir } from '../federation/storage'
import { BotConfiguration } from '../configuration/bot'
async function getWorkingDir (options: RegisterServerOptions): Promise<string> { async function getWorkingDir (options: RegisterServerOptions): Promise<string> {
const peertubeHelpers = options.peertubeHelpers const peertubeHelpers = options.peertubeHelpers
@ -125,6 +127,9 @@ interface ProsodyConfig {
logExpiration: ConfigLogExpiration logExpiration: ConfigLogExpiration
valuesToHideInDiagnostic: Map<string, string> valuesToHideInDiagnostic: Map<string, string>
certificates: ProsodyConfigCertificates certificates: ProsodyConfigCertificates
bots: {
moderation?: XMPPBotConfig
}
} }
async function getProsodyConfig (options: RegisterServerOptionsV5): Promise<ProsodyConfig> { async function getProsodyConfig (options: RegisterServerOptionsV5): Promise<ProsodyConfig> {
const logger = options.peertubeHelpers.logger const logger = options.peertubeHelpers.logger
@ -147,7 +152,8 @@ async function getProsodyConfig (options: RegisterServerOptionsV5): Promise<Pros
'prosody-components-interfaces', 'prosody-components-interfaces',
'prosody-components-list', 'prosody-components-list',
'chat-no-anonymous', 'chat-no-anonymous',
'federation-dont-publish-remotely' 'federation-dont-publish-remotely',
'disable-channel-configuration'
]) ])
const valuesToHideInDiagnostic = new Map<string, string>() const valuesToHideInDiagnostic = new Map<string, string>()
@ -168,6 +174,7 @@ async function getProsodyConfig (options: RegisterServerOptionsV5): Promise<Pros
// enableRemoteChatConnections: local users can communicate with external rooms // enableRemoteChatConnections: local users can communicate with external rooms
const enableRemoteChatConnections = !(settings['federation-dont-publish-remotely'] as boolean) const enableRemoteChatConnections = !(settings['federation-dont-publish-remotely'] as boolean)
let certificates: ProsodyConfigCertificates = false let certificates: ProsodyConfigCertificates = false
const bots: ProsodyConfig['bots'] = {}
const apikey = await getAPIKey(options) const apikey = await getAPIKey(options)
valuesToHideInDiagnostic.set('APIKey', apikey) valuesToHideInDiagnostic.set('APIKey', apikey)
@ -287,6 +294,12 @@ async function getProsodyConfig (options: RegisterServerOptionsV5): Promise<Pros
config.usePeertubeVCards(basePeertubeUrl) config.usePeertubeVCards(basePeertubeUrl)
config.useAnonymousRandomVCards(paths.avatars) config.useAnonymousRandomVCards(paths.avatars)
if (!settings['disable-channel-configuration']) {
config.useBotsVirtualHost()
bots.moderation = await BotConfiguration.singleton().getModerationBotGlobalConf()
valuesToHideInDiagnostic.set('BotPassword', bots.moderation.connection.password)
}
config.useTestModule(apikey, testApiUrl) config.useTestModule(apikey, testApiUrl)
let logLevel: ProsodyLogLevel | undefined let logLevel: ProsodyLogLevel | undefined
@ -315,7 +328,8 @@ async function getProsodyConfig (options: RegisterServerOptionsV5): Promise<Pros
logByDefault, logByDefault,
logExpiration, logExpiration,
valuesToHideInDiagnostic, valuesToHideInDiagnostic,
certificates certificates,
bots
} }
} }

View File

@ -140,6 +140,7 @@ class ProsodyConfigContent {
authenticated?: ProsodyConfigVirtualHost authenticated?: ProsodyConfigVirtualHost
anon?: ProsodyConfigVirtualHost anon?: ProsodyConfigVirtualHost
muc: ProsodyConfigComponent muc: ProsodyConfigComponent
bot?: ProsodyConfigVirtualHost
externalComponents: ProsodyConfigComponent[] = [] externalComponents: ProsodyConfigComponent[] = []
log: string log: string
prosodyDomain: string prosodyDomain: string
@ -170,7 +171,7 @@ class ProsodyConfigContent {
'version', // Replies to server version requests 'version', // Replies to server version requests
'uptime', // Report how long server has been running 'uptime', // Report how long server has been running
'ping', // Replies to XMPP pings with pongs 'ping', // Replies to XMPP pings with pongs
'bosh', // Enable BOSH clients, aka "Jabber over HTTP" // 'bosh', // Enable BOSH clients, aka "Jabber over HTTP"
// 'websocket', // Enable Websocket clients // 'websocket', // Enable Websocket clients
'posix', // POSIX functionality, sends server to background, enables syslog, etc. 'posix', // POSIX functionality, sends server to background, enables syslog, etc.
// 'pep', // Enables users to publish their avatar, mood, activity, playing music and more // 'pep', // Enables users to publish their avatar, mood, activity, playing music and more
@ -192,6 +193,7 @@ class ProsodyConfigContent {
this.global.set('certificates', this.paths.certs) this.global.set('certificates', this.paths.certs)
} }
this.muc.set('admins', [])
this.muc.set('muc_room_locking', false) this.muc.set('muc_room_locking', false)
this.muc.set('muc_tombstones', false) this.muc.set('muc_tombstones', false)
this.muc.set('muc_room_default_language', 'en') this.muc.set('muc_room_default_language', 'en')
@ -403,6 +405,16 @@ class ProsodyConfigContent {
} }
} }
/**
* Enable the bots virtualhost.
*/
useBotsVirtualHost (): void {
this.bot = new ProsodyConfigVirtualHost('bot.' + this.prosodyDomain)
this.bot.set('modules_enabled', ['ping'])
// TODO: bot vcards
}
setLog (level: ProsodyLogLevel, syslog?: ProsodyLogLevel[]): void { setLog (level: ProsodyLogLevel, syslog?: ProsodyLogLevel[]): void {
let log = '' let log = ''
log += 'log = {\n' log += 'log = {\n'
@ -431,6 +443,10 @@ class ProsodyConfigContent {
content += this.anon.write() content += this.anon.write()
content += '\n\n' content += '\n\n'
} }
if (this.bot) {
content += this.bot.write()
content += '\n\n'
}
content += this.muc.write() content += this.muc.write()
content += '\n\n' content += '\n\n'
for (const externalComponent of this.externalComponents) { for (const externalComponent of this.externalComponents) {

View File

@ -342,7 +342,7 @@ class RoomChannel {
channelConfigurationOptionsToBotRoomConf(this.options, channelConfigurationOptions) channelConfigurationOptionsToBotRoomConf(this.options, channelConfigurationOptions)
) )
await BotConfiguration.singleton().update(roomJID, botConf) await BotConfiguration.singleton().updateRoom(roomJID, botConf)
this.roomConfToUpdate.delete(roomJID) this.roomConfToUpdate.delete(roomJID)
} }

View File

@ -2,6 +2,7 @@ import type { RegisterServerOptions } from '@peertube/peertube-types'
import type { ConverseJSTheme } from '../../shared/lib/types' import type { ConverseJSTheme } from '../../shared/lib/types'
import { ensureProsodyRunning } from './prosody/ctl' import { ensureProsodyRunning } from './prosody/ctl'
import { RoomChannel } from './room-channel' import { RoomChannel } from './room-channel'
import { BotsCtl } from './bots/ctl'
import { loc } from './loc' import { loc } from './loc'
async function initSettings (options: RegisterServerOptions): Promise<void> { async function initSettings (options: RegisterServerOptions): Promise<void> {
@ -94,9 +95,9 @@ Please read
descriptionHTML: loc('experimental_warning') descriptionHTML: loc('experimental_warning')
}) })
registerSetting({ registerSetting({
name: 'disable-configuration', name: 'disable-channel-configuration',
label: loc('disable_configuration_label'), label: loc('disable_channel_configuration_label'),
// descriptionHTML: loc('disable_configuration_description'), // descriptionHTML: loc('disable_channel_configuration_description'),
type: 'input-checkbox', type: 'input-checkbox',
default: false, default: false,
private: false private: false
@ -401,8 +402,16 @@ Please read
// ********** settings changes management // ********** settings changes management
settingsManager.onSettingsChange(async (settings: any) => { settingsManager.onSettingsChange(async (settings: any) => {
// In case the Prosody port has changed, we must rewrite the Bot configuration file.
// To avoid race condition, we will just stop and start the bots at every settings saving.
await BotsCtl.destroySingleton()
await BotsCtl.initSingleton(options)
peertubeHelpers.logger.info('Saving settings, ensuring prosody is running') peertubeHelpers.logger.info('Saving settings, ensuring prosody is running')
await ensureProsodyRunning(options) await ensureProsodyRunning(options)
await BotsCtl.singleton().start()
// In case prosody-room-type changed, we must rebuild room-channel links. // In case prosody-room-type changed, we must rebuild room-channel links.
if (settings['prosody-room-type'] !== currentProsodyRoomtype) { if (settings['prosody-room-type'] !== currentProsodyRoomtype) {
peertubeHelpers.logger.info('Setting prosody-room-type has changed value, must rebuild room-channel infos') peertubeHelpers.logger.info('Setting prosody-room-type has changed value, must rebuild room-channel infos')

View File

@ -11,6 +11,7 @@ import { unloadDebugMode } from './lib/debug'
import { loadLoc } from './lib/loc' import { loadLoc } from './lib/loc'
import { RoomChannel } from './lib/room-channel' import { RoomChannel } from './lib/room-channel'
import { BotConfiguration } from './lib/configuration/bot' import { BotConfiguration } from './lib/configuration/bot'
import { BotsCtl } from './lib/bots/ctl'
import decache from 'decache' import decache from 'decache'
// FIXME: Peertube unregister don't have any parameter. // FIXME: Peertube unregister don't have any parameter.
@ -35,6 +36,9 @@ async function register (options: RegisterServerOptions): Promise<any> {
// roomChannelNeedsDataInit: if true, means that the data file does not exist (or is invalid), so we must initiate it // roomChannelNeedsDataInit: if true, means that the data file does not exist (or is invalid), so we must initiate it
const roomChannelNeedsDataInit = !await roomChannelSingleton.readData() const roomChannelNeedsDataInit = !await roomChannelSingleton.readData()
// BotsCtl.initSingleton() will force reload the bots conf files, so must be done before generating Prosody Conf.
await BotsCtl.initSingleton(options)
await migrateSettings(options) await migrateSettings(options)
await initSettings(options) await initSettings(options)
@ -48,20 +52,35 @@ async function register (options: RegisterServerOptions): Promise<any> {
await prepareProsody(options) await prepareProsody(options)
await ensureProsodyRunning(options) await ensureProsodyRunning(options)
let preBotPromise: Promise<void>
if (roomChannelNeedsDataInit) { if (roomChannelNeedsDataInit) {
logger.info('The RoomChannel singleton has not found any data, we must rebuild') logger.info('The RoomChannel singleton has not found any data, we must rebuild')
// no need to wait here, can be done without await. // no need to wait here, can be done without await.
roomChannelSingleton.rebuildData().then( preBotPromise = roomChannelSingleton.rebuildData().then(
() => { logger.info('RoomChannel singleton rebuild done') }, () => { logger.info('RoomChannel singleton rebuild done') },
(reason) => { logger.error('RoomChannel singleton rebuild failed: ' + (reason as string)) } (reason) => { logger.error('RoomChannel singleton rebuild failed: ' + (reason as string)) }
) )
} else {
preBotPromise = Promise.resolve()
} }
// Don't need to wait for the bot to start.
preBotPromise.then(
async () => {
await BotsCtl.singleton().start()
},
() => {}
)
} catch (error) { } catch (error) {
options.peertubeHelpers.logger.error('Error when launching Prosody: ' + (error as string)) options.peertubeHelpers.logger.error('Error when launching Prosody: ' + (error as string))
} }
} }
async function unregister (): Promise<any> { async function unregister (): Promise<any> {
try {
await BotsCtl.destroySingleton()
} catch (_error) {} // BotsCtl will log errors.
if (OPTIONS) { if (OPTIONS) {
try { try {
await ensureProsodyNotRunning(OPTIONS) await ensureProsodyNotRunning(OPTIONS)

View File

@ -29,7 +29,7 @@ Following settings concern the federation with other Peertube instances, and oth
Following settings concern the advanced channel options: Following settings concern the advanced channel options:
users will be able to add some customization on their channels, activate the moderation bot, ... users will be able to add some customization on their channels, activate the moderation bot, ...
### {{% livechat_label disable_configuration_label %}} ### {{% livechat_label disable_channel_configuration_label %}}
If you encounter any issue with this feature, you can disable it. If you encounter any issue with this feature, you can disable it.

View File

@ -93,9 +93,16 @@ There will be some cleaning batch, to delete deprecated files.
The `bot/muc_domain` (where muc_domain is the current MUC domain) folder contains configuration files that are read by the moderation bot. The `bot/muc_domain` (where muc_domain is the current MUC domain) folder contains configuration files that are read by the moderation bot.
This bot uses the [xmppjs-chat-bot](https://github.com/JohnXLivingston/xmppjs-chat-bot) package. This bot uses the [xmppjs-chat-bot](https://github.com/JohnXLivingston/xmppjs-chat-bot) package.
Note: we include the MUC domain (`room.instance.tld`) in the filename in case the instance domain changes. Note: we include the MUC domain (`room.instance.tld`) in the dirname in case the instance domain changes.
In such case, existing rooms could get lost, and we want a way to ignore them to avoid gettings errors. In such case, existing rooms could get lost, and we want a way to ignore them to avoid gettings errors.
## bot/muc_domain/moderation.json
The `bot/muc_domain/moderation.json` file contains the moderation bot global configuration.
This bot uses the [xmppjs-chat-bot](https://github.com/JohnXLivingston/xmppjs-chat-bot) package, see it's README file for more information.
Note: this includes the bot username and password. Don't let it leak.
### bot/muc_domain/rooms ### bot/muc_domain/rooms
The `bot/muc_domain/rooms` folder contains room configuration files. The `bot/muc_domain/rooms` folder contains room configuration files.