From 62456aead12391eaec4f904a861971bb62d04f4c Mon Sep 17 00:00:00 2001 From: John Livingston Date: Tue, 7 Dec 2021 10:29:20 +0100 Subject: [PATCH 01/20] Bots settings (WIP). --- client/admin-plugin-client-plugin.ts | 1 + server/lib/apikey.ts | 23 +++++++++--- server/lib/diagnostic/prosody.ts | 4 ++ server/lib/prosody/config.ts | 56 +++++++++++++++++++++++----- server/lib/prosody/config/bots.ts | 19 ++++++++++ server/lib/prosody/config/content.ts | 32 ++++++++++++++-- server/lib/settings.ts | 16 ++++++++ 7 files changed, 132 insertions(+), 19 deletions(-) create mode 100644 server/lib/prosody/config/bots.ts diff --git a/client/admin-plugin-client-plugin.ts b/client/admin-plugin-client-plugin.ts index 0576420c..7ba4e4cc 100644 --- a/client/admin-plugin-client-plugin.ts +++ b/client/admin-plugin-client-plugin.ts @@ -211,6 +211,7 @@ function register ({ registerHook, registerSettingsScript, peertubeHelpers }: Re case 'prosody-muc-log-by-default': case 'prosody-muc-expiration': case 'prosody-c2s': + case 'prosody-component-port': return options.formValues['chat-type'] !== ('builtin-prosody' as ChatType) case 'prosody-c2s-port': return !( diff --git a/server/lib/apikey.ts b/server/lib/apikey.ts index 9d5e6047..6b217412 100644 --- a/server/lib/apikey.ts +++ b/server/lib/apikey.ts @@ -3,15 +3,28 @@ For internal API, we will generate an api Key that must be provided as GET parameter for every API call. */ -async function getAPIKey ({ storageManager }: RegisterServerOptions): Promise { - let value: string = await storageManager.getData('APIKEY') +async function _getKey ({ storageManager }: RegisterServerOptions, key: string): Promise { + let value: string = await storageManager.getData(key) if (!value) { value = Math.random().toString(36).slice(2, 12) - await storageManager.storeData('APIKEY', value) + await storageManager.storeData(key, value) } return value } -export { - getAPIKey +async function getAPIKey (options: RegisterServerOptions): Promise { + return _getKey(options, 'APIKEY') +} + +async function getExternalComponentKey (options: RegisterServerOptions, componentName: string): Promise { + if (!/^[A-Z]+$/.test(componentName)) { + throw new Error('Invalid component name: ' + componentName) + } + const key = 'EXTERNALCOMPONENTKEY_' + componentName + return _getKey(options, key) +} + +export { + getAPIKey, + getExternalComponentKey } diff --git a/server/lib/diagnostic/prosody.ts b/server/lib/diagnostic/prosody.ts index 6488e07f..57252004 100644 --- a/server/lib/diagnostic/prosody.ts +++ b/server/lib/diagnostic/prosody.ts @@ -50,6 +50,10 @@ 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(', ')}`) + } + await fs.promises.access(filePath, fs.constants.R_OK) // throw an error if file does not exist. result.messages.push(`The prosody configuration file (${filePath}) exists`) const actualContent = await fs.promises.readFile(filePath, { diff --git a/server/lib/prosody/config.ts b/server/lib/prosody/config.ts index 577b0c7f..eddd52bd 100644 --- a/server/lib/prosody/config.ts +++ b/server/lib/prosody/config.ts @@ -4,8 +4,9 @@ import { getBaseRouterRoute } from '../helpers' import { ProsodyFilePaths } from './config/paths' import { ConfigLogExpiration, ProsodyConfigContent } from './config/content' import { getProsodyDomain } from './config/domain' -import { getAPIKey } from '../apikey' +import { getAPIKey, getExternalComponentKey } from '../apikey' import type { ProsodyLogLevel } from './config/content' +import { parseConfigDemoBotUUIDs } from './config/bots' async function getWorkingDir (options: RegisterServerOptions): Promise { const peertubeHelpers = options.peertubeHelpers @@ -63,6 +64,9 @@ async function getProsodyFilePaths (options: RegisterServerOptions): Promise { const logger = options.peertubeHelpers.logger logger.debug('Calling getProsodyConfig') - const port = (await options.settingsManager.getSetting('prosody-port') as string) || '52800' + let useExternalComponents = false + const bots: ProsodyConfigBots = {} + + 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 logByDefault = (await options.settingsManager.getSetting('prosody-muc-log-by-default') as boolean) ?? true - const logExpirationSetting = - (await options.settingsManager.getSetting('prosody-muc-expiration') as string) ?? DEFAULTLOGEXPIRATION - const enableC2s = (await options.settingsManager.getSetting('prosody-c2s') as boolean) || false + 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 = (await options.settingsManager.getSetting('prosody-room-type')) === 'channel' ? 'channel' : 'video' + const roomType = (settings['prosody-room-type']) === 'channel' ? 'channel' : 'video' const apikey = await getAPIKey(options) - let baseApiUrl = await options.settingsManager.getSetting('prosody-peertube-uri') as string + let baseApiUrl = settings['prosody-peertube-uri'] as string if (baseApiUrl && !/^https?:\/\/[a-z0-9.-_]+(?::\d+)?$/.test(baseApiUrl)) { throw new Error('Invalid prosody-peertube-uri') } @@ -109,7 +128,7 @@ async function getProsodyConfig (options: RegisterServerOptions): Promise 0) { + useExternalComponents = true + config.useDemoBot(await getExternalComponentKey(options, 'DEMOBOT')) + bots.demo = demoBotUUIDs + } + + 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) + } + const content = config.write() return { @@ -149,7 +184,8 @@ async function getProsodyConfig (options: RegisterServerOptions): Promise /#!demobot\b/.test(line)) + a = a.map(line => { + return line.replace(/#.*$/, '') + .replace(/^\s+/, '') + .replace(/\s+$/, '') + }) + return a.filter(line => line !== '') +} + +export { + parseConfigDemoBotUUIDs +} diff --git a/server/lib/prosody/config/content.ts b/server/lib/prosody/config/content.ts index da2e18c1..230a5d47 100644 --- a/server/lib/prosody/config/content.ts +++ b/server/lib/prosody/config/content.ts @@ -102,16 +102,19 @@ class ProsodyConfigVirtualHost extends ProsodyConfigBlock { class ProsodyConfigComponent extends ProsodyConfigBlock { name: string - type: string + type?: string - constructor (type: string, name: string) { + constructor (name: string, type?: string) { super(' ') this.type = type this.name = name } write (): string { - return `Component "${this.name}" "${this.type}"\n` + super.write() + if (this.type !== undefined) { + return `Component "${this.name}" "${this.type}"\n` + super.write() + } + return `Component "${this.name}"\n` + super.write() } } @@ -123,6 +126,7 @@ class ProsodyConfigContent { authenticated?: ProsodyConfigVirtualHost anon: ProsodyConfigVirtualHost muc: ProsodyConfigComponent + externalComponents: ProsodyConfigComponent[] = [] log: string prosodyDomain: string @@ -132,7 +136,7 @@ class ProsodyConfigContent { this.log = '' this.prosodyDomain = prosodyDomain this.anon = new ProsodyConfigVirtualHost('anon.' + prosodyDomain) - this.muc = new ProsodyConfigComponent('muc', 'room.' + prosodyDomain) + this.muc = new ProsodyConfigComponent('room.' + prosodyDomain, 'muc') this.global.set('daemonize', false) this.global.set('allow_registration', false) @@ -281,6 +285,21 @@ class ProsodyConfigContent { this.muc.set('peertubelivechat_test_peertube_api_url', apiurl) } + useExternalComponents (componentsPort: string): void { + this.global.set('component_ports', [componentsPort]) + this.global.set('component_interfaces', ['127.0.0.1', '::1']) + } + + useDemoBot (componentSecret: string): void { + const demoBot = new ProsodyConfigComponent('demobot.' + this.prosodyDomain) + demoBot.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) + } + setLog (level: ProsodyLogLevel, syslog?: ProsodyLogLevel[]): void { let log = '' log += 'log = {\n' @@ -309,6 +328,11 @@ class ProsodyConfigContent { content += '\n\n' content += this.muc.write() content += '\n\n' + this.externalComponents.forEach((externalComponent) => { + content += '\n\n' + content += externalComponent.write() + content += '\n\n' + }) return content } } diff --git a/server/lib/settings.ts b/server/lib/settings.ts index 703fc7e3..857cdc7a 100644 --- a/server/lib/settings.ts +++ b/server/lib/settings.ts @@ -359,6 +359,22 @@ archiving for a specific room, by editing its properties. ` }) + registerSetting({ + name: 'prosody-component-port', + label: 'The port to be use for external components', + type: 'input', + default: '53470', + private: true, + descriptionHTML: +`The port that will be used for extra components used by the builtin Prosody server.
+This is only used when one of these special features is used:
+
    +
  • Demo bot: this is a hidden feature, for demonstration purposes. See the documentation for more information.
  • +

+Change it if this port is already in use on your server. +` + }) + registerSetting({ name: 'prosody-c2s', label: 'Enable client to server connections', From 2244ae22c278272ddccc4d4ada9c4d3c9db0f56e Mon Sep 17 00:00:00 2001 From: John Livingston Date: Tue, 7 Dec 2021 10:50:28 +0100 Subject: [PATCH 02/20] Prosody diagnostic: hidden secret keys from the result. --- server/lib/diagnostic/prosody.ts | 10 +++++++--- server/lib/prosody/config.ts | 23 ++++++++++++++++++++--- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/server/lib/diagnostic/prosody.ts b/server/lib/diagnostic/prosody.ts index 57252004..5ba10e49 100644 --- a/server/lib/diagnostic/prosody.ts +++ b/server/lib/diagnostic/prosody.ts @@ -1,4 +1,4 @@ -import { getProsodyConfig, getWorkingDir } from '../prosody/config' +import { getProsodyConfig, getProsodyConfigContentForDiagnostic, getWorkingDir } from '../prosody/config' import { getProsodyAbout, testProsodyCorrectlyRunning } from '../prosody/ctl' import { newResult, TestResult } from './utils' import { getAPIKey } from '../apikey' @@ -62,7 +62,10 @@ export async function diagProsody (test: string, options: RegisterServerOptions) result.debug.push({ title: 'Current prosody configuration', - message: actualContent + // we have to hide secret keys and other values. + // But here, we haven't them for actualContent. + // So we will use values in wantedConfig, hopping it is enough. + message: getProsodyConfigContentForDiagnostic(wantedConfig, actualContent) }) const wantedContent = wantedConfig.content @@ -72,7 +75,8 @@ export async function diagProsody (test: string, options: RegisterServerOptions) result.messages.push('Prosody configuration file content is not correct.') result.debug.push({ title: 'Prosody configuration should be', - message: wantedContent + // we have to hide secret keys and other values: + message: getProsodyConfigContentForDiagnostic(wantedConfig) }) return result } diff --git a/server/lib/prosody/config.ts b/server/lib/prosody/config.ts index eddd52bd..cc5a30df 100644 --- a/server/lib/prosody/config.ts +++ b/server/lib/prosody/config.ts @@ -77,6 +77,7 @@ interface ProsodyConfig { logByDefault: boolean logExpiration: ConfigLogExpiration bots: ProsodyConfigBots + valuesToHideInDiagnostic: {[key: string]: string} } async function getProsodyConfig (options: RegisterServerOptions): Promise { const logger = options.peertubeHelpers.logger @@ -84,6 +85,7 @@ async function getProsodyConfig (options: RegisterServerOptions): Promise 0) { useExternalComponents = true - config.useDemoBot(await getExternalComponentKey(options, 'DEMOBOT')) + const componentSecret = await getExternalComponentKey(options, 'DEMOBOT') + valuesToHideInDiagnostic.ComponentSecret = componentSecret + config.useDemoBot(componentSecret) bots.demo = demoBotUUIDs } @@ -185,7 +191,8 @@ async function getProsodyConfig (options: RegisterServerOptions): Promise Date: Tue, 7 Dec 2021 11:59:32 +0100 Subject: [PATCH 03/20] Config: preparing the arrival of new config files (for bots). Code refactoring. --- server/lib/diagnostic/prosody.ts | 57 +++++++++++--------- server/lib/prosody/config.ts | 85 +++++++++++++++++++----------- server/lib/prosody/config/paths.ts | 1 + server/lib/prosody/ctl.ts | 27 +++++----- 4 files changed, 102 insertions(+), 68 deletions(-) diff --git a/server/lib/diagnostic/prosody.ts b/server/lib/diagnostic/prosody.ts index 5ba10e49..d4bfc0ef 100644 --- a/server/lib/diagnostic/prosody.ts +++ b/server/lib/diagnostic/prosody.ts @@ -1,4 +1,4 @@ -import { getProsodyConfig, getProsodyConfigContentForDiagnostic, getWorkingDir } from '../prosody/config' +import { getProsodyConfig, getWorkingDir } from '../prosody/config' import { getProsodyAbout, testProsodyCorrectlyRunning } from '../prosody/ctl' import { newResult, TestResult } from './utils' import { getAPIKey } from '../apikey' @@ -24,7 +24,6 @@ export async function diagProsody (test: string, options: RegisterServerOptions) let prosodyHost: string try { const wantedConfig = await getProsodyConfig(options) - const filePath = wantedConfig.paths.config result.messages.push(`Prosody will run on port '${wantedConfig.port}'`) prosodyPort = wantedConfig.port @@ -54,34 +53,40 @@ export async function diagProsody (test: string, options: RegisterServerOptions) result.messages.push(`The Demo bot is active for videos: ${wantedConfig.bots.demo.join(', ')}`) } - await fs.promises.access(filePath, fs.constants.R_OK) // throw an error if file does not exist. - result.messages.push(`The prosody configuration file (${filePath}) exists`) - const actualContent = await fs.promises.readFile(filePath, { - encoding: 'utf-8' - }) + const configFiles = wantedConfig.getConfigFiles() + for (const configFile of configFiles) { + const filePath = configFile.path + const configFileKey = configFile.key - result.debug.push({ - title: 'Current prosody configuration', - // we have to hide secret keys and other values. - // But here, we haven't them for actualContent. - // So we will use values in wantedConfig, hopping it is enough. - message: getProsodyConfigContentForDiagnostic(wantedConfig, actualContent) - }) - - const wantedContent = wantedConfig.content - if (actualContent === wantedContent) { - result.messages.push('Prosody configuration file content is correct.') - } else { - result.messages.push('Prosody configuration file content is not correct.') - result.debug.push({ - title: 'Prosody configuration should be', - // we have to hide secret keys and other values: - message: getProsodyConfigContentForDiagnostic(wantedConfig) + await fs.promises.access(filePath, fs.constants.R_OK) // throw an error if file does not exist. + result.messages.push(`The prosody '${configFileKey}' configuration file (${filePath}) exists`) + const actualContent = await fs.promises.readFile(filePath, { + encoding: 'utf-8' }) - return result + + result.debug.push({ + title: `Current prosody '${configFileKey}' configuration`, + // we have to hide secret keys and other values. + // But here, we haven't them for actualContent. + // So we will use values in wantedConfig, hopping it is enough. + message: wantedConfig.contentForDiagnostic(actualContent) + }) + + const wantedContent = configFile.content + if (actualContent === wantedContent) { + result.messages.push(`Prosody configuration file '${configFileKey}' content is correct.`) + } else { + result.messages.push(`Prosody configuration file '${configFileKey}'' content is not correct.`) + result.debug.push({ + title: `Prosody configuration '${configFileKey}' should be`, + // we have to hide secret keys and other values: + message: wantedConfig.contentForDiagnostic(wantedContent) + }) + return result + } } } catch (error) { - result.messages.push('Error when requiring the prosody config file: ' + (error as string)) + result.messages.push('Error when testing the prosody config: ' + (error as string)) return result } diff --git a/server/lib/prosody/config.ts b/server/lib/prosody/config.ts index cc5a30df..3503a4c0 100644 --- a/server/lib/prosody/config.ts +++ b/server/lib/prosody/config.ts @@ -60,6 +60,7 @@ async function getProsodyFilePaths (options: RegisterServerOptions): Promise + +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 { const logger = options.peertubeHelpers.logger logger.debug('Calling getProsodyConfig') @@ -182,18 +207,24 @@ async function getProsodyConfig (options: RegisterServerOptions): Promise { @@ -204,12 +235,16 @@ async function writeProsodyConfig (options: RegisterServerOptions): Promise Date: Tue, 7 Dec 2021 13:14:01 +0100 Subject: [PATCH 04/20] Demo Bot: first proof of concept. --- bots/.eslintrc.json | 40 ++++++++++ bots/bots.ts | 78 ++++++++++++++++++ bots/tsconfig.json | 26 ++++++ package-lock.json | 115 ++++++++++++++++++++++++++- package.json | 4 +- server/lib/diagnostic/prosody.ts | 4 +- server/lib/prosody/config.ts | 54 +++++++++---- server/lib/prosody/config/content.ts | 6 +- server/lib/prosody/config/paths.ts | 5 +- 9 files changed, 307 insertions(+), 25 deletions(-) create mode 100644 bots/.eslintrc.json create mode 100644 bots/bots.ts create mode 100644 bots/tsconfig.json diff --git a/bots/.eslintrc.json b/bots/.eslintrc.json new file mode 100644 index 00000000..de818741 --- /dev/null +++ b/bots/.eslintrc.json @@ -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" + } +} diff --git a/bots/bots.ts b/bots/bots.ts new file mode 100644 index 00000000..5bc547e8 --- /dev/null +++ b/bots/bots.ts @@ -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) diff --git a/bots/tsconfig.json b/bots/tsconfig.json new file mode 100644 index 00000000..5872a594 --- /dev/null +++ b/bots/tsconfig.json @@ -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": [] +} diff --git a/package-lock.json b/package-lock.json index 844d9fab..130dea94 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 99a07a5b..1b2bd2ae 100644 --- a/package.json +++ b/package.json @@ -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'", diff --git a/server/lib/diagnostic/prosody.ts b/server/lib/diagnostic/prosody.ts index d4bfc0ef..fafb29b5 100644 --- a/server/lib/diagnostic/prosody.ts +++ b/server/lib/diagnostic/prosody.ts @@ -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() diff --git a/server/lib/prosody/config.ts b/server/lib/prosody/config.ts index 3503a4c0..ec89834a 100644 --- a/server/lib/prosody/config.ts +++ b/server/lib/prosody/config.ts @@ -24,9 +24,9 @@ async function getWorkingDir (options: RegisterServerOptions): Promise { /** * Creates the working dir if needed, and returns it. */ -async function ensureWorkingDir (options: RegisterServerOptions): Promise { +async function ensureWorkingDirs (options: RegisterServerOptions): Promise { 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 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 Date: Tue, 7 Dec 2021 18:57:08 +0100 Subject: [PATCH 05/20] Demo Bot: WIP. --- bots/bots.ts | 115 ++++++++++++++---------------- bots/lib/bot/component.ts | 82 ++++++++++++++++++++++ bots/lib/bot/demobot.ts | 11 +++ bots/lib/bot/room.ts | 132 +++++++++++++++++++++++++++++++++++ bots/lib/bot/types.ts | 21 ++++++ bots/lib/config.ts | 88 +++++++++++++++++++++++ bots/lib/logger.ts | 23 ++++++ server/lib/prosody/config.ts | 4 +- 8 files changed, 410 insertions(+), 66 deletions(-) create mode 100644 bots/lib/bot/component.ts create mode 100644 bots/lib/bot/demobot.ts create mode 100644 bots/lib/bot/room.ts create mode 100644 bots/lib/bot/types.ts create mode 100644 bots/lib/config.ts create mode 100644 bots/lib/logger.ts diff --git a/bots/bots.ts b/bots/bots.ts index 5bc547e8..e1ccdfe2 100644 --- a/bots/bots.ts +++ b/bots/bots.ts @@ -1,78 +1,65 @@ -import * as path from 'path' +import { BotsConfig } from './lib/config' +import { logger } from './lib/logger' +import { ComponentBot } from './lib/bot/component' +import { DemoBot } from './lib/bot/demobot' -let demoBotConfigFile = process.argv[2] -if (!demoBotConfigFile) { +if (!process.argv[2]) { throw new Error('Missing parameter: the demobot configuration file path') } -demoBotConfigFile = path.resolve(demoBotConfigFile) +const botsConfig = new BotsConfig(process.argv[2]) -// 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).') +const runningBots: ComponentBot[] = [] + +async function start (botsConfig: BotsConfig): Promise { + await botsConfig.load() + + let atLeastOne: boolean = false + if (botsConfig.useDemoBot()) { + atLeastOne = true + logger.info('Starting DemoBot...') + + const config = botsConfig.getDemoBotConfig() + const instance = new DemoBot( + 'DemoBot', + { + service: config.service, + domain: config.domain, + password: config.password + }, + config.rooms, + 'DemoBot' // FIXME: handle the case where the nick is already taken. + ) + runningBots.push(instance) + instance.connect().catch(err => { throw err }) } - if (parts[parts.length - 1] !== 'demobot.js') { - throw new Error('demobot configuration file path seems invalid (filename is not demobot.js).') + if (!atLeastOne) { + logger.info('No bot to launch, exiting.') + process.exit(0) } } -checkBotConfigFilePath(demoBotConfigFile) -const demoBotConf = require(demoBotConfigFile).getConf() -if (!demoBotConf || !demoBotConf.UUIDs || !demoBotConf.UUIDs.length) { +async function shutdown (): Promise { + logger.info('Shutdown...') + for (const bot of runningBots) { + logger.info('Stopping the bot ' + bot.botName + '...') + await bot.stop() + } 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' +// catching signals and do something before exit +['SIGHUP', 'SIGINT', 'SIGQUIT', 'SIGILL', 'SIGTRAP', 'SIGABRT', + 'SIGBUS', 'SIGFPE', 'SIGUSR1', 'SIGSEGV', 'SIGUSR2', 'SIGTERM' +].forEach((sig) => { + process.on(sig, () => { + logger.debug('Receiving signal: ' + sig) + shutdown().catch((err) => { + logger.error(`Error on shutting down: ${err as string}`) }) - ) - 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) +start(botsConfig).catch((err) => { + logger.error(`Function start failed: ${err as string}`) + process.exit(1) +}) diff --git a/bots/lib/bot/component.ts b/bots/lib/bot/component.ts new file mode 100644 index 00000000..b569797c --- /dev/null +++ b/bots/lib/bot/component.ts @@ -0,0 +1,82 @@ +/* eslint-disable no-void */ +import { logger } from '../logger' +import { XMPP, XMPPXmlFunction, XMPPStanza, XMPPAddress } from './types' + +const { component, xml } = require('@xmpp/component') + +interface ComponentConnectionConfig { + service: string + domain: string + password: string +} + +abstract class ComponentBot { + protected xmpp?: XMPP + protected address?: XMPPAddress + + constructor ( + public readonly botName: string, + protected readonly connectionConfig: ComponentConnectionConfig + ) {} + + protected xml: XMPPXmlFunction = (...args) => xml(...args) + + public async connect (): Promise { + this.xmpp = component({ + service: this.connectionConfig.service, + domain: this.connectionConfig.domain, + password: this.connectionConfig.password + }) as XMPP + + this.xmpp.on('error', (err: any) => { + logger.error(err) + }) + + this.xmpp.on('offline', () => { + logger.info(`${this.botName} is now offline.`) + }) + + this.xmpp.on('stanza', (stanza: XMPPStanza) => { + logger.debug('stanza received' + (stanza?.toString ? ': ' + stanza.toString() : '')) + if (stanza.is('message')) { + void this.onMessage(stanza) + } + if (stanza.is('presence')) { + void this.onPresence(stanza) + } + if (stanza.is('iq')) { + void this.onIq(stanza) + } + }) + + this.xmpp.on('online', (address: XMPPAddress) => { + logger.debug('Online with address' + address.toString()) + + this.address = address + void this.onOnline() + }) + + this.xmpp.start() + } + + public async stop (): Promise { + const p = new Promise((resolve) => { + this.xmpp?.on('offline', () => { + logger.info(`Stoppping process: ${this.botName} is now offline.`) + resolve(true) + }) + }) + await this.xmpp?.stop() + await p + } + + protected async onMessage (_stanza: XMPPStanza): Promise {} + protected async onIq (_stanza: XMPPStanza): Promise {} + protected async onPresence (_stanza: XMPPStanza): Promise {} + protected async onOnline (): Promise {} +} + +export { + ComponentConnectionConfig, + ComponentBot +} diff --git a/bots/lib/bot/demobot.ts b/bots/lib/bot/demobot.ts new file mode 100644 index 00000000..9dd22299 --- /dev/null +++ b/bots/lib/bot/demobot.ts @@ -0,0 +1,11 @@ +import { RoomComponentBot } from './room' + +class DemoBot extends RoomComponentBot { + protected async onRoomJoin (roomId: string, nick: string): Promise { + await this.sendGroupchat(roomId, `Hello ${nick}! I'm the DemoBot, I'm here to demonstrate the chatroom.`) + } +} + +export { + DemoBot +} diff --git a/bots/lib/bot/room.ts b/bots/lib/bot/room.ts new file mode 100644 index 00000000..0266f944 --- /dev/null +++ b/bots/lib/bot/room.ts @@ -0,0 +1,132 @@ +import { ComponentBot, ComponentConnectionConfig } from './component' +import { XMPPStanza } from './types' +import { logger } from '../logger' + +interface RoomComponentBotRoomDescription { + jid: string + nick: string + users: Map +} + +abstract class RoomComponentBot extends ComponentBot { + protected readonly rooms: {[jid: string]: RoomComponentBotRoomDescription} = {} + + constructor ( + botName: string, + connectionConfig: ComponentConnectionConfig, + roomIds: string[], + protected readonly nick: string + ) { + super(botName, connectionConfig) + for (const roomId of roomIds) { + this.rooms[roomId] = { + jid: roomId, + nick: nick, + users: new Map() + } + } + } + + async onOnline (): Promise { + for (const roomId in this.rooms) { + const room = this.rooms[roomId] + logger.debug(`Connecting to room ${room.jid}...`) + const presence = this.xml( + 'presence', + { + from: this.address?.toString(), + to: room.jid + '/' + room.nick + }, + this.xml('x', { + xmlns: 'http://jabber.org/protocol/muc' + }) + ) + await this.xmpp?.send(presence) + } + await super.onOnline() + } + + protected async onPresence (stanza: XMPPStanza): Promise { + const [stanzaRoomId, stanzaNick] = stanza.attrs?.from.split('/') + if (this.rooms[stanzaRoomId]) { + await this.onRoomPresence(stanzaRoomId, stanza, stanzaNick) + } + } + + public async sendGroupchat (roomId: string, msg: string): Promise { + const room = this.rooms[roomId] + if (!room) { + logger.error('Trying to send a groupchat on an unknown room: ' + roomId) + return + } + const message = this.xml( + 'message', + { + type: 'groupchat', + to: room.jid, + from: this.address?.toString() + }, + this.xml('body', {}, msg) + ) + logger.debug('Sending message...: ' + (message.toString() as string)) + await this.xmpp?.send(message) + } + + public async stop (): Promise { + for (const roomId in this.rooms) { + const room = this.rooms[roomId] + logger.debug(`Leaving room ${room.jid}...`) + const presence = this.xml( + 'presence', + { + from: this.address?.toString(), + to: room.jid + '/' + room.nick, + type: 'unavailable' + } + ) + // FIXME: should wait for a presence stanza from the server. + await this.xmpp?.send(presence) + } + await super.stop() + } + + protected async onRoomPresence ( + roomId: string, + stanza: XMPPStanza, + nick?: string + ): Promise { + const room = this.rooms[roomId] + if (!room) { + return + } + if (!nick) { + return + } + const isPresent = stanza.attrs?.type !== 'unavailable' + // FIXME: selfPresence should better be tested by searching status=110 + const selfPresence = room.nick === nick + if (!isPresent) { + room.users.delete(nick) + if (!selfPresence) { + await this.onRoomPart(roomId, nick) + } + return + } + room.users.set(nick, { + nick + }) + if (!selfPresence) { + await this.onRoomJoin(roomId, nick) + } + } + + protected async onRoomJoin (_roomId: string, _nick: string): Promise {} + protected async onRoomPart (_roomId: string, _nick: string): Promise {} +} + +export { + RoomComponentBot +} diff --git a/bots/lib/bot/types.ts b/bots/lib/bot/types.ts new file mode 100644 index 00000000..03675bad --- /dev/null +++ b/bots/lib/bot/types.ts @@ -0,0 +1,21 @@ +import { EventEmitter } from 'events' + +export interface XMPP extends EventEmitter { + send: (xml: any) => any + start: () => any + stop: () => Promise +} + +export interface XMPPAddress { + toString: () => string +} + +export type XMPPStanzaType = 'message' | 'iq' | 'presence' + +export interface XMPPStanza { + attrs: any + is: (type: XMPPStanzaType) => boolean + toString: () => string +} + +export type XMPPXmlFunction = (type: string, attrs: object, content?: any) => any diff --git a/bots/lib/config.ts b/bots/lib/config.ts new file mode 100644 index 00000000..0f0f6048 --- /dev/null +++ b/bots/lib/config.ts @@ -0,0 +1,88 @@ +import * as path from 'path' +import * as fs from 'fs' +import decache from 'decache' +import { logger } from '../lib/logger' + +interface DemoBotConfig { + rooms: string[] + service: string + domain: string + mucDomain: string + password: string +} + +class BotsConfig { + protected readonly configDir: string + protected configs: { + demobot?: DemoBotConfig + } + + constructor (configDir: string) { + this.configDir = configDir = path.resolve(configDir) + + // Not necessary, but just in case: perform some path checking... (to limit code injection risks) + const parts = configDir.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('Bots configuration dir seems invalid (not in peertube-plugin-livechat folder).') + } + + this.configs = {} + } + + public async load (): Promise { + await this.loadDemoBot() + } + + protected async loadDemoBot (): Promise { + const configPath = path.resolve(this.configDir, 'demobot.js') + logger.debug(`Loading DemoBot config from file ${configPath}`) + if (!fs.existsSync(configPath)) { + logger.debug('The config file for DemoBot does not exist.') + delete this.configs.demobot + return + } + + decache(configPath) + + logger.debug('require DemoBot config file...') + const conf = require(configPath).getConf() as DemoBotConfig | null + if (!conf) { + logger.debug('getConf() returned null for the DemoBot.') + delete this.configs.demobot + return + } + if (!conf.rooms || !conf.domain || !conf.mucDomain || !conf.password || !conf.service) { + logger.error('Invalid DemoBot configuration: ' + JSON.stringify(conf)) + delete this.configs.demobot + return + } + + // Conf seems legit. But if there is no rooms, no need to keep it. + if (!conf.rooms.length) { + logger.debug('No room in DemoBot config.') + delete this.configs.demobot + return + } + + // TODO: detect changes? avoid reloading when not needed? or should it be by the caller? + logger.debug('Config loaded for demobot: ' + JSON.stringify(conf)) + this.configs.demobot = conf + } + + public useDemoBot (): boolean { + return (this.configs.demobot?.rooms?.length ?? 0) > 0 + } + + public getDemoBotConfig (): DemoBotConfig { + if (!this.configs.demobot) { + throw new Error('Should not call getDemoBotConfig when useDemoBot is false.') + } + return this.configs.demobot + } +} + +export { + BotsConfig +} diff --git a/bots/lib/logger.ts b/bots/lib/logger.ts new file mode 100644 index 00000000..d014a0dc --- /dev/null +++ b/bots/lib/logger.ts @@ -0,0 +1,23 @@ +class Logger { + public debug (s: string): void { + console.log(s) + } + + public info (s: string): void { + console.info(s) + } + + public warn (s: string): void { + console.warn(s) + } + + public error (s: string): void { + console.error(s) + } +} + +const logger = new Logger() + +export { + logger +} diff --git a/server/lib/prosody/config.ts b/server/lib/prosody/config.ts index ec89834a..aed905d5 100644 --- a/server/lib/prosody/config.ts +++ b/server/lib/prosody/config.ts @@ -198,7 +198,7 @@ async function getProsodyConfig (options: RegisterServerOptions): Promise 0) { useExternalComponents = true const componentSecret = await getExternalComponentKey(options, 'DEMOBOT') @@ -206,7 +206,7 @@ async function getProsodyConfig (options: RegisterServerOptions): Promise `${uuid}@room.${prosodyDomain}`), service: 'xmpp://127.0.0.1:' + externalComponentsPort, domain: 'demobot.' + prosodyDomain, mucDomain: 'room.' + prosodyDomain, From 2c72f3bf2f22d12dedaf666da46d338a7c7f6424 Mon Sep 17 00:00:00 2001 From: John Livingston Date: Wed, 8 Dec 2021 12:29:21 +0100 Subject: [PATCH 06/20] Fix @xmpp typing. --- bots/lib/bot/component.ts | 19 ++-- bots/lib/bot/types.ts | 20 +--- package-lock.json | 189 ++++++++++++++++++++++++++++++++++++++ package.json | 1 + 4 files changed, 202 insertions(+), 27 deletions(-) diff --git a/bots/lib/bot/component.ts b/bots/lib/bot/component.ts index b569797c..d54a1ed1 100644 --- a/bots/lib/bot/component.ts +++ b/bots/lib/bot/component.ts @@ -1,8 +1,8 @@ /* eslint-disable no-void */ import { logger } from '../logger' -import { XMPP, XMPPXmlFunction, XMPPStanza, XMPPAddress } from './types' - -const { component, xml } = require('@xmpp/component') +import type { XMPPStanza } from './types' +import { component, xml, Component } from '@xmpp/component' +import type { JID } from '@xmpp/jid' interface ComponentConnectionConfig { service: string @@ -11,22 +11,21 @@ interface ComponentConnectionConfig { } abstract class ComponentBot { - protected xmpp?: XMPP - protected address?: XMPPAddress + protected xmpp?: Component + protected address?: JID + protected xml = xml constructor ( public readonly botName: string, protected readonly connectionConfig: ComponentConnectionConfig ) {} - protected xml: XMPPXmlFunction = (...args) => xml(...args) - public async connect (): Promise { this.xmpp = component({ service: this.connectionConfig.service, domain: this.connectionConfig.domain, password: this.connectionConfig.password - }) as XMPP + }) this.xmpp.on('error', (err: any) => { logger.error(err) @@ -49,14 +48,14 @@ abstract class ComponentBot { } }) - this.xmpp.on('online', (address: XMPPAddress) => { + this.xmpp.on('online', (address) => { logger.debug('Online with address' + address.toString()) this.address = address void this.onOnline() }) - this.xmpp.start() + await this.xmpp.start() } public async stop (): Promise { diff --git a/bots/lib/bot/types.ts b/bots/lib/bot/types.ts index 03675bad..e197a8cd 100644 --- a/bots/lib/bot/types.ts +++ b/bots/lib/bot/types.ts @@ -1,21 +1,7 @@ -import { EventEmitter } from 'events' - -export interface XMPP extends EventEmitter { - send: (xml: any) => any - start: () => any - stop: () => Promise -} - -export interface XMPPAddress { - toString: () => string -} +import type { Element } from '@xmpp/xml' export type XMPPStanzaType = 'message' | 'iq' | 'presence' -export interface XMPPStanza { - attrs: any - is: (type: XMPPStanzaType) => boolean - toString: () => string +export interface XMPPStanza extends Element { + name: XMPPStanzaType } - -export type XMPPXmlFunction = (type: string, attrs: object, content?: any) => any diff --git a/package-lock.json b/package-lock.json index 130dea94..c26928c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -175,6 +175,15 @@ "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==", "dev": true }, + "@types/accepts": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.5.tgz", + "integrity": "sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/async": { "version": "3.2.9", "resolved": "https://registry.npmjs.org/@types/async/-/async-3.2.9.tgz", @@ -211,6 +220,24 @@ "@types/node": "*" } }, + "@types/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-0mPF08jn9zYI0n0Q/Pnz7C4kThdSt+6LD4amsrYDDpgBfrVWa3TcCOxKX1zkGgYniGagRv8heN2cbh+CAn+uuQ==", + "dev": true + }, + "@types/cookies": { + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.7.7.tgz", + "integrity": "sha512-h7BcvPUogWbKCzBR2lY4oqaZbO3jXZksexYJVFvkrFeLgbZjQkU4x8pRq6eg2MHXQhY0McQdqmmsxRWlVAHooA==", + "dev": true, + "requires": { + "@types/connect": "*", + "@types/express": "*", + "@types/keygrip": "*", + "@types/node": "*" + } + }, "@types/express": { "version": "4.17.13", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", @@ -254,11 +281,23 @@ "form-data": "^2.5.0" } }, + "@types/http-assert": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.3.tgz", + "integrity": "sha512-FyAOrDuQmBi8/or3ns4rwPno7/9tJTijVW6aQQjK02+kOQ8zmoNg2XJtAuQhvQcy1ASJq38wirX5//9J1EqoUA==", + "dev": true + }, "@types/http-cache-semantics": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz", "integrity": "sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A==" }, + "@types/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-e+2rjEwK6KDaNOm5Aa9wNGgyS9oSZU/4pfSMMPYNOfjvFI0WVXm29+ITRFr6aKDvvKo7uU1jV68MW4ScsfDi7Q==", + "dev": true + }, "@types/json-schema": { "version": "7.0.9", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", @@ -271,6 +310,12 @@ "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", "dev": true }, + "@types/keygrip": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.2.tgz", + "integrity": "sha512-GJhpTepz2udxGexqos8wgaBx4I/zWIDPh/KOGEwAqtuGDkOUJu5eFvwmdBX4AmB8Odsr+9pHCQqiAqDL/yKMKw==", + "dev": true + }, "@types/keyv": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.1.tgz", @@ -279,6 +324,37 @@ "@types/node": "*" } }, + "@types/koa": { + "version": "2.13.4", + "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.13.4.tgz", + "integrity": "sha512-dfHYMfU+z/vKtQB7NUrthdAEiSvnLebvBjwHtfFmpZmB7em2N3WVQdHgnFq+xvyVgxW5jKDmjWfLD3lw4g4uTw==", + "dev": true, + "requires": { + "@types/accepts": "*", + "@types/content-disposition": "*", + "@types/cookies": "*", + "@types/http-assert": "*", + "@types/http-errors": "*", + "@types/keygrip": "*", + "@types/koa-compose": "*", + "@types/node": "*" + } + }, + "@types/koa-compose": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@types/koa-compose/-/koa-compose-3.2.5.tgz", + "integrity": "sha512-B8nG/OoE1ORZqCkBVsup/AKcvjdgoHnfi4pZMn5UwAPCbhk/96xyv284eBYW8JlQbQ7zDmnpFr68I/40mFoIBQ==", + "dev": true, + "requires": { + "@types/koa": "*" + } + }, + "@types/ltx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/ltx/-/ltx-3.0.1.tgz", + "integrity": "sha512-X+1EoqEcSZ45MYJmg0rfMvEyQPGydLT00HJcPant+5J3+OM0N+ZVL6BdZ1Iy4K3dA+JBGe1WP7PvTM/GtxN/XA==", + "dev": true + }, "@types/mime": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", @@ -353,6 +429,119 @@ "winston": "*" } }, + "@types/xmpp__component": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@types/xmpp__component/-/xmpp__component-0.13.0.tgz", + "integrity": "sha512-4vKLiicgkZwW8bKofmmy0BJpw3MuOW73c5hVPhUtgBPDTh9hj7wQezhpOLX3AhQFto97YpLg2GwWzhnwfSl1BA==", + "dev": true, + "requires": { + "@types/xmpp__component-core": "*", + "@types/xmpp__iq": "*", + "@types/xmpp__middleware": "*", + "@types/xmpp__reconnect": "*" + } + }, + "@types/xmpp__component-core": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@types/xmpp__component-core/-/xmpp__component-core-0.13.0.tgz", + "integrity": "sha512-K9l6SLG91kTcchW/Nt5TL9Kfe5aWDyDcHWvoFgnwvGoF4g0K737HdZMzD0DN1TP7Gb2g/JNCiK245BuDYegAbw==", + "dev": true, + "requires": { + "@types/xmpp__connection-tcp": "*", + "@types/xmpp__jid": "*", + "@types/xmpp__xml": "*" + } + }, + "@types/xmpp__connection": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@types/xmpp__connection/-/xmpp__connection-0.13.0.tgz", + "integrity": "sha512-YsvLhgOfxY3TbDTeTT0ZrToqh3IsA0nKnXk/NxTES2O6wTxn9lQDRBYNgB6lkq+D50nA8nmT3d53acb0f4Rycw==", + "dev": true, + "requires": { + "@types/xmpp__error": "*", + "@types/xmpp__events": "*", + "@types/xmpp__jid": "*", + "@types/xmpp__xml": "*" + } + }, + "@types/xmpp__connection-tcp": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@types/xmpp__connection-tcp/-/xmpp__connection-tcp-0.13.0.tgz", + "integrity": "sha512-yHvAWck6JVs0H/E2tnoUVOsFPylLj1TX4ARdm1/jFRqOPWynw36B/RU0UW1KNSC8dKA6VAhl0mTICnGUZVtcug==", + "dev": true, + "requires": { + "@types/xmpp__connection": "*", + "@types/xmpp__xml": "*" + } + }, + "@types/xmpp__error": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@types/xmpp__error/-/xmpp__error-0.13.0.tgz", + "integrity": "sha512-W+tM0UDj3toruhdjhn/VK1mtjOF+YMz+FdxgkMVi6lwCXA/uDW79elW6WbeM8zhiM92ZoVPSgD2zt9YXmrkiZQ==", + "dev": true, + "requires": { + "@types/xmpp__xml": "*" + } + }, + "@types/xmpp__events": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@types/xmpp__events/-/xmpp__events-0.13.0.tgz", + "integrity": "sha512-somi0EF9BwaBPmDQk6r1hE6dtXXjv2ztSNk/hStcfGVY9NfD9ErcopWgzzbGdeQg2/WcMNlVwfYXQfIm6w3w+A==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/xmpp__iq": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@types/xmpp__iq/-/xmpp__iq-0.13.0.tgz", + "integrity": "sha512-jy3aTixRMi8uqiIfhbkIxeWB62NTFGXKdZsYOwlgLNQ9BUimnbGR8BmZGSic5meUTPUaEEpCx/xp3AnVYADICQ==", + "dev": true, + "requires": { + "@types/koa-compose": "*", + "@types/xmpp__events": "*", + "@types/xmpp__middleware": "*", + "@types/xmpp__xml": "*" + } + }, + "@types/xmpp__jid": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/xmpp__jid/-/xmpp__jid-1.3.2.tgz", + "integrity": "sha512-zh5mdcBY1zNzI9XxXZxsuq/XGd6YeSwZzwQJpV5NQEtZUiSJ1+YW19+w2pELLrlV2hoMOcSf8PfPwB9ocPwIDg==", + "dev": true + }, + "@types/xmpp__middleware": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@types/xmpp__middleware/-/xmpp__middleware-0.13.0.tgz", + "integrity": "sha512-bgwIFdl5khKt/UQY4f6Ca7pEIUGQPCN3CvZ4ZuYSwp5PY9EpH32Tj/akUwfWMuMqGsybvdTeuq7ewT1ic7hsZQ==", + "dev": true, + "requires": { + "@types/koa-compose": "*", + "@types/xmpp__connection": "*", + "@types/xmpp__error": "*", + "@types/xmpp__jid": "*", + "@types/xmpp__xml": "*" + } + }, + "@types/xmpp__reconnect": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@types/xmpp__reconnect/-/xmpp__reconnect-0.13.0.tgz", + "integrity": "sha512-MGuq9Dl24iU/t1nuGp/5yUsv4yAvQk5DOARw/iPXpAjB5hCBCzzvsN2ttkw8vAVsQ5DSbpgPWI33GQ2xF2MaSQ==", + "dev": true, + "requires": { + "@types/xmpp__connection": "*", + "@types/xmpp__events": "*" + } + }, + "@types/xmpp__xml": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/@types/xmpp__xml/-/xmpp__xml-0.13.1.tgz", + "integrity": "sha512-pxRGht/JVPhIwvcFkqv3fsXc1V/qj/C+vkTD75S1whpaNslJJbmA4hphOcbynvIegKdQHxfa56d22sOtHWjDsg==", + "dev": true, + "requires": { + "@types/ltx": "*" + } + }, "@typescript-eslint/eslint-plugin": { "version": "4.29.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.29.0.tgz", diff --git a/package.json b/package.json index 1b2bd2ae..a0e63ffb 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "@types/got": "^9.6.12", "@types/node": "^16.11.6", "@types/winston": "^2.4.4", + "@types/xmpp__component": "^0.13.0", "@typescript-eslint/eslint-plugin": "^4.29.0", "@typescript-eslint/parser": "^4.29.0", "eslint": "^7.32.0", From 42988a5d0491fbabd706052339986639dffafabf Mon Sep 17 00:00:00 2001 From: John Livingston Date: Wed, 8 Dec 2021 15:29:34 +0100 Subject: [PATCH 07/20] Demo Bot: Complete code refactoring. WIP. --- bots/bots.ts | 21 ++-- bots/lib/bot/component.ts | 103 ++++++++++------- bots/lib/bot/demobot.ts | 11 -- bots/lib/bot/handlers/base.ts | 11 ++ bots/lib/bot/handlers/demo.ts | 19 ++++ bots/lib/bot/room.ts | 202 +++++++++++++++++----------------- bots/lib/bot/types.ts | 6 + package.json | 2 + server/lib/prosody/config.ts | 2 +- 9 files changed, 218 insertions(+), 159 deletions(-) delete mode 100644 bots/lib/bot/demobot.ts create mode 100644 bots/lib/bot/handlers/base.ts create mode 100644 bots/lib/bot/handlers/demo.ts diff --git a/bots/bots.ts b/bots/bots.ts index e1ccdfe2..22aca0dd 100644 --- a/bots/bots.ts +++ b/bots/bots.ts @@ -1,14 +1,14 @@ import { BotsConfig } from './lib/config' import { logger } from './lib/logger' -import { ComponentBot } from './lib/bot/component' -import { DemoBot } from './lib/bot/demobot' +import { BotComponent } from './lib/bot/component' +import { BotHandlerDemo } from './lib/bot/handlers/demo' if (!process.argv[2]) { throw new Error('Missing parameter: the demobot configuration file path') } const botsConfig = new BotsConfig(process.argv[2]) -const runningBots: ComponentBot[] = [] +const runningBots: BotComponent[] = [] async function start (botsConfig: BotsConfig): Promise { await botsConfig.load() @@ -19,18 +19,23 @@ async function start (botsConfig: BotsConfig): Promise { logger.info('Starting DemoBot...') const config = botsConfig.getDemoBotConfig() - const instance = new DemoBot( + const instance = new BotComponent( 'DemoBot', { service: config.service, domain: config.domain, password: config.password }, - config.rooms, - 'DemoBot' // FIXME: handle the case where the nick is already taken. + config.mucDomain ) runningBots.push(instance) - instance.connect().catch(err => { throw err }) + + instance.connect().then(async () => { + for (const roomId of config.rooms) { + const room = await instance.joinRoom(roomId, 'DemoBot') + room.attachHandler(new BotHandlerDemo(room)) + } + }).catch(err => { throw err }) } if (!atLeastOne) { logger.info('No bot to launch, exiting.') @@ -42,7 +47,7 @@ async function shutdown (): Promise { logger.info('Shutdown...') for (const bot of runningBots) { logger.info('Stopping the bot ' + bot.botName + '...') - await bot.stop() + await bot.disconnect() } process.exit(0) } diff --git a/bots/lib/bot/component.ts b/bots/lib/bot/component.ts index d54a1ed1..5984a1ff 100644 --- a/bots/lib/bot/component.ts +++ b/bots/lib/bot/component.ts @@ -1,23 +1,20 @@ -/* eslint-disable no-void */ +import type { XMPPStanza, XMPPStanzaType } from './types' +import type { Node } from '@xmpp/xml' import { logger } from '../logger' -import type { XMPPStanza } from './types' -import { component, xml, Component } from '@xmpp/component' -import type { JID } from '@xmpp/jid' +import { component, xml, Component, Options } from '@xmpp/component' +import { parse, JID } from '@xmpp/jid' +import { BotRoom } from './room' -interface ComponentConnectionConfig { - service: string - domain: string - password: string -} - -abstract class ComponentBot { +class BotComponent { protected xmpp?: Component protected address?: JID - protected xml = xml + public readonly xml = xml + protected rooms: Map = new Map() constructor ( public readonly botName: string, - protected readonly connectionConfig: ComponentConnectionConfig + protected readonly connectionConfig: Options, + protected readonly mucDomain: string ) {} public async connect (): Promise { @@ -36,46 +33,78 @@ abstract class ComponentBot { }) this.xmpp.on('stanza', (stanza: XMPPStanza) => { - logger.debug('stanza received' + (stanza?.toString ? ': ' + stanza.toString() : '')) - if (stanza.is('message')) { - void this.onMessage(stanza) - } - if (stanza.is('presence')) { - void this.onPresence(stanza) - } - if (stanza.is('iq')) { - void this.onIq(stanza) + logger.debug('stanza received' + stanza.toString()) + if (!stanza.attrs.from) { return } + const jid = parse(stanza.attrs.from) + const roomJid = jid.bare() // removing the «resource» part of the jid. + const room = this.rooms.get(roomJid.toString()) + if (!room) { + return } + room.emit('stanza', stanza, jid.getResource()) }) this.xmpp.on('online', (address) => { logger.debug('Online with address' + address.toString()) this.address = address - void this.onOnline() + + // 'online' is emitted at reconnection, so we must reset rooms rosters + this.rooms.forEach(room => room.emit('reset')) + }) + + this.xmpp.on('offline', () => { + logger.info(`Stoppping process: ${this.botName} is now offline.`) }) await this.xmpp.start() } - public async stop (): Promise { - const p = new Promise((resolve) => { - this.xmpp?.on('offline', () => { - logger.info(`Stoppping process: ${this.botName} is now offline.`) - resolve(true) - }) - }) + public async disconnect (): Promise { + for (const [roomId, room] of this.rooms) { + logger.debug(`Leaving room ${roomId}...`) + await room.part() + } await this.xmpp?.stop() - await p + this.xmpp = undefined } - protected async onMessage (_stanza: XMPPStanza): Promise {} - protected async onIq (_stanza: XMPPStanza): Promise {} - protected async onPresence (_stanza: XMPPStanza): Promise {} - protected async onOnline (): Promise {} + public async sendStanza ( + type: XMPPStanzaType, + attrs: object, + ...children: Node[] + ): Promise { + attrs = Object.assign({ + from: this.address?.toString() + }, attrs) + + const stanza = this.xml(type, attrs, ...children) + logger.debug('stanza to emit: ' + stanza.toString()) + await this.xmpp?.send(stanza) + } + + public async joinRoom (roomId: string, nick: string): Promise { + const roomJID = new JID(roomId, this.mucDomain) + const roomJIDstr = roomJID.toString() + let room: BotRoom | undefined = this.rooms.get(roomJIDstr) + if (!room) { + room = new BotRoom(this, roomJID) + this.rooms.set(roomJIDstr, room) + } + await room.join(nick) + return room + } + + public async partRoom (roomId: string): Promise { + const roomJID = new JID(roomId, this.mucDomain) + const room = this.rooms.get(roomJID.toString()) + if (!room) { + return + } + await room.part() + } } export { - ComponentConnectionConfig, - ComponentBot + BotComponent } diff --git a/bots/lib/bot/demobot.ts b/bots/lib/bot/demobot.ts deleted file mode 100644 index 9dd22299..00000000 --- a/bots/lib/bot/demobot.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { RoomComponentBot } from './room' - -class DemoBot extends RoomComponentBot { - protected async onRoomJoin (roomId: string, nick: string): Promise { - await this.sendGroupchat(roomId, `Hello ${nick}! I'm the DemoBot, I'm here to demonstrate the chatroom.`) - } -} - -export { - DemoBot -} diff --git a/bots/lib/bot/handlers/base.ts b/bots/lib/bot/handlers/base.ts new file mode 100644 index 00000000..db4c1ed8 --- /dev/null +++ b/bots/lib/bot/handlers/base.ts @@ -0,0 +1,11 @@ +import type { BotRoom } from '../room' + +export abstract class BotHandler { + constructor ( + protected readonly room: BotRoom + ) { + this.init() + } + + protected abstract init (): void +} diff --git a/bots/lib/bot/handlers/demo.ts b/bots/lib/bot/handlers/demo.ts new file mode 100644 index 00000000..7adc51d7 --- /dev/null +++ b/bots/lib/bot/handlers/demo.ts @@ -0,0 +1,19 @@ +import type { XMPPUser } from '../types' +import { BotHandler } from './base' + +export class BotHandlerDemo extends BotHandler { + protected init (): void { + const room = this.room + room.on('room_join', (user: XMPPUser) => { + if (user.isMe) { + return + } + if (!room.isOnline()) { + return + } + room.sendGroupchat( + `Hello ${user.nick}! I'm the DemoBot, I'm here to demonstrate the chatroom.` + ).catch(() => {}) + }) + } +} diff --git a/bots/lib/bot/room.ts b/bots/lib/bot/room.ts index 0266f944..bf6d3ee1 100644 --- a/bots/lib/bot/room.ts +++ b/bots/lib/bot/room.ts @@ -1,132 +1,130 @@ -import { ComponentBot, ComponentConnectionConfig } from './component' -import { XMPPStanza } from './types' +import type { BotComponent } from './component' +import type { BotHandler } from './handlers/base' +import type { XMPPStanza, XMPPUser } from './types' +import EventEmitter from 'events' +import { JID } from '@xmpp/jid' import { logger } from '../logger' -interface RoomComponentBotRoomDescription { - jid: string - nick: string - users: Map -} +export class BotRoom extends EventEmitter { + protected state: 'offline' | 'online' = 'offline' + protected userJID: JID | undefined + protected readonly roster: Map = new Map() -abstract class RoomComponentBot extends ComponentBot { - protected readonly rooms: {[jid: string]: RoomComponentBotRoomDescription} = {} + protected readonly handlers: BotHandler[] = [] constructor ( - botName: string, - connectionConfig: ComponentConnectionConfig, - roomIds: string[], - protected readonly nick: string + protected readonly component: BotComponent, + protected readonly roomJID: JID ) { - super(botName, connectionConfig) - for (const roomId of roomIds) { - this.rooms[roomId] = { - jid: roomId, - nick: nick, - users: new Map() - } - } + super() + + this.on('reset', () => { + this.state = 'offline' + this.roster.clear() + }) + this.on('stanza', (stanza: XMPPStanza, resource?: string) => { + this.receiveStanza(stanza, resource) + }) } - async onOnline (): Promise { - for (const roomId in this.rooms) { - const room = this.rooms[roomId] - logger.debug(`Connecting to room ${room.jid}...`) - const presence = this.xml( - 'presence', - { - from: this.address?.toString(), - to: room.jid + '/' + room.nick - }, - this.xml('x', { - xmlns: 'http://jabber.org/protocol/muc' - }) - ) - await this.xmpp?.send(presence) - } - await super.onOnline() + public isOnline (): boolean { + return this.state === 'online' } - protected async onPresence (stanza: XMPPStanza): Promise { - const [stanzaRoomId, stanzaNick] = stanza.attrs?.from.split('/') - if (this.rooms[stanzaRoomId]) { - await this.onRoomPresence(stanzaRoomId, stanza, stanzaNick) - } + public async join (nick: string): Promise { + this.userJID = new JID(this.roomJID.getLocal(), this.roomJID.getDomain(), nick) + logger.debug(`Emitting a presence for room ${this.roomJID.toString()}...`) + await this.component.sendStanza('presence', + { + to: this.userJID.toString() + }, + this.component.xml('x', { + xmlns: 'http://jabber.org/protocol/muc' + }) + ) + // FIXME: should wait for a presence stanza from the server. + // FIXME: should handle used nick errors. } - public async sendGroupchat (roomId: string, msg: string): Promise { - const room = this.rooms[roomId] - if (!room) { - logger.error('Trying to send a groupchat on an unknown room: ' + roomId) - return - } - const message = this.xml( + public async part (): Promise { + if (!this.userJID) { return } + logger.debug(`Emitting a presence=unavailable for room ${this.roomJID.toString()}...`) + await this.component.sendStanza('presence', { + to: this.userJID.toString(), + type: 'unavailable' + }) + // FIXME: should wait for a presence stanza from the server. + } + + public async sendGroupchat (msg: string): Promise { + if (!this.userJID) { return } + logger.debug(`Emitting a groupchat message for room ${this.roomJID.toString()}...`) + await this.component.sendStanza( 'message', { type: 'groupchat', - to: room.jid, - from: this.address?.toString() + to: this.roomJID.toString() }, - this.xml('body', {}, msg) + this.component.xml('body', {}, msg) ) - logger.debug('Sending message...: ' + (message.toString() as string)) - await this.xmpp?.send(message) } - public async stop (): Promise { - for (const roomId in this.rooms) { - const room = this.rooms[roomId] - logger.debug(`Leaving room ${room.jid}...`) - const presence = this.xml( - 'presence', - { - from: this.address?.toString(), - to: room.jid + '/' + room.nick, - type: 'unavailable' - } - ) - // FIXME: should wait for a presence stanza from the server. - await this.xmpp?.send(presence) + public receiveStanza (stanza: XMPPStanza, fromResource?: string): void { + if (stanza.name === 'presence') { + this.receivePresenceStanza(stanza, fromResource) } - await super.stop() } - protected async onRoomPresence ( - roomId: string, - stanza: XMPPStanza, - nick?: string - ): Promise { - const room = this.rooms[roomId] - if (!room) { + public receivePresenceStanza (stanza: XMPPStanza, fromResource?: string): void { + if (!fromResource) { return } - if (!nick) { - return - } - const isPresent = stanza.attrs?.type !== 'unavailable' - // FIXME: selfPresence should better be tested by searching status=110 - const selfPresence = room.nick === nick - if (!isPresent) { - room.users.delete(nick) - if (!selfPresence) { - await this.onRoomPart(roomId, nick) + + const isPresent = stanza.attrs.type !== 'unavailable' + + const statusElems = stanza.getChild('x')?.getChildren('status') + const statusCodes = [] + if (statusElems) { + for (const s of statusElems) { + statusCodes.push(parseInt(s.attrs.code)) } - return } - room.users.set(nick, { - nick - }) - if (!selfPresence) { - await this.onRoomJoin(roomId, nick) + const isMe = statusCodes.includes(110) // status 110 means that is concern the current user. + + let user = this.roster.get(fromResource) + const previousState = user?.state + if (!isPresent) { + if (!user) { + return + } + user.state = 'offline' + if (isMe) { + this.state = 'offline' + } + if (previousState === 'online') { + this.emit('room_part', user) + } + } else { + if (!user) { + user = { + state: 'online', + nick: fromResource, + isMe: isMe + } + this.roster.set(fromResource, user) + } else { + user.state = 'online' + } + if (isMe) { + this.state = 'online' + } + if (previousState !== 'online') { + this.emit('room_join', user) + } } } - protected async onRoomJoin (_roomId: string, _nick: string): Promise {} - protected async onRoomPart (_roomId: string, _nick: string): Promise {} -} - -export { - RoomComponentBot + public attachHandler (handler: BotHandler): void { + this.handlers.push(handler) + } } diff --git a/bots/lib/bot/types.ts b/bots/lib/bot/types.ts index e197a8cd..5fc1dd2c 100644 --- a/bots/lib/bot/types.ts +++ b/bots/lib/bot/types.ts @@ -5,3 +5,9 @@ export type XMPPStanzaType = 'message' | 'iq' | 'presence' export interface XMPPStanza extends Element { name: XMPPStanzaType } + +export interface XMPPUser { + state: 'offline' | 'online' + nick: string + isMe: boolean +} diff --git a/package.json b/package.json index a0e63ffb..198ead6a 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ ], "dependencies": { "@xmpp/component": "^0.13.0", + "@xmpp/jid": "^0.13.0", "async": "^3.2.2", "body-parser": "^1.19.0", "decache": "^4.6.0", @@ -50,6 +51,7 @@ "@types/node": "^16.11.6", "@types/winston": "^2.4.4", "@types/xmpp__component": "^0.13.0", + "@types/xmpp__jid": "^1.3.2", "@typescript-eslint/eslint-plugin": "^4.29.0", "@typescript-eslint/parser": "^4.29.0", "eslint": "^7.32.0", diff --git a/server/lib/prosody/config.ts b/server/lib/prosody/config.ts index aed905d5..163be2dc 100644 --- a/server/lib/prosody/config.ts +++ b/server/lib/prosody/config.ts @@ -206,7 +206,7 @@ async function getProsodyConfig (options: RegisterServerOptions): Promise `${uuid}@room.${prosodyDomain}`), + rooms: demoBotUUIDs, service: 'xmpp://127.0.0.1:' + externalComponentsPort, domain: 'demobot.' + prosodyDomain, mucDomain: 'room.' + prosodyDomain, From b27455b08db3af41a50e54d2eb9daf93ad1947de Mon Sep 17 00:00:00 2001 From: John Livingston Date: Wed, 8 Dec 2021 15:51:29 +0100 Subject: [PATCH 08/20] DemoBot :Debounce hello messages. --- bots/lib/bot/handlers/demo.ts | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/bots/lib/bot/handlers/demo.ts b/bots/lib/bot/handlers/demo.ts index 7adc51d7..a290c0da 100644 --- a/bots/lib/bot/handlers/demo.ts +++ b/bots/lib/bot/handlers/demo.ts @@ -1,7 +1,10 @@ import type { XMPPUser } from '../types' +import { logger } from '../../logger' import { BotHandler } from './base' export class BotHandlerDemo extends BotHandler { + protected readonly lastHellos: Map = new Map() + protected init (): void { const room = this.room room.on('room_join', (user: XMPPUser) => { @@ -11,9 +14,23 @@ export class BotHandlerDemo extends BotHandler { if (!room.isOnline()) { return } - room.sendGroupchat( - `Hello ${user.nick}! I'm the DemoBot, I'm here to demonstrate the chatroom.` - ).catch(() => {}) + const lastHello = this.lastHellos.get(user.nick) + const now = new Date() + let msg: string + if (lastHello) { + logger.debug(`The user ${user.nick} was already seen at ${lastHello.toString()}`) + if ((now.getTime() - lastHello.getTime()) < 3600 * 1000) { // no more than one hello per hour + logger.info(`The user ${user.nick} was seen to recently, no message to send.`) + return + } + logger.info(`The user ${user.nick} was seen a long time ago, sending a message.`) + msg = `Hello ${user.nick}! Happy to see you again.` + } else { + logger.info(`The user ${user.nick} is here for the first time. Sending a message.`) + msg = `Hello ${user.nick}! I'm the DemoBot, I'm here to demonstrate the chatroom.` + } + this.lastHellos.set(user.nick, now) + room.sendGroupchat(msg).catch(() => {}) }) } } From 4efc507b2cf5a37762bb65fbe62ff5288b324eda Mon Sep 17 00:00:00 2001 From: John Livingston Date: Wed, 8 Dec 2021 16:12:47 +0100 Subject: [PATCH 09/20] DemoBot: random messages. --- bots/lib/bot/component.ts | 1 + bots/lib/bot/handlers/base.ts | 1 + bots/lib/bot/handlers/demo.ts | 32 ++++++++++++++++++++++++++++++++ bots/lib/bot/room.ts | 14 ++++++++++++++ 4 files changed, 48 insertions(+) diff --git a/bots/lib/bot/component.ts b/bots/lib/bot/component.ts index 5984a1ff..fddd3d6b 100644 --- a/bots/lib/bot/component.ts +++ b/bots/lib/bot/component.ts @@ -63,6 +63,7 @@ class BotComponent { public async disconnect (): Promise { for (const [roomId, room] of this.rooms) { logger.debug(`Leaving room ${roomId}...`) + await room.detachHandlers() await room.part() } await this.xmpp?.stop() diff --git a/bots/lib/bot/handlers/base.ts b/bots/lib/bot/handlers/base.ts index db4c1ed8..290988cd 100644 --- a/bots/lib/bot/handlers/base.ts +++ b/bots/lib/bot/handlers/base.ts @@ -8,4 +8,5 @@ export abstract class BotHandler { } protected abstract init (): void + public abstract stop (): void } diff --git a/bots/lib/bot/handlers/demo.ts b/bots/lib/bot/handlers/demo.ts index a290c0da..a625ae5e 100644 --- a/bots/lib/bot/handlers/demo.ts +++ b/bots/lib/bot/handlers/demo.ts @@ -2,8 +2,19 @@ import type { XMPPUser } from '../types' import { logger } from '../../logger' import { BotHandler } from './base' +const RANDOM_MESSAGES: string[] = [ + '🎵🎶', + '🎵🎶 I\'m just a bot, I\'m just a bot in the world. 🎵🎶', + 'You can see who is connected by opening the right panel.', + 'This is a random message.', + 'Oh, yet another random message.', + 'You can mention a user using a @ in front of a user\'s nick. Try to mention me.' +] + export class BotHandlerDemo extends BotHandler { protected readonly lastHellos: Map = new Map() + protected randomCount: number = 0 + protected randomTimeout: NodeJS.Timeout | undefined protected init (): void { const room = this.room @@ -32,5 +43,26 @@ export class BotHandlerDemo extends BotHandler { this.lastHellos.set(user.nick, now) room.sendGroupchat(msg).catch(() => {}) }) + + this.randomTimeout = setInterval(() => { + this.sendRandomMessage() + }, 10 * 1000) + } + + public stop (): void { + if (this.randomTimeout) { + clearInterval(this.randomTimeout) + } + } + + protected sendRandomMessage (): void { + const room = this.room + if (!room.isOnline()) { return } + // checking if there is someone to listen... + const onlineUserCount = this.room.onlineUserCount() + if (onlineUserCount < 2) { return } + const cpt = this.randomCount++ + logger.info(`Emitting the random message number ${cpt}.`) + this.room.sendGroupchat(RANDOM_MESSAGES[cpt % RANDOM_MESSAGES.length]).catch(() => {}) } } diff --git a/bots/lib/bot/room.ts b/bots/lib/bot/room.ts index bf6d3ee1..dead0e54 100644 --- a/bots/lib/bot/room.ts +++ b/bots/lib/bot/room.ts @@ -31,6 +31,14 @@ export class BotRoom extends EventEmitter { return this.state === 'online' } + public onlineUserCount (): number { + let count = 0 + this.roster.forEach(user => { + if (user.state === 'online') { count++ } + }) + return count + } + public async join (nick: string): Promise { this.userJID = new JID(this.roomJID.getLocal(), this.roomJID.getDomain(), nick) logger.debug(`Emitting a presence for room ${this.roomJID.toString()}...`) @@ -127,4 +135,10 @@ export class BotRoom extends EventEmitter { public attachHandler (handler: BotHandler): void { this.handlers.push(handler) } + + public detachHandlers (): void { + for (const handler of this.handlers) { + handler.stop() + } + } } From 8dc09307f3afa3512d98f2aa519eb51788411783 Mon Sep 17 00:00:00 2001 From: John Livingston Date: Wed, 8 Dec 2021 16:54:22 +0100 Subject: [PATCH 10/20] DemoBot: replying to mentions. --- bots/lib/bot/component.ts | 4 ++++ bots/lib/bot/handlers/demo.ts | 8 +++++++ bots/lib/bot/room.ts | 45 +++++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+) diff --git a/bots/lib/bot/component.ts b/bots/lib/bot/component.ts index fddd3d6b..f1926320 100644 --- a/bots/lib/bot/component.ts +++ b/bots/lib/bot/component.ts @@ -104,6 +104,10 @@ class BotComponent { } await room.part() } + + public getAddress (): JID | undefined { + return this.address + } } export { diff --git a/bots/lib/bot/handlers/demo.ts b/bots/lib/bot/handlers/demo.ts index a625ae5e..a5453ba1 100644 --- a/bots/lib/bot/handlers/demo.ts +++ b/bots/lib/bot/handlers/demo.ts @@ -44,6 +44,14 @@ export class BotHandlerDemo extends BotHandler { room.sendGroupchat(msg).catch(() => {}) }) + room.on('room_message', (msg: string, user?: XMPPUser, mentionned?: boolean) => { + if (!user || user.isMe) { return } + if (!room.isOnline()) { return } + if (!mentionned) { return } + + room.sendGroupchat(`Yep @${user.nick}?`).catch(() => {}) + }) + this.randomTimeout = setInterval(() => { this.sendRandomMessage() }, 10 * 1000) diff --git a/bots/lib/bot/room.ts b/bots/lib/bot/room.ts index dead0e54..126b2d3a 100644 --- a/bots/lib/bot/room.ts +++ b/bots/lib/bot/room.ts @@ -81,6 +81,9 @@ export class BotRoom extends EventEmitter { if (stanza.name === 'presence') { this.receivePresenceStanza(stanza, fromResource) } + if (stanza.name === 'message') { + this.receiveMessageStanza(stanza, fromResource) + } } public receivePresenceStanza (stanza: XMPPStanza, fromResource?: string): void { @@ -132,6 +135,48 @@ export class BotRoom extends EventEmitter { } } + protected receiveMessageStanza (stanza: XMPPStanza, fromResource?: string): void { + if (stanza.attrs.type !== 'groupchat') { + return + } + // ignoring messages send by the bot himself + if (stanza.attrs.from === this.userJID?.toString()) { + return + } + // ignoring history messages + if (stanza.getChild('delay')) { + return + } + const body = stanza.getChild('body') + // ignoring message without body (subject, ...) + if (!body) { + return + } + + let mentionned: boolean = false // I'm I mentionned? + // TODO: fix this ugly code. + if (this.userJID) { + const references = stanza.getChildren('reference') + for (const reference of references) { + if (reference.attrs.type === 'mention') { + if (reference.attrs.uri === 'xmpp:' + this.userJID.toString()) { + mentionned = true + } else { + const addr = this.component.getAddress() + if (addr) { + if (reference.attrs.uri === 'xmpp:' + addr.toString()) { + mentionned = true + } + } + } + } + } + } + + const user = fromResource ? this.roster.get(fromResource) : undefined + this.emit('room_message', body.toString(), user, mentionned) + } + public attachHandler (handler: BotHandler): void { this.handlers.push(handler) } From 605b306dc8641f4d3c60ed61147060a56857e572 Mon Sep 17 00:00:00 2001 From: John Livingston Date: Wed, 8 Dec 2021 16:56:47 +0100 Subject: [PATCH 11/20] Changelog --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dbadeb81..cd6ebb42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## (unreleased yet) + +### Features + +* Adding Bots to builtin Prosody mode! + * The DemoBot: it is a bot than can join rooms to demonstrate the plugin (it is an hidden feature). + ## v5.0.1 ### Breaking changes From ae593c26f19edd378f357513dbd14195b511a429 Mon Sep 17 00:00:00 2001 From: John Livingston Date: Wed, 8 Dec 2021 16:59:37 +0100 Subject: [PATCH 12/20] Logging. --- bots/lib/bot/handlers/demo.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/bots/lib/bot/handlers/demo.ts b/bots/lib/bot/handlers/demo.ts index a5453ba1..60350295 100644 --- a/bots/lib/bot/handlers/demo.ts +++ b/bots/lib/bot/handlers/demo.ts @@ -68,6 +68,7 @@ export class BotHandlerDemo extends BotHandler { if (!room.isOnline()) { return } // checking if there is someone to listen... const onlineUserCount = this.room.onlineUserCount() + logger.debug(`Online user count in room: ${onlineUserCount}`) if (onlineUserCount < 2) { return } const cpt = this.randomCount++ logger.info(`Emitting the random message number ${cpt}.`) From f0962e864c95ec66671d7843dbea839e27b7c331 Mon Sep 17 00:00:00 2001 From: John Livingston Date: Wed, 8 Dec 2021 17:22:54 +0100 Subject: [PATCH 13/20] Fix translation. --- CHANGELOG.md | 6 +++++- languages/es.json | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd6ebb42..38ff2741 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,13 +7,17 @@ * Adding Bots to builtin Prosody mode! * The DemoBot: it is a bot than can join rooms to demonstrate the plugin (it is an hidden feature). +### Fixes + +* Fix spanish translation. + ## v5.0.1 ### Breaking changes * If you have some CSS customization for the plugin, it may be broken. * Auto color detection can have bad result for some Peertube themes. If so, you can disable it in the settings. -* Note: if updating the plugin to v5.0.1 fails with an error like `Cannot find module 'validate-color'`, try to restart peertube, then install/update the plugin. See https://github.com/Chocobozzz/PeerTube/issues/4593 +* Note: if updating the plugin to v5.0.1 fails with an error like `Cannot find module 'validate-color'`, try to restart peertube, then install/update the plugin. See ### Features diff --git a/languages/es.json b/languages/es.json index 5723b540..8ae336cc 100644 --- a/languages/es.json +++ b/languages/es.json @@ -1,6 +1,6 @@ { "Open chat": "Abrir el chat", - "Open chat in a new window": "Abrir el chat en una venta nueva", + "Open chat in a new window": "Abrir el chat en una nueva ventana", "Close chat": "Cerrar el chat", "Use chat": false, "If enabled, there will be a chat next to the video.": false From acf7aeb06bbdc4a18539562a567d289f6456f50a Mon Sep 17 00:00:00 2001 From: John Livingston Date: Wed, 8 Dec 2021 17:38:15 +0100 Subject: [PATCH 14/20] Moderation Bot. --- ROADMAP.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ROADMAP.md b/ROADMAP.md index 1fbc3419..876e8994 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -39,3 +39,4 @@ This roadmap is given as an indication. It will be updated as we go along accord [ ] | Builtin Prosody | Add on option to limit webchat to registered users. [x] | Builtin Prosody (or all modes?) | Offer a way for users to have a webchat per channel. See [#59](https://github.com/JohnXLivingston/peertube-plugin-livechat/issues/58). | v4.0.0 [ ] | Common | Handle federation: if video is remote, then try to check if the plugin is installed on the instance, and open it. +[ ] | Builtin Prosody | Moderation Bot. From 7b239c9520e424009c698e33a20127069343fb87 Mon Sep 17 00:00:00 2001 From: John Livingston Date: Wed, 8 Dec 2021 18:12:34 +0100 Subject: [PATCH 15/20] OBS overlay with Matterbridge --- documentation/prosody.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/documentation/prosody.md b/documentation/prosody.md index 8f567a70..8efa2a04 100644 --- a/documentation/prosody.md +++ b/documentation/prosody.md @@ -116,3 +116,9 @@ If the video is local (not from a remote Peertube), the video owner will be admi You can use [ConverseJS moderation commands](https://conversejs.org/docs/html/features.html#moderating-chatrooms) to moderate the room. When you open the chat room in full screen, there will also be a menu with dedicated commands on the top right. + +## Advanced usages + +### OBS Overlay using Matterbridge + +Here is a tutorial to use Matterbridge with the plugin: From 5384050ef5a606c90fc86244524f3e9f86f9186b Mon Sep 17 00:00:00 2001 From: John Livingston Date: Wed, 8 Dec 2021 18:28:41 +0100 Subject: [PATCH 16/20] Doc. --- documentation/prosody.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/documentation/prosody.md b/documentation/prosody.md index 8efa2a04..84f3ae7f 100644 --- a/documentation/prosody.md +++ b/documentation/prosody.md @@ -122,3 +122,8 @@ When you open the chat room in full screen, there will also be a menu with dedic ### OBS Overlay using Matterbridge Here is a tutorial to use Matterbridge with the plugin: + +### Demobot + +This is a hidden feature. It is a bot that can join rooms, and demonstrate the plugin capacities. +This is not documented for now. From 252a6abb1f3eadade08303e84c496a90433ac8d0 Mon Sep 17 00:00:00 2001 From: John Livingston Date: Wed, 8 Dec 2021 18:58:50 +0100 Subject: [PATCH 17/20] DemoBot: changing the interval. --- bots/lib/bot/handlers/demo.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bots/lib/bot/handlers/demo.ts b/bots/lib/bot/handlers/demo.ts index 60350295..25d08101 100644 --- a/bots/lib/bot/handlers/demo.ts +++ b/bots/lib/bot/handlers/demo.ts @@ -54,7 +54,7 @@ export class BotHandlerDemo extends BotHandler { this.randomTimeout = setInterval(() => { this.sendRandomMessage() - }, 10 * 1000) + }, 60 * 1000) } public stop (): void { From aaab1fe6041146ae19db6641c8a7b799dca7b946 Mon Sep 17 00:00:00 2001 From: John Livingston Date: Wed, 8 Dec 2021 19:33:28 +0100 Subject: [PATCH 18/20] Fix some cases where the chat immediatly close Peertube events are not fired in the good order. This is a dirty fix: saving the current window location when initializing the chat, and comparing the saved url when navigation-end is triggered. --- CHANGELOG.md | 6 ++++++ client/common-client-plugin.ts | 9 +++++++++ client/videowatch-client-plugin.ts | 1 + 3 files changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dbadeb81..fc91b0a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## v5.0.2 + +### Fixes + +* Fix some cases where the chat immediatly close (Peertube events are not fired in the good order). + ## v5.0.1 ### Breaking changes diff --git a/client/common-client-plugin.ts b/client/common-client-plugin.ts index e5c40d82..6bee6279 100644 --- a/client/common-client-plugin.ts +++ b/client/common-client-plugin.ts @@ -5,6 +5,15 @@ async function register ({ peertubeHelpers, registerHook, registerVideoField }: handler: () => { const container = document.querySelector('#peertube-plugin-livechat-container') if (container) { + const url = container.getAttribute('peertube-plugin-livechat-current-url') + if (url && url === window.location.href) { + console.warn( + '[peertube-plugin-livechat navigation-end] ' + + 'It seems that action:router.navigation-end was called after action:video-watch.video.loaded. ' + + 'No removing the chat from the DOM.' + ) + return + } container.remove() } } diff --git a/client/videowatch-client-plugin.ts b/client/videowatch-client-plugin.ts index ef888934..db017173 100644 --- a/client/videowatch-client-plugin.ts +++ b/client/videowatch-client-plugin.ts @@ -289,6 +289,7 @@ function register ({ registerHook, peertubeHelpers }: RegisterOptions): void { container = document.createElement('div') container.setAttribute('id', 'peertube-plugin-livechat-container') container.setAttribute('peertube-plugin-livechat-state', 'initializing') + container.setAttribute('peertube-plugin-livechat-current-url', window.location.href) placeholder.append(container) peertubeHelpers.getSettings().then((s: any) => { From 0786ae51a0b7d4aeb9bcaa88e0880aee64e3e70c Mon Sep 17 00:00:00 2001 From: John Livingston Date: Wed, 8 Dec 2021 19:35:26 +0100 Subject: [PATCH 19/20] 5.0.2 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 844d9fab..5e4dde69 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "peertube-plugin-livechat", - "version": "5.0.1", + "version": "5.0.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 99a07a5b..7481786b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "peertube-plugin-livechat", "description": "PeerTube plugin livechat: offers a way to embed a chat system into Peertube.", - "version": "5.0.1", + "version": "5.0.2", "author": { "name": "John Livingston", "url": "https://github.com/JohnXLivingston" From 2e7cec04d91f42f231c540921f6ca6d2cd8fcade Mon Sep 17 00:00:00 2001 From: John Livingston Date: Sat, 11 Dec 2021 17:12:04 +0100 Subject: [PATCH 20/20] Reverting work on DemoBot (it is now an external package). --- CHANGELOG.md | 6 +- bots/.eslintrc.json | 40 ---- bots/bots.ts | 70 ------ bots/lib/bot/component.ts | 115 ---------- bots/lib/bot/handlers/base.ts | 12 -- bots/lib/bot/handlers/demo.ts | 77 ------- bots/lib/bot/room.ts | 189 ----------------- bots/lib/bot/types.ts | 13 -- bots/lib/config.ts | 88 -------- bots/lib/logger.ts | 23 -- bots/tsconfig.json | 26 --- client/admin-plugin-client-plugin.ts | 1 - documentation/prosody.md | 5 - package-lock.json | 304 +-------------------------- package.json | 7 +- server/lib/apikey.ts | 21 +- server/lib/diagnostic/prosody.ts | 57 +++-- server/lib/prosody/config.ts | 158 ++++---------- server/lib/prosody/config/bots.ts | 19 -- server/lib/prosody/config/content.ts | 32 +-- server/lib/prosody/config/paths.ts | 4 - server/lib/prosody/ctl.ts | 27 ++- server/lib/settings.ts | 16 -- 23 files changed, 89 insertions(+), 1221 deletions(-) delete mode 100644 bots/.eslintrc.json delete mode 100644 bots/bots.ts delete mode 100644 bots/lib/bot/component.ts delete mode 100644 bots/lib/bot/handlers/base.ts delete mode 100644 bots/lib/bot/handlers/demo.ts delete mode 100644 bots/lib/bot/room.ts delete mode 100644 bots/lib/bot/types.ts delete mode 100644 bots/lib/config.ts delete mode 100644 bots/lib/logger.ts delete mode 100644 bots/tsconfig.json delete mode 100644 server/lib/prosody/config/bots.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 263c33b1..a39cc63a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,14 +2,10 @@ ## (unreleased yet) -### Features - -* Adding Bots to builtin Prosody mode! - * The DemoBot: it is a bot than can join rooms to demonstrate the plugin (it is an hidden feature). - ### Fixes * Fix spanish translation. +* Hide secret keys in diagnostic tool. ## v5.0.2 diff --git a/bots/.eslintrc.json b/bots/.eslintrc.json deleted file mode 100644 index de818741..00000000 --- a/bots/.eslintrc.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "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" - } -} diff --git a/bots/bots.ts b/bots/bots.ts deleted file mode 100644 index 22aca0dd..00000000 --- a/bots/bots.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { BotsConfig } from './lib/config' -import { logger } from './lib/logger' -import { BotComponent } from './lib/bot/component' -import { BotHandlerDemo } from './lib/bot/handlers/demo' - -if (!process.argv[2]) { - throw new Error('Missing parameter: the demobot configuration file path') -} -const botsConfig = new BotsConfig(process.argv[2]) - -const runningBots: BotComponent[] = [] - -async function start (botsConfig: BotsConfig): Promise { - await botsConfig.load() - - let atLeastOne: boolean = false - if (botsConfig.useDemoBot()) { - atLeastOne = true - logger.info('Starting DemoBot...') - - const config = botsConfig.getDemoBotConfig() - const instance = new BotComponent( - 'DemoBot', - { - service: config.service, - domain: config.domain, - password: config.password - }, - config.mucDomain - ) - runningBots.push(instance) - - instance.connect().then(async () => { - for (const roomId of config.rooms) { - const room = await instance.joinRoom(roomId, 'DemoBot') - room.attachHandler(new BotHandlerDemo(room)) - } - }).catch(err => { throw err }) - } - if (!atLeastOne) { - logger.info('No bot to launch, exiting.') - process.exit(0) - } -} - -async function shutdown (): Promise { - logger.info('Shutdown...') - for (const bot of runningBots) { - logger.info('Stopping the bot ' + bot.botName + '...') - await bot.disconnect() - } - process.exit(0) -} - -// catching signals and do something before exit -['SIGHUP', 'SIGINT', 'SIGQUIT', 'SIGILL', 'SIGTRAP', 'SIGABRT', - 'SIGBUS', 'SIGFPE', 'SIGUSR1', 'SIGSEGV', 'SIGUSR2', 'SIGTERM' -].forEach((sig) => { - process.on(sig, () => { - logger.debug('Receiving signal: ' + sig) - shutdown().catch((err) => { - logger.error(`Error on shutting down: ${err as string}`) - }) - }) -}) - -start(botsConfig).catch((err) => { - logger.error(`Function start failed: ${err as string}`) - process.exit(1) -}) diff --git a/bots/lib/bot/component.ts b/bots/lib/bot/component.ts deleted file mode 100644 index f1926320..00000000 --- a/bots/lib/bot/component.ts +++ /dev/null @@ -1,115 +0,0 @@ -import type { XMPPStanza, XMPPStanzaType } from './types' -import type { Node } from '@xmpp/xml' -import { logger } from '../logger' -import { component, xml, Component, Options } from '@xmpp/component' -import { parse, JID } from '@xmpp/jid' -import { BotRoom } from './room' - -class BotComponent { - protected xmpp?: Component - protected address?: JID - public readonly xml = xml - protected rooms: Map = new Map() - - constructor ( - public readonly botName: string, - protected readonly connectionConfig: Options, - protected readonly mucDomain: string - ) {} - - public async connect (): Promise { - this.xmpp = component({ - service: this.connectionConfig.service, - domain: this.connectionConfig.domain, - password: this.connectionConfig.password - }) - - this.xmpp.on('error', (err: any) => { - logger.error(err) - }) - - this.xmpp.on('offline', () => { - logger.info(`${this.botName} is now offline.`) - }) - - this.xmpp.on('stanza', (stanza: XMPPStanza) => { - logger.debug('stanza received' + stanza.toString()) - if (!stanza.attrs.from) { return } - const jid = parse(stanza.attrs.from) - const roomJid = jid.bare() // removing the «resource» part of the jid. - const room = this.rooms.get(roomJid.toString()) - if (!room) { - return - } - room.emit('stanza', stanza, jid.getResource()) - }) - - this.xmpp.on('online', (address) => { - logger.debug('Online with address' + address.toString()) - - this.address = address - - // 'online' is emitted at reconnection, so we must reset rooms rosters - this.rooms.forEach(room => room.emit('reset')) - }) - - this.xmpp.on('offline', () => { - logger.info(`Stoppping process: ${this.botName} is now offline.`) - }) - - await this.xmpp.start() - } - - public async disconnect (): Promise { - for (const [roomId, room] of this.rooms) { - logger.debug(`Leaving room ${roomId}...`) - await room.detachHandlers() - await room.part() - } - await this.xmpp?.stop() - this.xmpp = undefined - } - - public async sendStanza ( - type: XMPPStanzaType, - attrs: object, - ...children: Node[] - ): Promise { - attrs = Object.assign({ - from: this.address?.toString() - }, attrs) - - const stanza = this.xml(type, attrs, ...children) - logger.debug('stanza to emit: ' + stanza.toString()) - await this.xmpp?.send(stanza) - } - - public async joinRoom (roomId: string, nick: string): Promise { - const roomJID = new JID(roomId, this.mucDomain) - const roomJIDstr = roomJID.toString() - let room: BotRoom | undefined = this.rooms.get(roomJIDstr) - if (!room) { - room = new BotRoom(this, roomJID) - this.rooms.set(roomJIDstr, room) - } - await room.join(nick) - return room - } - - public async partRoom (roomId: string): Promise { - const roomJID = new JID(roomId, this.mucDomain) - const room = this.rooms.get(roomJID.toString()) - if (!room) { - return - } - await room.part() - } - - public getAddress (): JID | undefined { - return this.address - } -} - -export { - BotComponent -} diff --git a/bots/lib/bot/handlers/base.ts b/bots/lib/bot/handlers/base.ts deleted file mode 100644 index 290988cd..00000000 --- a/bots/lib/bot/handlers/base.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { BotRoom } from '../room' - -export abstract class BotHandler { - constructor ( - protected readonly room: BotRoom - ) { - this.init() - } - - protected abstract init (): void - public abstract stop (): void -} diff --git a/bots/lib/bot/handlers/demo.ts b/bots/lib/bot/handlers/demo.ts deleted file mode 100644 index 25d08101..00000000 --- a/bots/lib/bot/handlers/demo.ts +++ /dev/null @@ -1,77 +0,0 @@ -import type { XMPPUser } from '../types' -import { logger } from '../../logger' -import { BotHandler } from './base' - -const RANDOM_MESSAGES: string[] = [ - '🎵🎶', - '🎵🎶 I\'m just a bot, I\'m just a bot in the world. 🎵🎶', - 'You can see who is connected by opening the right panel.', - 'This is a random message.', - 'Oh, yet another random message.', - 'You can mention a user using a @ in front of a user\'s nick. Try to mention me.' -] - -export class BotHandlerDemo extends BotHandler { - protected readonly lastHellos: Map = new Map() - protected randomCount: number = 0 - protected randomTimeout: NodeJS.Timeout | undefined - - protected init (): void { - const room = this.room - room.on('room_join', (user: XMPPUser) => { - if (user.isMe) { - return - } - if (!room.isOnline()) { - return - } - const lastHello = this.lastHellos.get(user.nick) - const now = new Date() - let msg: string - if (lastHello) { - logger.debug(`The user ${user.nick} was already seen at ${lastHello.toString()}`) - if ((now.getTime() - lastHello.getTime()) < 3600 * 1000) { // no more than one hello per hour - logger.info(`The user ${user.nick} was seen to recently, no message to send.`) - return - } - logger.info(`The user ${user.nick} was seen a long time ago, sending a message.`) - msg = `Hello ${user.nick}! Happy to see you again.` - } else { - logger.info(`The user ${user.nick} is here for the first time. Sending a message.`) - msg = `Hello ${user.nick}! I'm the DemoBot, I'm here to demonstrate the chatroom.` - } - this.lastHellos.set(user.nick, now) - room.sendGroupchat(msg).catch(() => {}) - }) - - room.on('room_message', (msg: string, user?: XMPPUser, mentionned?: boolean) => { - if (!user || user.isMe) { return } - if (!room.isOnline()) { return } - if (!mentionned) { return } - - room.sendGroupchat(`Yep @${user.nick}?`).catch(() => {}) - }) - - this.randomTimeout = setInterval(() => { - this.sendRandomMessage() - }, 60 * 1000) - } - - public stop (): void { - if (this.randomTimeout) { - clearInterval(this.randomTimeout) - } - } - - protected sendRandomMessage (): void { - const room = this.room - if (!room.isOnline()) { return } - // checking if there is someone to listen... - const onlineUserCount = this.room.onlineUserCount() - logger.debug(`Online user count in room: ${onlineUserCount}`) - if (onlineUserCount < 2) { return } - const cpt = this.randomCount++ - logger.info(`Emitting the random message number ${cpt}.`) - this.room.sendGroupchat(RANDOM_MESSAGES[cpt % RANDOM_MESSAGES.length]).catch(() => {}) - } -} diff --git a/bots/lib/bot/room.ts b/bots/lib/bot/room.ts deleted file mode 100644 index 126b2d3a..00000000 --- a/bots/lib/bot/room.ts +++ /dev/null @@ -1,189 +0,0 @@ -import type { BotComponent } from './component' -import type { BotHandler } from './handlers/base' -import type { XMPPStanza, XMPPUser } from './types' -import EventEmitter from 'events' -import { JID } from '@xmpp/jid' -import { logger } from '../logger' - -export class BotRoom extends EventEmitter { - protected state: 'offline' | 'online' = 'offline' - protected userJID: JID | undefined - protected readonly roster: Map = new Map() - - protected readonly handlers: BotHandler[] = [] - - constructor ( - protected readonly component: BotComponent, - protected readonly roomJID: JID - ) { - super() - - this.on('reset', () => { - this.state = 'offline' - this.roster.clear() - }) - this.on('stanza', (stanza: XMPPStanza, resource?: string) => { - this.receiveStanza(stanza, resource) - }) - } - - public isOnline (): boolean { - return this.state === 'online' - } - - public onlineUserCount (): number { - let count = 0 - this.roster.forEach(user => { - if (user.state === 'online') { count++ } - }) - return count - } - - public async join (nick: string): Promise { - this.userJID = new JID(this.roomJID.getLocal(), this.roomJID.getDomain(), nick) - logger.debug(`Emitting a presence for room ${this.roomJID.toString()}...`) - await this.component.sendStanza('presence', - { - to: this.userJID.toString() - }, - this.component.xml('x', { - xmlns: 'http://jabber.org/protocol/muc' - }) - ) - // FIXME: should wait for a presence stanza from the server. - // FIXME: should handle used nick errors. - } - - public async part (): Promise { - if (!this.userJID) { return } - logger.debug(`Emitting a presence=unavailable for room ${this.roomJID.toString()}...`) - await this.component.sendStanza('presence', { - to: this.userJID.toString(), - type: 'unavailable' - }) - // FIXME: should wait for a presence stanza from the server. - } - - public async sendGroupchat (msg: string): Promise { - if (!this.userJID) { return } - logger.debug(`Emitting a groupchat message for room ${this.roomJID.toString()}...`) - await this.component.sendStanza( - 'message', - { - type: 'groupchat', - to: this.roomJID.toString() - }, - this.component.xml('body', {}, msg) - ) - } - - public receiveStanza (stanza: XMPPStanza, fromResource?: string): void { - if (stanza.name === 'presence') { - this.receivePresenceStanza(stanza, fromResource) - } - if (stanza.name === 'message') { - this.receiveMessageStanza(stanza, fromResource) - } - } - - public receivePresenceStanza (stanza: XMPPStanza, fromResource?: string): void { - if (!fromResource) { - return - } - - const isPresent = stanza.attrs.type !== 'unavailable' - - const statusElems = stanza.getChild('x')?.getChildren('status') - const statusCodes = [] - if (statusElems) { - for (const s of statusElems) { - statusCodes.push(parseInt(s.attrs.code)) - } - } - const isMe = statusCodes.includes(110) // status 110 means that is concern the current user. - - let user = this.roster.get(fromResource) - const previousState = user?.state - if (!isPresent) { - if (!user) { - return - } - user.state = 'offline' - if (isMe) { - this.state = 'offline' - } - if (previousState === 'online') { - this.emit('room_part', user) - } - } else { - if (!user) { - user = { - state: 'online', - nick: fromResource, - isMe: isMe - } - this.roster.set(fromResource, user) - } else { - user.state = 'online' - } - if (isMe) { - this.state = 'online' - } - if (previousState !== 'online') { - this.emit('room_join', user) - } - } - } - - protected receiveMessageStanza (stanza: XMPPStanza, fromResource?: string): void { - if (stanza.attrs.type !== 'groupchat') { - return - } - // ignoring messages send by the bot himself - if (stanza.attrs.from === this.userJID?.toString()) { - return - } - // ignoring history messages - if (stanza.getChild('delay')) { - return - } - const body = stanza.getChild('body') - // ignoring message without body (subject, ...) - if (!body) { - return - } - - let mentionned: boolean = false // I'm I mentionned? - // TODO: fix this ugly code. - if (this.userJID) { - const references = stanza.getChildren('reference') - for (const reference of references) { - if (reference.attrs.type === 'mention') { - if (reference.attrs.uri === 'xmpp:' + this.userJID.toString()) { - mentionned = true - } else { - const addr = this.component.getAddress() - if (addr) { - if (reference.attrs.uri === 'xmpp:' + addr.toString()) { - mentionned = true - } - } - } - } - } - } - - const user = fromResource ? this.roster.get(fromResource) : undefined - this.emit('room_message', body.toString(), user, mentionned) - } - - public attachHandler (handler: BotHandler): void { - this.handlers.push(handler) - } - - public detachHandlers (): void { - for (const handler of this.handlers) { - handler.stop() - } - } -} diff --git a/bots/lib/bot/types.ts b/bots/lib/bot/types.ts deleted file mode 100644 index 5fc1dd2c..00000000 --- a/bots/lib/bot/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { Element } from '@xmpp/xml' - -export type XMPPStanzaType = 'message' | 'iq' | 'presence' - -export interface XMPPStanza extends Element { - name: XMPPStanzaType -} - -export interface XMPPUser { - state: 'offline' | 'online' - nick: string - isMe: boolean -} diff --git a/bots/lib/config.ts b/bots/lib/config.ts deleted file mode 100644 index 0f0f6048..00000000 --- a/bots/lib/config.ts +++ /dev/null @@ -1,88 +0,0 @@ -import * as path from 'path' -import * as fs from 'fs' -import decache from 'decache' -import { logger } from '../lib/logger' - -interface DemoBotConfig { - rooms: string[] - service: string - domain: string - mucDomain: string - password: string -} - -class BotsConfig { - protected readonly configDir: string - protected configs: { - demobot?: DemoBotConfig - } - - constructor (configDir: string) { - this.configDir = configDir = path.resolve(configDir) - - // Not necessary, but just in case: perform some path checking... (to limit code injection risks) - const parts = configDir.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('Bots configuration dir seems invalid (not in peertube-plugin-livechat folder).') - } - - this.configs = {} - } - - public async load (): Promise { - await this.loadDemoBot() - } - - protected async loadDemoBot (): Promise { - const configPath = path.resolve(this.configDir, 'demobot.js') - logger.debug(`Loading DemoBot config from file ${configPath}`) - if (!fs.existsSync(configPath)) { - logger.debug('The config file for DemoBot does not exist.') - delete this.configs.demobot - return - } - - decache(configPath) - - logger.debug('require DemoBot config file...') - const conf = require(configPath).getConf() as DemoBotConfig | null - if (!conf) { - logger.debug('getConf() returned null for the DemoBot.') - delete this.configs.demobot - return - } - if (!conf.rooms || !conf.domain || !conf.mucDomain || !conf.password || !conf.service) { - logger.error('Invalid DemoBot configuration: ' + JSON.stringify(conf)) - delete this.configs.demobot - return - } - - // Conf seems legit. But if there is no rooms, no need to keep it. - if (!conf.rooms.length) { - logger.debug('No room in DemoBot config.') - delete this.configs.demobot - return - } - - // TODO: detect changes? avoid reloading when not needed? or should it be by the caller? - logger.debug('Config loaded for demobot: ' + JSON.stringify(conf)) - this.configs.demobot = conf - } - - public useDemoBot (): boolean { - return (this.configs.demobot?.rooms?.length ?? 0) > 0 - } - - public getDemoBotConfig (): DemoBotConfig { - if (!this.configs.demobot) { - throw new Error('Should not call getDemoBotConfig when useDemoBot is false.') - } - return this.configs.demobot - } -} - -export { - BotsConfig -} diff --git a/bots/lib/logger.ts b/bots/lib/logger.ts deleted file mode 100644 index d014a0dc..00000000 --- a/bots/lib/logger.ts +++ /dev/null @@ -1,23 +0,0 @@ -class Logger { - public debug (s: string): void { - console.log(s) - } - - public info (s: string): void { - console.info(s) - } - - public warn (s: string): void { - console.warn(s) - } - - public error (s: string): void { - console.error(s) - } -} - -const logger = new Logger() - -export { - logger -} diff --git a/bots/tsconfig.json b/bots/tsconfig.json deleted file mode 100644 index 5872a594..00000000 --- a/bots/tsconfig.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "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": [] -} diff --git a/client/admin-plugin-client-plugin.ts b/client/admin-plugin-client-plugin.ts index 7ba4e4cc..0576420c 100644 --- a/client/admin-plugin-client-plugin.ts +++ b/client/admin-plugin-client-plugin.ts @@ -211,7 +211,6 @@ function register ({ registerHook, registerSettingsScript, peertubeHelpers }: Re case 'prosody-muc-log-by-default': case 'prosody-muc-expiration': case 'prosody-c2s': - case 'prosody-component-port': return options.formValues['chat-type'] !== ('builtin-prosody' as ChatType) case 'prosody-c2s-port': return !( diff --git a/documentation/prosody.md b/documentation/prosody.md index 84f3ae7f..8efa2a04 100644 --- a/documentation/prosody.md +++ b/documentation/prosody.md @@ -122,8 +122,3 @@ When you open the chat room in full screen, there will also be a menu with dedic ### OBS Overlay using Matterbridge Here is a tutorial to use Matterbridge with the plugin: - -### Demobot - -This is a hidden feature. It is a bot that can join rooms, and demonstrate the plugin capacities. -This is not documented for now. diff --git a/package-lock.json b/package-lock.json index 1a12b932..5e4dde69 100644 --- a/package-lock.json +++ b/package-lock.json @@ -175,15 +175,6 @@ "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==", "dev": true }, - "@types/accepts": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.5.tgz", - "integrity": "sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, "@types/async": { "version": "3.2.9", "resolved": "https://registry.npmjs.org/@types/async/-/async-3.2.9.tgz", @@ -220,24 +211,6 @@ "@types/node": "*" } }, - "@types/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-0mPF08jn9zYI0n0Q/Pnz7C4kThdSt+6LD4amsrYDDpgBfrVWa3TcCOxKX1zkGgYniGagRv8heN2cbh+CAn+uuQ==", - "dev": true - }, - "@types/cookies": { - "version": "0.7.7", - "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.7.7.tgz", - "integrity": "sha512-h7BcvPUogWbKCzBR2lY4oqaZbO3jXZksexYJVFvkrFeLgbZjQkU4x8pRq6eg2MHXQhY0McQdqmmsxRWlVAHooA==", - "dev": true, - "requires": { - "@types/connect": "*", - "@types/express": "*", - "@types/keygrip": "*", - "@types/node": "*" - } - }, "@types/express": { "version": "4.17.13", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", @@ -281,23 +254,11 @@ "form-data": "^2.5.0" } }, - "@types/http-assert": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.3.tgz", - "integrity": "sha512-FyAOrDuQmBi8/or3ns4rwPno7/9tJTijVW6aQQjK02+kOQ8zmoNg2XJtAuQhvQcy1ASJq38wirX5//9J1EqoUA==", - "dev": true - }, "@types/http-cache-semantics": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz", "integrity": "sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A==" }, - "@types/http-errors": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-1.8.1.tgz", - "integrity": "sha512-e+2rjEwK6KDaNOm5Aa9wNGgyS9oSZU/4pfSMMPYNOfjvFI0WVXm29+ITRFr6aKDvvKo7uU1jV68MW4ScsfDi7Q==", - "dev": true - }, "@types/json-schema": { "version": "7.0.9", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", @@ -310,12 +271,6 @@ "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", "dev": true }, - "@types/keygrip": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.2.tgz", - "integrity": "sha512-GJhpTepz2udxGexqos8wgaBx4I/zWIDPh/KOGEwAqtuGDkOUJu5eFvwmdBX4AmB8Odsr+9pHCQqiAqDL/yKMKw==", - "dev": true - }, "@types/keyv": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.1.tgz", @@ -324,37 +279,6 @@ "@types/node": "*" } }, - "@types/koa": { - "version": "2.13.4", - "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.13.4.tgz", - "integrity": "sha512-dfHYMfU+z/vKtQB7NUrthdAEiSvnLebvBjwHtfFmpZmB7em2N3WVQdHgnFq+xvyVgxW5jKDmjWfLD3lw4g4uTw==", - "dev": true, - "requires": { - "@types/accepts": "*", - "@types/content-disposition": "*", - "@types/cookies": "*", - "@types/http-assert": "*", - "@types/http-errors": "*", - "@types/keygrip": "*", - "@types/koa-compose": "*", - "@types/node": "*" - } - }, - "@types/koa-compose": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/@types/koa-compose/-/koa-compose-3.2.5.tgz", - "integrity": "sha512-B8nG/OoE1ORZqCkBVsup/AKcvjdgoHnfi4pZMn5UwAPCbhk/96xyv284eBYW8JlQbQ7zDmnpFr68I/40mFoIBQ==", - "dev": true, - "requires": { - "@types/koa": "*" - } - }, - "@types/ltx": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/ltx/-/ltx-3.0.1.tgz", - "integrity": "sha512-X+1EoqEcSZ45MYJmg0rfMvEyQPGydLT00HJcPant+5J3+OM0N+ZVL6BdZ1Iy4K3dA+JBGe1WP7PvTM/GtxN/XA==", - "dev": true - }, "@types/mime": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", @@ -429,119 +353,6 @@ "winston": "*" } }, - "@types/xmpp__component": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@types/xmpp__component/-/xmpp__component-0.13.0.tgz", - "integrity": "sha512-4vKLiicgkZwW8bKofmmy0BJpw3MuOW73c5hVPhUtgBPDTh9hj7wQezhpOLX3AhQFto97YpLg2GwWzhnwfSl1BA==", - "dev": true, - "requires": { - "@types/xmpp__component-core": "*", - "@types/xmpp__iq": "*", - "@types/xmpp__middleware": "*", - "@types/xmpp__reconnect": "*" - } - }, - "@types/xmpp__component-core": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@types/xmpp__component-core/-/xmpp__component-core-0.13.0.tgz", - "integrity": "sha512-K9l6SLG91kTcchW/Nt5TL9Kfe5aWDyDcHWvoFgnwvGoF4g0K737HdZMzD0DN1TP7Gb2g/JNCiK245BuDYegAbw==", - "dev": true, - "requires": { - "@types/xmpp__connection-tcp": "*", - "@types/xmpp__jid": "*", - "@types/xmpp__xml": "*" - } - }, - "@types/xmpp__connection": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@types/xmpp__connection/-/xmpp__connection-0.13.0.tgz", - "integrity": "sha512-YsvLhgOfxY3TbDTeTT0ZrToqh3IsA0nKnXk/NxTES2O6wTxn9lQDRBYNgB6lkq+D50nA8nmT3d53acb0f4Rycw==", - "dev": true, - "requires": { - "@types/xmpp__error": "*", - "@types/xmpp__events": "*", - "@types/xmpp__jid": "*", - "@types/xmpp__xml": "*" - } - }, - "@types/xmpp__connection-tcp": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@types/xmpp__connection-tcp/-/xmpp__connection-tcp-0.13.0.tgz", - "integrity": "sha512-yHvAWck6JVs0H/E2tnoUVOsFPylLj1TX4ARdm1/jFRqOPWynw36B/RU0UW1KNSC8dKA6VAhl0mTICnGUZVtcug==", - "dev": true, - "requires": { - "@types/xmpp__connection": "*", - "@types/xmpp__xml": "*" - } - }, - "@types/xmpp__error": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@types/xmpp__error/-/xmpp__error-0.13.0.tgz", - "integrity": "sha512-W+tM0UDj3toruhdjhn/VK1mtjOF+YMz+FdxgkMVi6lwCXA/uDW79elW6WbeM8zhiM92ZoVPSgD2zt9YXmrkiZQ==", - "dev": true, - "requires": { - "@types/xmpp__xml": "*" - } - }, - "@types/xmpp__events": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@types/xmpp__events/-/xmpp__events-0.13.0.tgz", - "integrity": "sha512-somi0EF9BwaBPmDQk6r1hE6dtXXjv2ztSNk/hStcfGVY9NfD9ErcopWgzzbGdeQg2/WcMNlVwfYXQfIm6w3w+A==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/xmpp__iq": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@types/xmpp__iq/-/xmpp__iq-0.13.0.tgz", - "integrity": "sha512-jy3aTixRMi8uqiIfhbkIxeWB62NTFGXKdZsYOwlgLNQ9BUimnbGR8BmZGSic5meUTPUaEEpCx/xp3AnVYADICQ==", - "dev": true, - "requires": { - "@types/koa-compose": "*", - "@types/xmpp__events": "*", - "@types/xmpp__middleware": "*", - "@types/xmpp__xml": "*" - } - }, - "@types/xmpp__jid": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@types/xmpp__jid/-/xmpp__jid-1.3.2.tgz", - "integrity": "sha512-zh5mdcBY1zNzI9XxXZxsuq/XGd6YeSwZzwQJpV5NQEtZUiSJ1+YW19+w2pELLrlV2hoMOcSf8PfPwB9ocPwIDg==", - "dev": true - }, - "@types/xmpp__middleware": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@types/xmpp__middleware/-/xmpp__middleware-0.13.0.tgz", - "integrity": "sha512-bgwIFdl5khKt/UQY4f6Ca7pEIUGQPCN3CvZ4ZuYSwp5PY9EpH32Tj/akUwfWMuMqGsybvdTeuq7ewT1ic7hsZQ==", - "dev": true, - "requires": { - "@types/koa-compose": "*", - "@types/xmpp__connection": "*", - "@types/xmpp__error": "*", - "@types/xmpp__jid": "*", - "@types/xmpp__xml": "*" - } - }, - "@types/xmpp__reconnect": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@types/xmpp__reconnect/-/xmpp__reconnect-0.13.0.tgz", - "integrity": "sha512-MGuq9Dl24iU/t1nuGp/5yUsv4yAvQk5DOARw/iPXpAjB5hCBCzzvsN2ttkw8vAVsQ5DSbpgPWI33GQ2xF2MaSQ==", - "dev": true, - "requires": { - "@types/xmpp__connection": "*", - "@types/xmpp__events": "*" - } - }, - "@types/xmpp__xml": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/@types/xmpp__xml/-/xmpp__xml-0.13.1.tgz", - "integrity": "sha512-pxRGht/JVPhIwvcFkqv3fsXc1V/qj/C+vkTD75S1whpaNslJJbmA4hphOcbynvIegKdQHxfa56d22sOtHWjDsg==", - "dev": true, - "requires": { - "@types/ltx": "*" - } - }, "@typescript-eslint/eslint-plugin": { "version": "4.29.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.29.0.tgz", @@ -879,108 +690,6 @@ "@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", @@ -3083,7 +2792,8 @@ "events": { "version": "3.3.0", "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": { "version": "1.0.3", @@ -4497,11 +4207,6 @@ "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", @@ -4615,11 +4320,6 @@ "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", diff --git a/package.json b/package.json index bcdec751..7481786b 100644 --- a/package.json +++ b/package.json @@ -31,8 +31,6 @@ "dist/assets/style.css" ], "dependencies": { - "@xmpp/component": "^0.13.0", - "@xmpp/jid": "^0.13.0", "async": "^3.2.2", "body-parser": "^1.19.0", "decache": "^4.6.0", @@ -50,8 +48,6 @@ "@types/got": "^9.6.12", "@types/node": "^16.11.6", "@types/winston": "^2.4.4", - "@types/xmpp__component": "^0.13.0", - "@types/xmpp__jid": "^1.3.2", "@typescript-eslint/eslint-plugin": "^4.29.0", "@typescript-eslint/parser": "^4.29.0", "eslint": "^7.32.0", @@ -86,7 +82,6 @@ "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", @@ -94,7 +89,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:bots", + "build": "npm-run-all -s clean:light -p build:converse build:images build:webpack build:server build:serverconverse build:prosodymodules build:styles", "lint": "npm-run-all -s lint:script lint:styles", "lint:script": "npx eslint --ext .js --ext .ts .", "lint:styles": "stylelint 'conversejs/**/*.scss' 'assets/**/*.css'", diff --git a/server/lib/apikey.ts b/server/lib/apikey.ts index 6b217412..9d5e6047 100644 --- a/server/lib/apikey.ts +++ b/server/lib/apikey.ts @@ -3,28 +3,15 @@ For internal API, we will generate an api Key that must be provided as GET parameter for every API call. */ -async function _getKey ({ storageManager }: RegisterServerOptions, key: string): Promise { - let value: string = await storageManager.getData(key) +async function getAPIKey ({ storageManager }: RegisterServerOptions): Promise { + let value: string = await storageManager.getData('APIKEY') if (!value) { value = Math.random().toString(36).slice(2, 12) - await storageManager.storeData(key, value) + await storageManager.storeData('APIKEY', value) } return value } -async function getAPIKey (options: RegisterServerOptions): Promise { - return _getKey(options, 'APIKEY') -} - -async function getExternalComponentKey (options: RegisterServerOptions, componentName: string): Promise { - if (!/^[A-Z]+$/.test(componentName)) { - throw new Error('Invalid component name: ' + componentName) - } - const key = 'EXTERNALCOMPONENTKEY_' + componentName - return _getKey(options, key) -} - export { - getAPIKey, - getExternalComponentKey + getAPIKey } diff --git a/server/lib/diagnostic/prosody.ts b/server/lib/diagnostic/prosody.ts index fafb29b5..6990339d 100644 --- a/server/lib/diagnostic/prosody.ts +++ b/server/lib/diagnostic/prosody.ts @@ -1,4 +1,4 @@ -import { getProsodyConfig, getWorkingDir } from '../prosody/config' +import { getProsodyConfig, getProsodyConfigContentForDiagnostic, getWorkingDir } from '../prosody/config' import { getProsodyAbout, testProsodyCorrectlyRunning } from '../prosody/ctl' import { newResult, TestResult } from './utils' import { getAPIKey } from '../apikey' @@ -24,6 +24,7 @@ export async function diagProsody (test: string, options: RegisterServerOptions) let prosodyHost: string try { const wantedConfig = await getProsodyConfig(options) + const filePath = wantedConfig.paths.config result.messages.push(`Prosody will run on port '${wantedConfig.port}'`) prosodyPort = wantedConfig.port @@ -49,44 +50,34 @@ export async function diagProsody (test: string, options: RegisterServerOptions) } result.messages.push(`Room content will be saved for '${wantedConfig.logExpiration.value}'`) - if (wantedConfig.bots.demobot) { - result.messages.push(`The Demo bot is active for videos: ${wantedConfig.bots.demobot.join(', ')}`) - } + await fs.promises.access(filePath, fs.constants.R_OK) // throw an error if file does not exist. + result.messages.push(`The prosody configuration file (${filePath}) exists`) + const actualContent = await fs.promises.readFile(filePath, { + encoding: 'utf-8' + }) - const configFiles = wantedConfig.getConfigFiles() - for (const configFile of configFiles) { - const filePath = configFile.path - const configFileKey = configFile.key - - await fs.promises.access(filePath, fs.constants.R_OK) // throw an error if file does not exist. - result.messages.push(`The prosody '${configFileKey}' configuration file (${filePath}) exists`) - const actualContent = await fs.promises.readFile(filePath, { - encoding: 'utf-8' - }) + result.debug.push({ + title: 'Current prosody configuration', + // we have to hide secret keys and other values. + // But here, we haven't them for actualContent. + // So we will use values in wantedConfig, hopping it is enough. + message: getProsodyConfigContentForDiagnostic(wantedConfig, actualContent) + }) + const wantedContent = wantedConfig.content + if (actualContent === wantedContent) { + result.messages.push('Prosody configuration file content is correct.') + } else { + result.messages.push('Prosody configuration file content is not correct.') result.debug.push({ - title: `Current prosody '${configFileKey}' configuration`, - // we have to hide secret keys and other values. - // But here, we haven't them for actualContent. - // So we will use values in wantedConfig, hopping it is enough. - message: wantedConfig.contentForDiagnostic(actualContent) + title: 'Prosody configuration should be', + // we have to hide secret keys and other values: + message: getProsodyConfigContentForDiagnostic(wantedConfig) }) - - const wantedContent = configFile.content - if (actualContent === wantedContent) { - result.messages.push(`Prosody configuration file '${configFileKey}' content is correct.`) - } else { - result.messages.push(`Prosody configuration file '${configFileKey}'' content is not correct.`) - result.debug.push({ - title: `Prosody configuration '${configFileKey}' should be`, - // we have to hide secret keys and other values: - message: wantedConfig.contentForDiagnostic(wantedContent) - }) - return result - } + return result } } catch (error) { - result.messages.push('Error when testing the prosody config: ' + (error as string)) + result.messages.push('Error when requiring the prosody config file: ' + (error as string)) return result } diff --git a/server/lib/prosody/config.ts b/server/lib/prosody/config.ts index 163be2dc..cab7285e 100644 --- a/server/lib/prosody/config.ts +++ b/server/lib/prosody/config.ts @@ -4,9 +4,8 @@ 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 { getAPIKey } from '../apikey' import type { ProsodyLogLevel } from './config/content' -import { parseConfigDemoBotUUIDs } from './config/bots' async function getWorkingDir (options: RegisterServerOptions): Promise { const peertubeHelpers = options.peertubeHelpers @@ -24,9 +23,9 @@ async function getWorkingDir (options: RegisterServerOptions): Promise { /** * Creates the working dir if needed, and returns it. */ -async function ensureWorkingDirs (options: RegisterServerOptions): Promise { +async function ensureWorkingDir (options: RegisterServerOptions): Promise { const logger = options.peertubeHelpers.logger - logger.debug('Calling ensureworkingDirs') + logger.debug('Calling ensureworkingDir') const paths = await getProsodyFilePaths(options) const dir = paths.dir @@ -39,12 +38,10 @@ async function ensureWorkingDirs (options: RegisterServerOptions): Promise - -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 - } + paths: ProsodyFilePaths + host: string + port: string + baseApiUrl: string + roomType: 'video' | 'channel' + logByDefault: boolean + logExpiration: ConfigLogExpiration + valuesToHideInDiagnostic: {[key: string]: string} } - async function getProsodyConfig (options: RegisterServerOptions): Promise { 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', @@ -124,25 +85,20 @@ async function getProsodyConfig (options: RegisterServerOptions): Promise 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 - } - ], + return { + content, paths, - prosodyDomain, port, baseApiUrl, + host: prosodyDomain, roomType, logByDefault, logExpiration, - bots, valuesToHideInDiagnostic - ) + } } async function writeProsodyConfig (options: RegisterServerOptions): Promise { @@ -254,19 +172,15 @@ async function writeProsodyConfig (options: RegisterServerOptions): Promise /#!demobot\b/.test(line)) - a = a.map(line => { - return line.replace(/#.*$/, '') - .replace(/^\s+/, '') - .replace(/\s+$/, '') - }) - return a.filter(line => line !== '') -} - -export { - parseConfigDemoBotUUIDs -} diff --git a/server/lib/prosody/config/content.ts b/server/lib/prosody/config/content.ts index 5c0baf17..da2e18c1 100644 --- a/server/lib/prosody/config/content.ts +++ b/server/lib/prosody/config/content.ts @@ -102,19 +102,16 @@ class ProsodyConfigVirtualHost extends ProsodyConfigBlock { class ProsodyConfigComponent extends ProsodyConfigBlock { name: string - type?: string + type: string - constructor (name: string, type?: string) { + constructor (type: string, name: string) { super(' ') this.type = type this.name = name } write (): string { - if (this.type !== undefined) { - return `Component "${this.name}" "${this.type}"\n` + super.write() - } - return `Component "${this.name}"\n` + super.write() + return `Component "${this.name}" "${this.type}"\n` + super.write() } } @@ -126,7 +123,6 @@ class ProsodyConfigContent { authenticated?: ProsodyConfigVirtualHost anon: ProsodyConfigVirtualHost muc: ProsodyConfigComponent - externalComponents: ProsodyConfigComponent[] = [] log: string prosodyDomain: string @@ -136,7 +132,7 @@ class ProsodyConfigContent { this.log = '' this.prosodyDomain = prosodyDomain this.anon = new ProsodyConfigVirtualHost('anon.' + prosodyDomain) - this.muc = new ProsodyConfigComponent('room.' + prosodyDomain, 'muc') + this.muc = new ProsodyConfigComponent('muc', 'room.' + prosodyDomain) this.global.set('daemonize', false) this.global.set('allow_registration', false) @@ -285,21 +281,6 @@ class ProsodyConfigContent { this.muc.set('peertubelivechat_test_peertube_api_url', apiurl) } - useExternalComponents (componentsPort: string): void { - this.global.set('component_ports', [componentsPort]) - this.global.set('component_interfaces', ['127.0.0.1', '::1']) - } - - useDemoBot (componentSecret: string): void { - 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(demoBotComponent) - } - setLog (level: ProsodyLogLevel, syslog?: ProsodyLogLevel[]): void { let log = '' log += 'log = {\n' @@ -328,11 +309,6 @@ class ProsodyConfigContent { content += '\n\n' content += this.muc.write() content += '\n\n' - this.externalComponents.forEach((externalComponent) => { - content += '\n\n' - content += externalComponent.write() - content += '\n\n' - }) return content } } diff --git a/server/lib/prosody/config/paths.ts b/server/lib/prosody/config/paths.ts index 40ff1d09..8683fddf 100644 --- a/server/lib/prosody/config/paths.ts +++ b/server/lib/prosody/config/paths.ts @@ -5,10 +5,6 @@ interface ProsodyFilePaths { log: string config: string data: string - bots: { - dir: string - demobot: string - } modules: string } diff --git a/server/lib/prosody/ctl.ts b/server/lib/prosody/ctl.ts index 3a54a34f..19c9e2fe 100644 --- a/server/lib/prosody/ctl.ts +++ b/server/lib/prosody/ctl.ts @@ -113,23 +113,20 @@ async function testProsodyCorrectlyRunning (options: RegisterServerOptions): Pro try { const wantedConfig = await getProsodyConfig(options) - const configFiles = wantedConfig.getConfigFiles() - for (const configFile of configFiles) { - const filePath = configFile.path + const filePath = wantedConfig.paths.config - await fs.promises.access(filePath, fs.constants.R_OK) // throw an error if file does not exist. - result.messages.push(`The prosody configuration file (${configFile.key}: ${filePath}) exists`) - const actualContent = await fs.promises.readFile(filePath, { - encoding: 'utf-8' - }) + await fs.promises.access(filePath, fs.constants.R_OK) // throw an error if file does not exist. + result.messages.push(`The prosody configuration file (${filePath}) exists`) + const actualContent = await fs.promises.readFile(filePath, { + encoding: 'utf-8' + }) - const wantedContent = configFile.content - if (actualContent === wantedContent) { - result.messages.push(`Prosody configuration file '${configFile.key}' content is correct.`) - } else { - result.messages.push(`Prosody configuration file '${configFile.key}' content is not correct.`) - return result - } + const wantedContent = wantedConfig.content + if (actualContent === wantedContent) { + result.messages.push('Prosody configuration file content is correct.') + } else { + result.messages.push('Prosody configuration file content is not correct.') + return result } } catch (error) { result.messages.push('Error when requiring the prosody config file: ' + (error as string)) diff --git a/server/lib/settings.ts b/server/lib/settings.ts index 857cdc7a..703fc7e3 100644 --- a/server/lib/settings.ts +++ b/server/lib/settings.ts @@ -359,22 +359,6 @@ archiving for a specific room, by editing its properties. ` }) - registerSetting({ - name: 'prosody-component-port', - label: 'The port to be use for external components', - type: 'input', - default: '53470', - private: true, - descriptionHTML: -`The port that will be used for extra components used by the builtin Prosody server.
-This is only used when one of these special features is used:
-
    -
  • Demo bot: this is a hidden feature, for demonstration purposes. See the documentation for more information.
  • -

-Change it if this port is already in use on your server. -` - }) - registerSetting({ name: 'prosody-c2s', label: 'Enable client to server connections',