Store bot configuration WIP
This commit is contained in:
parent
231ca3d177
commit
35c9494ed7
4618
package-lock.json
generated
4618
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||
|
219
server/lib/configuration/bot.ts
Normal file
219
server/lib/configuration/bot.ts
Normal 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
|
||||
}
|
@ -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 (
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
Loading…
x
Reference in New Issue
Block a user