peertube-plugin-livechat/server/lib/prosody/config.ts

332 lines
11 KiB
TypeScript

import * as fs from 'fs'
import * as path from 'path'
import { getBaseRouterRoute } from '../helpers'
import { ProsodyFilePaths } from './config/paths'
import { ConfigLogExpiration, ProsodyConfigContent } from './config/content'
import { getProsodyDomain } from './config/domain'
import { getAPIKey, getExternalComponentKey } from '../apikey'
import type { ProsodyLogLevel } from './config/content'
import { parseConfigDemoBotUUIDs } from './config/bots'
async function getWorkingDir (options: RegisterServerOptions): Promise<string> {
const peertubeHelpers = options.peertubeHelpers
const logger = peertubeHelpers.logger
logger.debug('Calling getWorkingDir')
if (!peertubeHelpers.plugin) {
throw new Error('Missing peertubeHelpers.plugin, have you the correct Peertube version?')
}
const dir = path.resolve(peertubeHelpers.plugin.getDataDirectoryPath(), 'prosody')
logger.debug('getWorkingDir will return the dir ' + dir)
return dir
}
/**
* Creates the working dir if needed, and returns it.
*/
async function ensureWorkingDirs (options: RegisterServerOptions): Promise<string> {
const logger = options.peertubeHelpers.logger
logger.debug('Calling ensureworkingDirs')
const paths = await getProsodyFilePaths(options)
const dir = paths.dir
if (!fs.existsSync(dir)) {
logger.info(`The working dir ${dir} does not exists, trying to create it`)
await fs.promises.mkdir(dir)
logger.debug(`Working dir ${dir} was created`)
}
logger.debug(`Testing write access on ${dir}`)
await fs.promises.access(dir, fs.constants.W_OK) // will throw an error if no access
logger.debug(`Write access ok on ${dir}`)
for (const path of [paths.data, paths.bots.dir]) {
if (!fs.existsSync(path)) {
logger.info(`The data dir ${path} does not exists, trying to create it`)
await fs.promises.mkdir(path)
logger.debug(`Working dir ${path} was created`)
}
}
return dir
}
async function getProsodyFilePaths (options: RegisterServerOptions): Promise<ProsodyFilePaths> {
const logger = options.peertubeHelpers.logger
logger.debug('Calling getProsodyFilePaths')
const dir = await getWorkingDir(options)
return {
dir: dir,
pid: path.resolve(dir, 'prosody.pid'),
error: path.resolve(dir, 'prosody.err'),
log: path.resolve(dir, 'prosody.log'),
config: path.resolve(dir, 'prosody.cfg.lua'),
data: path.resolve(dir, 'data'),
bots: {
dir: path.resolve(dir, 'bots'),
demobot: path.resolve(dir, 'bots', 'demobot.js')
},
modules: path.resolve(__dirname, '../../prosody-modules')
}
}
interface ProsodyConfigBots {
demobot?: string[] // if the demo bot is activated, here are the video UUIDS where it will be.
}
type ProsodyConfigFilesKey = 'prosody' | 'demobot'
type ProsodyConfigFiles = Array<{
key: ProsodyConfigFilesKey
path: string
content: string
}>
class ProsodyConfig {
constructor (
private readonly configFiles: ProsodyConfigFiles,
public paths: ProsodyFilePaths,
public host: string,
public port: string,
public baseApiUrl: string,
public roomType: 'video' | 'channel',
public logByDefault: boolean,
public logExpiration: ConfigLogExpiration,
public bots: ProsodyConfigBots,
public valuesToHideInDiagnostic: {[key: string]: string}
) {}
public getConfigFiles (): ProsodyConfigFiles {
return this.configFiles
}
public contentForDiagnostic (content: string): string {
let r: string = content
for (const key in this.valuesToHideInDiagnostic) {
// replaceAll not available, using trick:
r = r.split(this.valuesToHideInDiagnostic[key]).join(`***${key}***`)
}
return r
}
}
async function getProsodyConfig (options: RegisterServerOptions): Promise<ProsodyConfig> {
const logger = options.peertubeHelpers.logger
logger.debug('Calling getProsodyConfig')
let useExternalComponents = false
const bots: ProsodyConfigBots = {}
const valuesToHideInDiagnostic: {[key: string]: string} = {}
const settings = await options.settingsManager.getSettings([
'prosody-port',
'prosody-muc-log-by-default',
'prosody-muc-expiration',
'prosody-c2s',
'prosody-room-type',
'prosody-peertube-uri',
'prosody-c2s-port',
'prosody-component-port',
'chat-videos-list'
])
const port = (settings['prosody-port'] as string) || '52800'
if (!/^\d+$/.test(port)) {
throw new Error('Invalid port')
}
const externalComponentsPort = (settings['prosody-component-port'] as string) || '53470'
if (!/^\d+$/.test(externalComponentsPort)) {
throw new Error('Invalid external components port')
}
const logByDefault = (settings['prosody-muc-log-by-default'] as boolean) ?? true
const logExpirationSetting = (settings['prosody-muc-expiration'] as string) ?? DEFAULTLOGEXPIRATION
const enableC2s = (settings['prosody-c2s'] as boolean) || false
const prosodyDomain = await getProsodyDomain(options)
const paths = await getProsodyFilePaths(options)
const roomType = (settings['prosody-room-type']) === 'channel' ? 'channel' : 'video'
const apikey = await getAPIKey(options)
valuesToHideInDiagnostic.APIKey = apikey
let baseApiUrl = settings['prosody-peertube-uri'] as string
if (baseApiUrl && !/^https?:\/\/[a-z0-9.-_]+(?::\d+)?$/.test(baseApiUrl)) {
throw new Error('Invalid prosody-peertube-uri')
}
if (!baseApiUrl) {
baseApiUrl = options.peertubeHelpers.config.getWebserverUrl()
}
baseApiUrl += getBaseRouterRoute(options) + 'api/'
const authApiUrl = baseApiUrl + 'user' // FIXME: should be protected by apikey, but mod_auth_http cant handle params
const roomApiUrl = baseApiUrl + 'room?apikey=' + apikey + '&jid={room.jid|jid_node}'
const testApiUrl = baseApiUrl + 'test?apikey=' + apikey
const config = new ProsodyConfigContent(paths, prosodyDomain)
config.useHttpAuthentication(authApiUrl)
config.usePeertubeBosh(prosodyDomain, port)
config.useMucHttpDefault(roomApiUrl)
if (enableC2s) {
const c2sPort = (settings['prosody-c2s-port'] as string) || '52822'
if (!/^\d+$/.test(c2sPort)) {
throw new Error('Invalid c2s port')
}
config.useC2S(c2sPort)
}
const logExpiration = readLogExpiration(options, logExpirationSetting)
config.useMam(logByDefault, logExpiration)
// TODO: add a settings to choose?
config.useDefaultPersistent()
config.useListRoomsApi(apikey)
config.useTestModule(apikey, testApiUrl)
let logLevel: ProsodyLogLevel | undefined
if (logger.level && (typeof logger.level === 'string')) {
if (logger.level === 'error' || logger.level === 'info' || logger.level === 'debug') {
logLevel = logger.level
} else if (logger.level === 'warn' || logger.level === 'warning') {
// Should be 'warn', but just in case... (this value was buggy with peertube <= 3.2.0-rc1)
logLevel = 'warn'
}
}
if (logLevel === undefined) {
logger.info('No log level found in Peertube, will use default "info" for Prosody')
logLevel = 'info'
}
config.setLog(logLevel)
const demoBotUUIDs = parseConfigDemoBotUUIDs((settings['chat-videos-list'] as string) || '')
let demoBotContentObj: string = 'null'
if (demoBotUUIDs?.length > 0) {
useExternalComponents = true
const componentSecret = await getExternalComponentKey(options, 'DEMOBOT')
valuesToHideInDiagnostic.ComponentSecret = componentSecret
config.useDemoBot(componentSecret)
bots.demobot = demoBotUUIDs
demoBotContentObj = JSON.stringify({
rooms: demoBotUUIDs,
service: 'xmpp://127.0.0.1:' + externalComponentsPort,
domain: 'demobot.' + prosodyDomain,
mucDomain: 'room.' + prosodyDomain,
password: componentSecret
})
}
let demoBotContent = '"use strict";\n'
demoBotContent += 'Object.defineProperty(exports, "__esModule", { value: true });\n'
demoBotContent += `function getConf () { return ${demoBotContentObj}; }` + '\n'
demoBotContent += 'exports.getConf = getConf;\n'
if (useExternalComponents) {
config.useExternalComponents(externalComponentsPort)
}
const content = config.write()
return new ProsodyConfig(
[
{
key: 'prosody',
path: paths.config,
content: content
},
{
key: 'demobot',
path: paths.bots.demobot,
content: demoBotContent
}
],
paths,
prosodyDomain,
port,
baseApiUrl,
roomType,
logByDefault,
logExpiration,
bots,
valuesToHideInDiagnostic
)
}
async function writeProsodyConfig (options: RegisterServerOptions): Promise<ProsodyConfig> {
const logger = options.peertubeHelpers.logger
logger.debug('Calling writeProsodyConfig')
logger.debug('Ensuring that the working dir exists')
await ensureWorkingDirs(options)
logger.debug('Computing the Prosody config content')
const config = await getProsodyConfig(options)
const configFiles = config.getConfigFiles()
for (const configFile of configFiles) {
const content = configFile.content
const fileName = configFile.path
logger.info(`Writing prosody configuration file '${configFile.key}' to ${fileName}.`)
await fs.promises.writeFile(fileName, content)
logger.debug(`Prosody configuration file '${configFile.key}' writen.`)
}
return config
}
const DEFAULTLOGEXPIRATION = '1w'
const DEFAULTLOGEXPIRATIONTYPE = 'period'
function readLogExpiration (options: RegisterServerOptions, logExpiration: string): ConfigLogExpiration {
const logger = options.peertubeHelpers.logger
logExpiration = logExpiration?.trim()
if (logExpiration === 'never') {
return {
value: 'never',
type: 'never'
}
}
if (/^\d+$/.test(logExpiration)) {
if (logExpiration === '0') {
logger.error('Invalid prosody-muc-expiration value, cannot be 0.')
return {
value: DEFAULTLOGEXPIRATION,
type: DEFAULTLOGEXPIRATIONTYPE,
error: '0 is not an acceptable value.'
}
}
return {
value: logExpiration,
type: 'seconds',
seconds: parseInt(logExpiration)
}
}
const matches = logExpiration.match(/^(\d+)([d|w|m|y])$/)
if (matches) {
const d = matches[1]
if (d === '0') {
logger.error(`Invalid prosody-muc-expiration value, cannot be ${logExpiration}.`)
return {
value: DEFAULTLOGEXPIRATION,
type: DEFAULTLOGEXPIRATIONTYPE,
error: '0 is not an acceptable value.'
}
}
return {
value: logExpiration,
type: 'period'
}
}
logger.error(`Invalid prosody-muc-expiration value '${logExpiration}'.`)
return {
value: DEFAULTLOGEXPIRATION,
type: DEFAULTLOGEXPIRATIONTYPE,
error: `Invalid value '${logExpiration}'.`
}
}
export {
getProsodyConfig,
getWorkingDir,
ensureWorkingDirs,
getProsodyFilePaths,
writeProsodyConfig
}