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"
}
},
"@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": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
@ -2792,8 +2894,7 @@
"events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
"dev": true
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="
},
"evp_bytestokey": {
"version": "1.0.3",
@ -4207,6 +4308,11 @@
"integrity": "sha512-h9ivI88e1lFNmTT4HovBN33Ysn0OIJG7IPG2mkpx2uniQXFWqo35QdiX7w0TovlUFXfW8aPFblP5/q0jlOr2sA==",
"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": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz",
@ -4320,6 +4426,11 @@
"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": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",

View File

@ -31,6 +31,7 @@
"dist/assets/style.css"
],
"dependencies": {
"@xmpp/component": "^0.13.0",
"async": "^3.2.2",
"body-parser": "^1.19.0",
"decache": "^4.6.0",
@ -82,6 +83,7 @@
"clean": "rm -rf dist/* build/*",
"clean:light": "rm -rf dist/*",
"prepare": "npm run clean && npm run build",
"build:bots": "npx tsc --build bots/tsconfig.json",
"build:converse": "bash conversejs/build-conversejs.sh",
"build:images": "mkdir -p dist/client/images && npx svgo -f public/images/ -o dist/client/images/",
"build:webpack": "webpack --mode=production",
@ -89,7 +91,7 @@
"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: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:script": "npx eslint --ext .js --ext .ts .",
"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}'`)
if (wantedConfig.bots.demo) {
result.messages.push(`The Demo bot is active for videos: ${wantedConfig.bots.demo.join(', ')}`)
if (wantedConfig.bots.demobot) {
result.messages.push(`The Demo bot is active for videos: ${wantedConfig.bots.demobot.join(', ')}`)
}
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.
*/
async function ensureWorkingDir (options: RegisterServerOptions): Promise<string> {
async function ensureWorkingDirs (options: RegisterServerOptions): Promise<string> {
const logger = options.peertubeHelpers.logger
logger.debug('Calling ensureworkingDir')
logger.debug('Calling ensureworkingDirs')
const paths = await getProsodyFilePaths(options)
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
logger.debug(`Write access ok on ${dir}`)
if (!fs.existsSync(paths.data)) {
logger.info(`The data dir ${paths.data} does not exists, trying to create it`)
await fs.promises.mkdir(paths.data)
logger.debug(`Working dir ${paths.data} was created`)
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
@ -60,16 +62,19 @@ async function getProsodyFilePaths (options: RegisterServerOptions): Promise<Pro
log: path.resolve(dir, 'prosody.log'),
config: path.resolve(dir, 'prosody.cfg.lua'),
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')
}
}
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<{
key: ProsodyConfigFilesKey
path: string
@ -128,6 +133,10 @@ async function getProsodyConfig (options: RegisterServerOptions): Promise<Prosod
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
@ -189,19 +198,27 @@ async function getProsodyConfig (options: RegisterServerOptions): Promise<Prosod
config.setLog(logLevel)
const demoBotUUIDs = parseConfigDemoBotUUIDs((settings['chat-videos-list'] as string) || '')
let demoBotContentObj: string = JSON.stringify({})
if (demoBotUUIDs?.length > 0) {
useExternalComponents = true
const componentSecret = await getExternalComponentKey(options, 'DEMOBOT')
valuesToHideInDiagnostic.ComponentSecret = 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) {
const externalComponentsPort = (settings['prosody-component-port'] as string) || '53470'
if (!/^\d+$/.test(externalComponentsPort)) {
throw new Error('Invalid external components port')
}
config.useExternalComponents(externalComponentsPort)
}
@ -213,6 +230,11 @@ async function getProsodyConfig (options: RegisterServerOptions): Promise<Prosod
key: 'prosody',
path: paths.config,
content: content
},
{
key: 'demobot',
path: paths.bots.demobot,
content: demoBotContent
}
],
paths,
@ -232,7 +254,7 @@ async function writeProsodyConfig (options: RegisterServerOptions): Promise<Pros
logger.debug('Calling writeProsodyConfig')
logger.debug('Ensuring that the working dir exists')
await ensureWorkingDir(options)
await ensureWorkingDirs(options)
logger.debug('Computing the Prosody config content')
const config = await getProsodyConfig(options)
@ -303,7 +325,7 @@ function readLogExpiration (options: RegisterServerOptions, logExpiration: strin
export {
getProsodyConfig,
getWorkingDir,
ensureWorkingDir,
ensureWorkingDirs,
getProsodyFilePaths,
writeProsodyConfig
}

View File

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

View File

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