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:
parent
65fd49a81c
commit
f97e54d499
@ -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
|
||||||
|
|
||||||
|
@ -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',
|
||||||
|
@ -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
14
package-lock.json
generated
@ -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",
|
||||||
|
@ -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
182
server/lib/bots/ctl.ts
Normal 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
|
||||||
|
}
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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')
|
||||||
|
@ -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)
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user