Store bot configuration WIP

This commit is contained in:
John Livingston 2023-09-15 17:55:07 +02:00
parent 231ca3d177
commit 35c9494ed7
No known key found for this signature in database
GPG Key ID: B17B5640CE66CDBC
7 changed files with 3916 additions and 988 deletions

4618
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -37,7 +37,8 @@
"got": "^11.8.2",
"http-proxy": "^1.18.1",
"log-rotate": "^0.2.8",
"validate-color": "^2.2.1"
"validate-color": "^2.2.1",
"xmppjs-chat-bot": "^0.2.1"
},
"devDependencies": {
"@peertube/feed": "^5.1.0",

View File

@ -0,0 +1,219 @@
import type { RegisterServerOptions } from '@peertube/peertube-types'
import { RoomConf } from 'xmppjs-chat-bot'
import { getProsodyDomain } from '../prosody/config/domain'
import { RoomChannel } from '../room-channel'
import * as path from 'path'
import * as fs from 'fs'
let singleton: BotConfiguration | undefined
type RoomConfCache =
null // already loaded, but file does not exist
| RoomConf // loaded, and contains the room conf
type ChannelCommonRoomConf = Omit<RoomConf, 'local' | 'domain'>
/**
* Handles Bot configuration files.
*/
class BotConfiguration {
protected readonly options: RegisterServerOptions
protected readonly prosodyDomain: string
protected readonly confDir: string
protected readonly roomConfDir: string
protected readonly logger: {
debug: (s: string) => void
info: (s: string) => void
warn: (s: string) => void
error: (s: string) => void
}
protected readonly roomConfCache: Map<string, RoomConfCache> = new Map<string, RoomConfCache>()
constructor (params: {
options: RegisterServerOptions
prosodyDomain: string
confDir: string
roomConfDir: string
}) {
this.options = params.options
this.prosodyDomain = params.prosodyDomain
this.confDir = params.confDir
this.roomConfDir = params.roomConfDir
const logger = params.options.peertubeHelpers.logger
this.logger = {
debug: (s) => logger.debug('[BotConfiguration] ' + s),
info: (s) => logger.info('[BotConfiguration] ' + s),
warn: (s) => logger.warn('[BotConfiguration] ' + s),
error: (s) => logger.error('[BotConfiguration] ' + s)
}
}
/**
* Instanciate the BotConfiguration singleton
*/
public static async initSingleton (options: RegisterServerOptions): Promise<BotConfiguration> {
const prosodyDomain = await getProsodyDomain(options)
const confDir = path.resolve(
options.peertubeHelpers.plugin.getDataDirectoryPath(),
'bot',
prosodyDomain
)
const roomConfDir = path.resolve(
confDir,
'rooms'
)
await fs.promises.mkdir(confDir, { recursive: true })
await fs.promises.mkdir(roomConfDir, { recursive: true })
singleton = new BotConfiguration({
options,
prosodyDomain,
confDir,
roomConfDir
})
return singleton
}
/**
* Returns the singleton, of thrown an exception if it is not initialized yet.
*/
public static singleton (): BotConfiguration {
if (!singleton) {
throw new Error('BotConfiguration singleton not initialized yet')
}
return singleton
}
/**
* Recompute the room configuration, and save it to disk.
* @param roomJIDParam room JID (local part only, or full JID)
*/
public async updateChannelConf (channelId: number | string, conf: ChannelCommonRoomConf): Promise<void> {
const jids = RoomChannel.singleton().getChannelRoomJIDs(channelId)
// cloning to avoid issues:
const roomConf: RoomConf = JSON.parse(JSON.stringify(conf))
roomConf.domain = this.prosodyDomain
for (const jid of jids) {
roomConf.local = jid
if (!(roomConf.enabled ?? true)) {
// Bot disabled... If the room config file does not exist, no need to create
const current = await this._getRoomConf(jid)
if (!current) {
this.logger.debug(`Bot is disabled for channel ${channelId}, room ${jid} has not current conf, skipping`)
return
}
}
this.roomConfCache.set(jid, roomConf)
await this._writeRoomConf(jid)
}
}
/**
* Disable the bot for the room.
* Can be used when a video/channel is deleted, for example.
* @param roomJIDParam Room JID (can be local part only, or full JID)
*/
public async disableRoom (roomJIDParam: string): Promise<void> {
const roomJID = this._canonicJID(roomJIDParam)
if (!roomJID) {
this.logger.error('Invalid room JID')
return
}
const conf = await this._getRoomConf(roomJID)
if (!conf) {
// no conf, so nothing to write.
return
}
conf.enabled = false
await this._writeRoomConf(roomJID)
}
/**
* Get the room conf.
* Note: the returned object is not cloned. So it can be modified
* (and that's one reason why this is a protected method)
* The other reason why it is protected, is because it assumes roomJID is already canonical.
* @param roomJID room JID, canonic form
* @returns the room conf, or null if does not exists
*/
protected async _getRoomConf (roomJID: string): Promise<RoomConf | null> {
const cached = this.roomConfCache.get(roomJID)
if (cached !== undefined) {
return cached
}
const filePath = path.resolve(
this.roomConfDir,
roomJID + '.json'
)
let content: string
try {
content = (await fs.promises.readFile(filePath, {
encoding: 'utf-8'
})).toString()
} catch (err) {
this.logger.debug('Failed to read room conf file, assuming it does not exists')
this.roomConfCache.set(roomJID, null)
return null
}
let json: RoomConf
try {
json = JSON.parse(content) as RoomConf
} catch (err) {
this.logger.error(`Error parsing JSON file ${filePath}, assuming empty`)
this.roomConfCache.set(roomJID, null)
return null
}
this.roomConfCache.set(roomJID, json)
return json
}
protected async _writeRoomConf (roomJID: string): Promise<void> {
const conf = this.roomConfCache.get(roomJID)
if (!conf) {
throw new Error(`No conf for room ${roomJID}, cant write it`)
}
const filePath = path.resolve(
this.roomConfDir,
roomJID + '.json'
)
await fs.promises.writeFile(filePath, JSON.stringify(conf), {
encoding: 'utf-8'
})
}
protected _canonicJID (roomJID: string): string | null {
const splits = roomJID.split('@')
if (splits.length < 2) {
return roomJID
}
if (splits.length > 2) {
this.logger.error('The room JID contains multiple @, not valid')
return null
}
if (splits[1] !== this.prosodyDomain) {
this.logger.error('The room JID is not on the correct domain')
return null
}
return splits[0]
}
}
export {
BotConfiguration
}

View File

@ -1,6 +1,7 @@
import type { RegisterServerOptions } from '@peertube/peertube-types'
import type { ChannelConfiguration, ChannelInfos } from '../../../../shared/lib/types'
import { sanitizeChannelConfigurationOptions } from '../../configuration/channel/sanitize'
import { BotConfiguration } from '../../configuration/bot'
import * as fs from 'fs'
import * as path from 'path'
@ -63,6 +64,13 @@ async function storeChannelConfigurationOptions (
await fs.promises.writeFile(filePath, jsonContent, {
encoding: 'utf-8'
})
const roomConf = {
enabled: channelConfiguration.configuration.bot,
// TODO: nick
handlers: []
}
await BotConfiguration.singleton().updateChannelConf(channelInfos.id, roomConf)
}
function _getFilePath (

View File

@ -8,7 +8,7 @@ import * as fs from 'fs'
let singleton: RoomChannel | undefined
/**
* Class used to request some informations about relation between rooms and channels.
* Class used to request and store some informations about relation between rooms and channels.
*/
class RoomChannel {
protected readonly options: RegisterServerOptions
@ -380,6 +380,40 @@ class RoomChannel {
this.scheduleSync()
}
/**
* Get the channel ID for a given room.
* Returns null if not found.
* @param roomJIDParam room JID (local part, or full JID)
*/
public getRoomChannelId (roomJIDParam: string): number | null {
const roomJID = this._canonicJID(roomJIDParam)
if (!roomJID) {
this.logger.error('Invalid room JID: ' + roomJIDParam)
return null
}
return this.room2Channel.get(roomJID) ?? null
}
/**
* Returns room local JID parts for all room linked to given channel.
* @param channelId channel id
* @returns list of room JIDs local parts
*/
public getChannelRoomJIDs (channelId: number | string): string[] {
channelId = parseInt(channelId.toString())
if (isNaN(channelId)) {
this.logger.error('Invalid channelId, we wont link')
return []
}
const rooms = this.channel2Rooms.get(channelId)
if (!rooms) {
return []
}
return Array.from(rooms.keys())
}
protected _canonicJID (roomJID: string): string | null {
const splits = roomJID.split('@')
if (splits.length < 2) {

View File

@ -10,6 +10,7 @@ import { prepareProsody, ensureProsodyRunning, ensureProsodyNotRunning } from '.
import { unloadDebugMode } from './lib/debug'
import { loadLoc } from './lib/loc'
import { RoomChannel } from './lib/room-channel'
import { BotConfiguration } from './lib/configuration/bot'
import decache from 'decache'
// FIXME: Peertube unregister don't have any parameter.
@ -27,6 +28,8 @@ async function register (options: RegisterServerOptions): Promise<any> {
// First: load languages files, so we can localize strings.
await loadLoc()
// Then load the BotConfiguration singleton
await BotConfiguration.initSingleton(options)
// Then load the RoomChannel singleton
const roomChannelSingleton = await RoomChannel.initSingleton(options)
// roomChannelNeedsDataInit: if true, means that the data file does not exist (or is invalid), so we must initiate it

View File

@ -60,8 +60,8 @@ The content of the files are similar to the content sent by the front-end when s
Some parts of the plugin need a quick way to get the channel id from the room Jabber ID, or the all room Jabber ID from a channel id.
We won't use SQL queries, because we only want such information for video that have a chatroom.
So we will store in the `room-channel/muc_domain.json` file (where `muc_domain` is the actual MUC domain,
something like `room.instance.tld`) a JSON object representing these relations.
So we will store in the `room-channel/muc_domain.json` file (where `muc_domain` is the current MUC domain,
something like `room.instance.tld`) a JSON object representing these relations).
In the JSON object, keys are the channel ID (as string), values are arrays of strings representing the rooms JIDs local part (without the MUC domain).
@ -87,3 +87,16 @@ In such case, existing rooms could get lost, and we want a way to ignore them to
Note: there could be some inconsistencies, when video or rooms are deleted.
The code must take this into account, and always double check room or channel existence.
There will be some cleaning batch, to delete deprecated files.
## bot/muc_domain
The `bot/muc_domain` (where muc_domain is the current MUC domain) folder contains configuration files that are read by the moderation bot.
This bot uses the [xmppjs-chat-bot](https://github.com/JohnXLivingston/xmppjs-chat-bot) package.
Note: we include the MUC domain (`room.instance.tld`) in the filename in case the instance domain changes.
In such case, existing rooms could get lost, and we want a way to ignore them to avoid gettings errors.
### bot/muc_domain/rooms
The `bot/muc_domain/rooms` folder contains room configuration files.
See the [xmppjs-chat-bot](https://github.com/JohnXLivingston/xmppjs-chat-bot) package help for more information.