Demo Bot: first proof of concept.

This commit is contained in:
John Livingston 2021-12-07 13:14:01 +01:00
parent f8ce4e6583
commit 978ee83eee
No known key found for this signature in database
GPG Key ID: B17B5640CE66CDBC
9 changed files with 307 additions and 25 deletions

40
bots/.eslintrc.json Normal file
View File

@ -0,0 +1,40 @@
{
"root": true,
"env": {
"browser": false,
"es6": true
},
"extends": [
"standard-with-typescript"
],
"globals": {},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2018,
"project": [
"./bots/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"
}
}

78
bots/bots.ts Normal file
View File

@ -0,0 +1,78 @@
import * as path from 'path'
let demoBotConfigFile = process.argv[2]
if (!demoBotConfigFile) {
throw new Error('Missing parameter: the demobot configuration file path')
}
demoBotConfigFile = path.resolve(demoBotConfigFile)
// Not necessary, but just in case: perform some path checking...
function checkBotConfigFilePath (configPath: string): void {
const parts = configPath.split(path.sep)
if (!parts.includes('peertube-plugin-livechat')) {
// Indeed, the path should contain the plugin name
// (/var/www/peertube/storage/plugins/data/peertube-plugin-livechat/...)
throw new Error('demobot configuration file path seems invalid (not in peertube-plugin-livechat folder).')
}
if (parts[parts.length - 1] !== 'demobot.js') {
throw new Error('demobot configuration file path seems invalid (filename is not demobot.js).')
}
}
checkBotConfigFilePath(demoBotConfigFile)
const demoBotConf = require(demoBotConfigFile).getConf()
if (!demoBotConf || !demoBotConf.UUIDs || !demoBotConf.UUIDs.length) {
process.exit(0)
}
const { component, xml } = require('@xmpp/component')
const xmpp = component({
service: demoBotConf.service,
domain: demoBotConf.domain,
password: demoBotConf.password
})
const roomId = `${demoBotConf.UUIDs[0] as string}@${demoBotConf.mucDomain as string}`
xmpp.on('error', (err: any) => {
console.error(err)
})
xmpp.on('offline', () => {
console.log('offline')
})
xmpp.on('stanza', async (stanza: any) => {
console.log('stanza received' + (stanza?.toString ? ': ' + (stanza.toString() as string) : ''))
// if (stanza.is('message')) {
// console.log('stanza was a message: ' + (stanza.toString() as string))
// }
})
xmpp.on('online', async (address: any) => {
console.log('Online with address: ' + JSON.stringify(address))
const presence = xml(
'presence',
{
from: address.toString(),
to: roomId + '/DemoBot'
},
xml('x', {
xmlns: 'http://jabber.org/protocol/muc'
})
)
console.log('Sending presence...: ' + (presence.toString() as string))
await xmpp.send(presence)
setTimeout(() => {
const message = xml(
'message',
{ type: 'groupchat', to: roomId, from: address.toString() },
xml('body', {}, 'Hello world')
)
console.log('Sending message...: ' + (message.toString() as string))
xmpp.send(message)
}, 1000)
})
xmpp.start().catch(console.error)

26
bots/tsconfig.json Normal file
View File

@ -0,0 +1,26 @@
{
"extends": "@tsconfig/node12/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,
"strictBindCallApply": true, // should already be true because of strict:true
"noUnusedLocals": true,
"removeComments": true,
"sourceMap": true,
"baseUrl": "./",
"outDir": "../dist/",
"paths": {}
},
"include": [
"./**/*",
"../shared/**/*"
],
"exclude": []
}

115
package-lock.json generated
View File

@ -690,6 +690,108 @@
"@xtuc/long": "4.2.2" "@xtuc/long": "4.2.2"
} }
}, },
"@xmpp/component": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@xmpp/component/-/component-0.13.0.tgz",
"integrity": "sha512-xl2dCJiM7GH98ncdU86JjiKzGfP7ykTJZW6iSKiAaniUKDRixLDMaKM/X0CR+4sXm3rqvRUTYyzndCmCi8CUpg==",
"requires": {
"@xmpp/component-core": "^0.13.0",
"@xmpp/iq": "^0.13.0",
"@xmpp/middleware": "^0.13.0",
"@xmpp/reconnect": "^0.13.0"
}
},
"@xmpp/component-core": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@xmpp/component-core/-/component-core-0.13.0.tgz",
"integrity": "sha512-/stz9Eo11Q79z1lJ0yWNv0FsSf8AAYko6ctRjHRlHEGkLhQDw959v4k5eB82YrtApoHLoHCxtJMxDwwWAtlprA==",
"requires": {
"@xmpp/connection-tcp": "^0.13.0",
"@xmpp/jid": "^0.13.0",
"@xmpp/xml": "^0.13.0"
}
},
"@xmpp/connection": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@xmpp/connection/-/connection-0.13.0.tgz",
"integrity": "sha512-8aLM+XsHYfI/Q7DsOAClEgA825eHIztCZVP4z+diAYuyhyN1P0e4en1dQjK7QOVvOg+DsA8qTcZ8C0b3pY7EFw==",
"requires": {
"@xmpp/error": "^0.13.0",
"@xmpp/events": "^0.13.0",
"@xmpp/jid": "^0.13.0",
"@xmpp/xml": "^0.13.0"
}
},
"@xmpp/connection-tcp": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@xmpp/connection-tcp/-/connection-tcp-0.13.0.tgz",
"integrity": "sha512-qsP+/ILYWA6D5MrZfS/7nNtaO469EAPAJ7P9gNA9hj5ZOu5mX6LwGecSBegpnXXP5b378iSlqOLskkVDUmSahg==",
"requires": {
"@xmpp/connection": "^0.13.0",
"@xmpp/xml": "^0.13.0"
}
},
"@xmpp/error": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@xmpp/error/-/error-0.13.0.tgz",
"integrity": "sha512-cTyGMrXzuEulRiG29vvHhaU0vTpOxDQS49dyUAW+2Rj5ex9OXXGiWWbJDodEO9B/rHiUXr1U63818Yv4lxZJBA=="
},
"@xmpp/events": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@xmpp/events/-/events-0.13.0.tgz",
"integrity": "sha512-G+9NczMWWOawn62r1JIv/N413G2biI+hURiN4iH74FGvjagXwassUeJgPnDUEFp2FTKX3dIrJDkXH49ZcFuo/g==",
"requires": {
"events": "^3.3.0"
}
},
"@xmpp/id": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@xmpp/id/-/id-0.13.0.tgz",
"integrity": "sha512-6m9KAreJ13/FnonnLCeK1a6jJx8PqpdLZfRWxUfQu1Wg4nAlgYrcDSYny+/BUm5ICkAEILjvBtOh/EmJ3wMNmA=="
},
"@xmpp/iq": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@xmpp/iq/-/iq-0.13.0.tgz",
"integrity": "sha512-3fH7lLIgQ4I/I9nKst+YqFP4WIjV24TVnTDxGQthj7POkmvl2MFo63rlTvA4PV1uRn8FmlyetgP/vbGo+c7yuQ==",
"requires": {
"@xmpp/events": "^0.13.0",
"@xmpp/id": "^0.13.0",
"@xmpp/middleware": "^0.13.0",
"@xmpp/xml": "^0.13.0"
}
},
"@xmpp/jid": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@xmpp/jid/-/jid-0.13.0.tgz",
"integrity": "sha512-R8XkQOLK7V+wDiXozc9VzoACb4+XPR6K8zno1fur9le7AnUrX/vUvb8/ZcvenFNWVYplvZS6h9GkZPPEGvmUyQ=="
},
"@xmpp/middleware": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@xmpp/middleware/-/middleware-0.13.0.tgz",
"integrity": "sha512-ZUaArnur2q74nTvwbBckflsxGo73VqEBKk/GaQv0q9Lgg6FjQO/BA6lTlZ597h3V5MBi7SGHPcJ335p1/Rd0uw==",
"requires": {
"@xmpp/error": "^0.13.0",
"@xmpp/jid": "^0.13.0",
"@xmpp/xml": "^0.13.0",
"koa-compose": "^4.1.0"
}
},
"@xmpp/reconnect": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@xmpp/reconnect/-/reconnect-0.13.0.tgz",
"integrity": "sha512-I0uxzGb6Mr6QlCPjgIGb8eBbPYJc2FauOfpoZ/O7Km+i41MxLmVyNaKP0aY2JhWIxls727X9VMMtjTlK8vE5RQ==",
"requires": {
"@xmpp/events": "^0.13.0"
}
},
"@xmpp/xml": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@xmpp/xml/-/xml-0.13.0.tgz",
"integrity": "sha512-bgKaUzzJXp8nXCQPzVRJLy1XZQlLrcmjzUe1V7127NcXJddEgk1Ie/esVhh1BUMlPgRdl7BCRQkYe40S6KuuXw==",
"requires": {
"ltx": "^3.0.0"
}
},
"@xtuc/ieee754": { "@xtuc/ieee754": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
@ -2792,8 +2894,7 @@
"events": { "events": {
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="
"dev": true
}, },
"evp_bytestokey": { "evp_bytestokey": {
"version": "1.0.3", "version": "1.0.3",
@ -4207,6 +4308,11 @@
"integrity": "sha512-h9ivI88e1lFNmTT4HovBN33Ysn0OIJG7IPG2mkpx2uniQXFWqo35QdiX7w0TovlUFXfW8aPFblP5/q0jlOr2sA==", "integrity": "sha512-h9ivI88e1lFNmTT4HovBN33Ysn0OIJG7IPG2mkpx2uniQXFWqo35QdiX7w0TovlUFXfW8aPFblP5/q0jlOr2sA==",
"dev": true "dev": true
}, },
"koa-compose": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz",
"integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw=="
},
"kuler": { "kuler": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz",
@ -4320,6 +4426,11 @@
"yallist": "^4.0.0" "yallist": "^4.0.0"
} }
}, },
"ltx": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ltx/-/ltx-3.0.0.tgz",
"integrity": "sha512-bu3/4/ApUmMqVNuIkHaRhqVtEi6didYcBDIF56xhPRCzVpdztCipZ62CUuaxMlMBUzaVL93+4LZRqe02fuAG6A=="
},
"make-dir": { "make-dir": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",

View File

@ -31,6 +31,7 @@
"dist/assets/style.css" "dist/assets/style.css"
], ],
"dependencies": { "dependencies": {
"@xmpp/component": "^0.13.0",
"async": "^3.2.2", "async": "^3.2.2",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"decache": "^4.6.0", "decache": "^4.6.0",
@ -82,6 +83,7 @@
"clean": "rm -rf dist/* build/*", "clean": "rm -rf dist/* build/*",
"clean:light": "rm -rf dist/*", "clean:light": "rm -rf dist/*",
"prepare": "npm run clean && npm run build", "prepare": "npm run clean && npm run build",
"build:bots": "npx tsc --build bots/tsconfig.json",
"build:converse": "bash conversejs/build-conversejs.sh", "build:converse": "bash conversejs/build-conversejs.sh",
"build:images": "mkdir -p dist/client/images && npx svgo -f public/images/ -o dist/client/images/", "build:images": "mkdir -p dist/client/images && npx svgo -f public/images/ -o dist/client/images/",
"build:webpack": "webpack --mode=production", "build:webpack": "webpack --mode=production",
@ -89,7 +91,7 @@
"build:serverconverse": "mkdir -p dist/server/conversejs && cp conversejs/index.html dist/server/conversejs/", "build:serverconverse": "mkdir -p dist/server/conversejs && cp conversejs/index.html dist/server/conversejs/",
"build:prosodymodules": "mkdir -p dist/server/prosody-modules && cp -r prosody-modules/* dist/server/prosody-modules/", "build:prosodymodules": "mkdir -p dist/server/prosody-modules && cp -r prosody-modules/* dist/server/prosody-modules/",
"build:styles": "sass assets:dist/assets", "build:styles": "sass assets:dist/assets",
"build": "npm-run-all -s clean:light -p build:converse build:images build:webpack build:server build:serverconverse build:prosodymodules build:styles", "build": "npm-run-all -s clean:light -p build:converse build:images build:webpack build:server build:serverconverse build:prosodymodules build:styles build:bots",
"lint": "npm-run-all -s lint:script lint:styles", "lint": "npm-run-all -s lint:script lint:styles",
"lint:script": "npx eslint --ext .js --ext .ts .", "lint:script": "npx eslint --ext .js --ext .ts .",
"lint:styles": "stylelint 'conversejs/**/*.scss' 'assets/**/*.css'", "lint:styles": "stylelint 'conversejs/**/*.scss' 'assets/**/*.css'",

View File

@ -49,8 +49,8 @@ export async function diagProsody (test: string, options: RegisterServerOptions)
} }
result.messages.push(`Room content will be saved for '${wantedConfig.logExpiration.value}'`) result.messages.push(`Room content will be saved for '${wantedConfig.logExpiration.value}'`)
if (wantedConfig.bots.demo) { if (wantedConfig.bots.demobot) {
result.messages.push(`The Demo bot is active for videos: ${wantedConfig.bots.demo.join(', ')}`) result.messages.push(`The Demo bot is active for videos: ${wantedConfig.bots.demobot.join(', ')}`)
} }
const configFiles = wantedConfig.getConfigFiles() const configFiles = wantedConfig.getConfigFiles()

View File

@ -24,9 +24,9 @@ async function getWorkingDir (options: RegisterServerOptions): Promise<string> {
/** /**
* Creates the working dir if needed, and returns it. * Creates the working dir if needed, and returns it.
*/ */
async function ensureWorkingDir (options: RegisterServerOptions): Promise<string> { async function ensureWorkingDirs (options: RegisterServerOptions): Promise<string> {
const logger = options.peertubeHelpers.logger const logger = options.peertubeHelpers.logger
logger.debug('Calling ensureworkingDir') logger.debug('Calling ensureworkingDirs')
const paths = await getProsodyFilePaths(options) const paths = await getProsodyFilePaths(options)
const dir = paths.dir const dir = paths.dir
@ -39,10 +39,12 @@ async function ensureWorkingDir (options: RegisterServerOptions): Promise<string
await fs.promises.access(dir, fs.constants.W_OK) // will throw an error if no access await fs.promises.access(dir, fs.constants.W_OK) // will throw an error if no access
logger.debug(`Write access ok on ${dir}`) logger.debug(`Write access ok on ${dir}`)
if (!fs.existsSync(paths.data)) { for (const path of [paths.data, paths.bots.dir]) {
logger.info(`The data dir ${paths.data} does not exists, trying to create it`) if (!fs.existsSync(path)) {
await fs.promises.mkdir(paths.data) logger.info(`The data dir ${path} does not exists, trying to create it`)
logger.debug(`Working dir ${paths.data} was created`) await fs.promises.mkdir(path)
logger.debug(`Working dir ${path} was created`)
}
} }
return dir return dir
@ -60,16 +62,19 @@ async function getProsodyFilePaths (options: RegisterServerOptions): Promise<Pro
log: path.resolve(dir, 'prosody.log'), log: path.resolve(dir, 'prosody.log'),
config: path.resolve(dir, 'prosody.cfg.lua'), config: path.resolve(dir, 'prosody.cfg.lua'),
data: path.resolve(dir, 'data'), data: path.resolve(dir, 'data'),
bots: path.resolve(dir, 'bots'), bots: {
dir: path.resolve(dir, 'bots'),
demobot: path.resolve(dir, 'bots', 'demobot.js')
},
modules: path.resolve(__dirname, '../../prosody-modules') modules: path.resolve(__dirname, '../../prosody-modules')
} }
} }
interface ProsodyConfigBots { interface ProsodyConfigBots {
demo?: string[] // if the demo bot is activated, here are the video UUIDS where it will be. demobot?: string[] // if the demo bot is activated, here are the video UUIDS where it will be.
} }
type ProsodyConfigFilesKey = 'prosody' type ProsodyConfigFilesKey = 'prosody' | 'demobot'
type ProsodyConfigFiles = Array<{ type ProsodyConfigFiles = Array<{
key: ProsodyConfigFilesKey key: ProsodyConfigFilesKey
path: string path: string
@ -128,6 +133,10 @@ async function getProsodyConfig (options: RegisterServerOptions): Promise<Prosod
if (!/^\d+$/.test(port)) { if (!/^\d+$/.test(port)) {
throw new Error('Invalid 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 logByDefault = (settings['prosody-muc-log-by-default'] as boolean) ?? true
const logExpirationSetting = (settings['prosody-muc-expiration'] as string) ?? DEFAULTLOGEXPIRATION const logExpirationSetting = (settings['prosody-muc-expiration'] as string) ?? DEFAULTLOGEXPIRATION
const enableC2s = (settings['prosody-c2s'] as boolean) || false const enableC2s = (settings['prosody-c2s'] as boolean) || false
@ -189,19 +198,27 @@ async function getProsodyConfig (options: RegisterServerOptions): Promise<Prosod
config.setLog(logLevel) config.setLog(logLevel)
const demoBotUUIDs = parseConfigDemoBotUUIDs((settings['chat-videos-list'] as string) || '') const demoBotUUIDs = parseConfigDemoBotUUIDs((settings['chat-videos-list'] as string) || '')
let demoBotContentObj: string = JSON.stringify({})
if (demoBotUUIDs?.length > 0) { if (demoBotUUIDs?.length > 0) {
useExternalComponents = true useExternalComponents = true
const componentSecret = await getExternalComponentKey(options, 'DEMOBOT') const componentSecret = await getExternalComponentKey(options, 'DEMOBOT')
valuesToHideInDiagnostic.ComponentSecret = componentSecret valuesToHideInDiagnostic.ComponentSecret = componentSecret
config.useDemoBot(componentSecret) config.useDemoBot(componentSecret)
bots.demo = demoBotUUIDs bots.demobot = demoBotUUIDs
demoBotContentObj = JSON.stringify({
UUIDs: 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) { if (useExternalComponents) {
const externalComponentsPort = (settings['prosody-component-port'] as string) || '53470'
if (!/^\d+$/.test(externalComponentsPort)) {
throw new Error('Invalid external components port')
}
config.useExternalComponents(externalComponentsPort) config.useExternalComponents(externalComponentsPort)
} }
@ -213,6 +230,11 @@ async function getProsodyConfig (options: RegisterServerOptions): Promise<Prosod
key: 'prosody', key: 'prosody',
path: paths.config, path: paths.config,
content: content content: content
},
{
key: 'demobot',
path: paths.bots.demobot,
content: demoBotContent
} }
], ],
paths, paths,
@ -232,7 +254,7 @@ async function writeProsodyConfig (options: RegisterServerOptions): Promise<Pros
logger.debug('Calling writeProsodyConfig') logger.debug('Calling writeProsodyConfig')
logger.debug('Ensuring that the working dir exists') logger.debug('Ensuring that the working dir exists')
await ensureWorkingDir(options) await ensureWorkingDirs(options)
logger.debug('Computing the Prosody config content') logger.debug('Computing the Prosody config content')
const config = await getProsodyConfig(options) const config = await getProsodyConfig(options)
@ -303,7 +325,7 @@ function readLogExpiration (options: RegisterServerOptions, logExpiration: strin
export { export {
getProsodyConfig, getProsodyConfig,
getWorkingDir, getWorkingDir,
ensureWorkingDir, ensureWorkingDirs,
getProsodyFilePaths, getProsodyFilePaths,
writeProsodyConfig writeProsodyConfig
} }

View File

@ -291,13 +291,13 @@ class ProsodyConfigContent {
} }
useDemoBot (componentSecret: string): void { useDemoBot (componentSecret: string): void {
const demoBot = new ProsodyConfigComponent('demobot.' + this.prosodyDomain) const demoBotComponent = new ProsodyConfigComponent('demobot.' + this.prosodyDomain)
demoBot.set('component_secret', componentSecret) demoBotComponent.set('component_secret', componentSecret)
// If we want the bot to be moderator, should do the trick: // If we want the bot to be moderator, should do the trick:
// this.global.add('admins', 'demobot.' + this.prosodyDomain) // this.global.add('admins', 'demobot.' + this.prosodyDomain)
this.externalComponents.push(demoBot) this.externalComponents.push(demoBotComponent)
} }
setLog (level: ProsodyLogLevel, syslog?: ProsodyLogLevel[]): void { setLog (level: ProsodyLogLevel, syslog?: ProsodyLogLevel[]): void {

View File

@ -5,7 +5,10 @@ interface ProsodyFilePaths {
log: string log: string
config: string config: string
data: string data: string
bots: string bots: {
dir: string
demobot: string
}
modules: string modules: string
} }