peertube-plugin-livechat/server/lib/bots/ctl.ts

192 lines
5.9 KiB
TypeScript
Raw Normal View History

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: {
2023-09-19 13:54:56 +00:00
...process.env, // will include NODE_ENV and co
NODE_TLS_REJECT_UNAUTHORIZED: '0' // Prosody use self-signed certificates, the bot must accept themp
}
})
moderationBotProcess.stdout?.on('data', (data) => {
this.logger.debug(`ModerationBot stdout: ${data as string}`)
})
moderationBotProcess.stderr?.on('data', (data) => {
2023-09-19 14:27:49 +00:00
// change error level for non-relevant errors:
data = data.toString()
if (/Warning.*NODE_TLS_REJECT_UNAUTHORIZED.*'0'.*TLS/.test(data)) {
this.logger.debug(`ModerationBot stderr: ${data as string}`)
return
}
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
2023-09-19 13:54:56 +00:00
// Trying to kill, and force kill if it takes more than 1 seconds
const timeout = setTimeout(() => {
2023-09-19 13:54:56 +00:00
try {
this.logger.error('Moderation bot was not killed within 1 seconds, force killing')
moderationBotProcess.kill('SIGKILL')
} catch (_err) {}
resolved = true
resolve()
2023-09-19 13:54:56 +00:00
}, 1000)
moderationBotProcess.on('exit', () => {
if (resolved) { return }
resolved = true
resolve()
2023-09-19 13:54:56 +00:00
if (timeout) { clearTimeout(timeout) }
})
moderationBotProcess.on('close', () => {
if (resolved) { return }
resolved = true
resolve()
2023-09-19 13:54:56 +00:00
if (timeout) { clearTimeout(timeout) }
})
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
}