WIP: store and get relation between rooms and channels (refactoring)
This commit is contained in:
parent
32b52adebb
commit
c900d2d1d4
55
server/lib/configuration/channel/init.ts
Normal file
55
server/lib/configuration/channel/init.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import type { RegisterServerOptions, MVideoFullLight, VideoChannel } from '@peertube/peertube-types'
|
||||
import { RoomChannel } from '../../room-channel'
|
||||
|
||||
/**
|
||||
* Register stuffs related to channel configuration
|
||||
*/
|
||||
async function initChannelConfiguration (options: RegisterServerOptions): Promise<void> {
|
||||
const logger = options.peertubeHelpers.logger
|
||||
const registerHook = options.registerHook
|
||||
logger.info('Registring room-channel hooks...')
|
||||
|
||||
registerHook({
|
||||
target: 'action:api.video.deleted',
|
||||
handler: async (params: { video: MVideoFullLight }) => {
|
||||
// When a video is deleted, we can delete the channel2room and room2channel files.
|
||||
// Note: don't need to check if there is a chat for this video, just deleting existing files...
|
||||
const video = params.video
|
||||
logger.info(`Video ${video.uuid} deleted, removing 'channel configuration' related stuff.`)
|
||||
// Here the associated channel can be either channel.X@mucdomain or video_uuid@mucdomain.
|
||||
// In first case, nothing to do... in the second, we must delete.
|
||||
// So we don't need to check which case is effective, just delete video_uuid@mucdomain.
|
||||
try {
|
||||
RoomChannel.singleton().removeRoom(video.uuid)
|
||||
} catch (err) {
|
||||
logger.error(err)
|
||||
}
|
||||
|
||||
// Note: we don't delete the room. So that admins can check logs afterward, if any doubts.
|
||||
}
|
||||
})
|
||||
|
||||
registerHook({
|
||||
target: 'action:api.video-channel.deleted',
|
||||
handler: async (params: { channel: VideoChannel }) => {
|
||||
// When a video is deleted, we can delete the channel2room and room2channel files.
|
||||
// Note: don't need to check if there is a chat for this video, just deleting existing files...
|
||||
const channelId = params.channel.id
|
||||
logger.info(`Channel ${channelId} deleted, removing 'channel configuration' related stuff.`)
|
||||
// Here the associated channel can be either channel.X@mucdomain or video_uuid@mucdomain.
|
||||
// In first case, nothing to do... in the second, we must delete.
|
||||
// So we don't need to check which case is effective, just delete video_uuid@mucdomain.
|
||||
try {
|
||||
RoomChannel.singleton().removeChannel(channelId)
|
||||
} catch (err) {
|
||||
logger.error(err)
|
||||
}
|
||||
|
||||
// Note: we don't delete the room. So that admins can check logs afterward, if any doubts.
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export {
|
||||
initChannelConfiguration
|
||||
}
|
1
server/lib/room-channel/index.ts
Normal file
1
server/lib/room-channel/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './room-channel-class'
|
281
server/lib/room-channel/room-channel-class.ts
Normal file
281
server/lib/room-channel/room-channel-class.ts
Normal file
@ -0,0 +1,281 @@
|
||||
import type { RegisterServerOptions } from '@peertube/peertube-types'
|
||||
import { getProsodyDomain } from '../prosody/config/domain'
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs'
|
||||
|
||||
let singleton: RoomChannel | undefined
|
||||
|
||||
/**
|
||||
* Class used to request some informations about relation between rooms and channels.
|
||||
*/
|
||||
class RoomChannel {
|
||||
protected readonly options: RegisterServerOptions
|
||||
protected readonly prosodyDomain: string
|
||||
protected readonly dataFilePath: string
|
||||
protected readonly logger: {
|
||||
debug: (s: string) => void
|
||||
info: (s: string) => void
|
||||
warn: (s: string) => void
|
||||
error: (s: string) => void
|
||||
}
|
||||
|
||||
protected room2Channel: Map<string, number> = new Map<string, number>()
|
||||
protected channel2Rooms: Map<number, Map<string, true>> = new Map<number, Map<string, true>>()
|
||||
|
||||
constructor (params: {
|
||||
options: RegisterServerOptions
|
||||
prosodyDomain: string
|
||||
dataFilePath: string
|
||||
}) {
|
||||
this.options = params.options
|
||||
this.prosodyDomain = params.prosodyDomain
|
||||
this.dataFilePath = params.dataFilePath
|
||||
|
||||
const logger = params.options.peertubeHelpers.logger
|
||||
this.logger = {
|
||||
debug: (s) => logger.debug('[RoomChannel] ' + s),
|
||||
info: (s) => logger.info('[RoomChannel] ' + s),
|
||||
warn: (s) => logger.warn('[RoomChannel] ' + s),
|
||||
error: (s) => logger.error('[RoomChannel] ' + s)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Instanciate the singleton
|
||||
*/
|
||||
public static async initSingleton (options: RegisterServerOptions): Promise<RoomChannel> {
|
||||
const prosodyDomain = await getProsodyDomain(options)
|
||||
const dataFilePath = path.resolve(
|
||||
options.peertubeHelpers.plugin.getDataDirectoryPath(),
|
||||
'room-channel',
|
||||
prosodyDomain + '.json'
|
||||
)
|
||||
|
||||
singleton = new RoomChannel({
|
||||
options,
|
||||
prosodyDomain,
|
||||
dataFilePath
|
||||
})
|
||||
|
||||
return singleton
|
||||
}
|
||||
|
||||
/**
|
||||
* frees the singleton
|
||||
*/
|
||||
public static async destroySingleton (): Promise<void> {
|
||||
if (!singleton) { return }
|
||||
await singleton.sync()
|
||||
singleton = undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the singleton, or raise an exception if it is too soon.
|
||||
* @returns the singleton
|
||||
*/
|
||||
public static singleton (): RoomChannel {
|
||||
if (!singleton) {
|
||||
throw new Error('RoomChannel singleton is not initialized yet')
|
||||
}
|
||||
return singleton
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads data from the room-channel data file.
|
||||
* @return Returns true if the data where found and valid. If there is no data (or no valid data), returns false.
|
||||
*/
|
||||
public async readData (): Promise<boolean> {
|
||||
// Reading the data file (see https://livingston.frama.io/peertube-plugin-livechat/fr/technical/data/)
|
||||
|
||||
this.room2Channel.clear()
|
||||
this.channel2Rooms.clear()
|
||||
|
||||
let content: string
|
||||
try {
|
||||
content = (await fs.promises.readFile(this.dataFilePath)).toString()
|
||||
} catch (err) {
|
||||
this.logger.info('Failed reading room-channel data file (' + this.dataFilePath + '), assuming it does not exists')
|
||||
return false
|
||||
}
|
||||
content ??= '{}'
|
||||
|
||||
let data: any
|
||||
try {
|
||||
data = JSON.parse(content)
|
||||
} catch (err) {
|
||||
this.logger.error('Unable to parse the content of the room-channel data file, will start with an empty database.')
|
||||
return false
|
||||
}
|
||||
|
||||
if (typeof data !== 'object') {
|
||||
this.logger.error('Invalid room-channel data file content')
|
||||
return false
|
||||
}
|
||||
|
||||
for (const k in data) {
|
||||
if (!/^\d+$/.test(k)) {
|
||||
this.logger.error('Invalid channel ID type, should be a number, dropping')
|
||||
continue
|
||||
}
|
||||
const channelId = parseInt(k)
|
||||
const rooms = data[k]
|
||||
if (!Array.isArray(rooms)) {
|
||||
this.logger.error('Invalid room list for Channel ' + channelId.toString() + ', dropping')
|
||||
continue
|
||||
}
|
||||
|
||||
const c2r = new Map<string, true>()
|
||||
this.channel2Rooms.set(channelId, c2r)
|
||||
|
||||
for (const jid of rooms) {
|
||||
if (typeof jid !== 'string') {
|
||||
this.logger.error('Invalid room jid for Channel ' + channelId.toString() + ', dropping')
|
||||
continue
|
||||
}
|
||||
c2r.set(jid, true)
|
||||
this.room2Channel.set(jid, channelId)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuilt the data from scratch.
|
||||
* Can be used for the initial migration.
|
||||
*/
|
||||
public async rebuildData (): Promise<void> {
|
||||
this.logger.error('rebuildData Not implemented yet')
|
||||
await this.sync() // FIXME: or maybe scheduleSync ?
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncs data to disk.
|
||||
*/
|
||||
public async sync (): Promise<void> {
|
||||
this.logger.error('sync Not implemented yet')
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules a sync.
|
||||
* Each times data are modified, we can schedule a sync, but we don't have to wait the file writing to be done.
|
||||
*/
|
||||
public scheduleSync (): void {
|
||||
this.logger.error('scheduleSync Not implemented yet')
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a relation between room and channel id
|
||||
* @param channelId The channel ID
|
||||
* @param roomJID The room JID. Can be the local part only, or the full JID.
|
||||
* In the second case, the domain will be checked.
|
||||
*/
|
||||
public link (channelId: number | string, roomJIDParam: string): void {
|
||||
channelId = parseInt(channelId.toString())
|
||||
if (isNaN(channelId)) {
|
||||
this.logger.error('Invalid channelId, we wont link')
|
||||
return
|
||||
}
|
||||
|
||||
const roomJID = this._canonicJID(roomJIDParam)
|
||||
if (!roomJID) {
|
||||
this.logger.error('Invalid room JID, we wont link')
|
||||
return
|
||||
}
|
||||
|
||||
// First, if the room was linked to another channel, we must unlink.
|
||||
const previousChannelId = this.room2Channel.get(roomJID)
|
||||
if (previousChannelId) {
|
||||
this.room2Channel.delete(roomJID)
|
||||
const previousRooms = this.channel2Rooms.get(previousChannelId)
|
||||
if (previousRooms) {
|
||||
previousRooms.delete(roomJID)
|
||||
}
|
||||
}
|
||||
|
||||
this.room2Channel.set(roomJID, channelId)
|
||||
let rooms = this.channel2Rooms.get(channelId)
|
||||
if (!rooms) {
|
||||
rooms = new Map<string, true>()
|
||||
this.channel2Rooms.set(channelId, rooms)
|
||||
}
|
||||
rooms.set(roomJID, true)
|
||||
|
||||
this.scheduleSync()
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all relations for this room
|
||||
* @param roomJID the room JID
|
||||
*/
|
||||
public removeRoom (roomJIDParam: string): void {
|
||||
const roomJID = this._canonicJID(roomJIDParam)
|
||||
if (!roomJID) {
|
||||
this.logger.error('Invalid room JID, we wont link')
|
||||
return
|
||||
}
|
||||
|
||||
const channelId = this.room2Channel.get(roomJID)
|
||||
if (channelId) {
|
||||
const rooms = this.channel2Rooms.get(channelId)
|
||||
if (rooms) {
|
||||
rooms.delete(roomJID)
|
||||
}
|
||||
}
|
||||
|
||||
this.room2Channel.delete(roomJID)
|
||||
|
||||
this.scheduleSync()
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all relations for this channel
|
||||
* @param channelId the channel id
|
||||
*/
|
||||
public removeChannel (channelId: number | string): void {
|
||||
channelId = parseInt(channelId.toString())
|
||||
if (isNaN(channelId)) {
|
||||
this.logger.error('Invalid channelId, we wont remove')
|
||||
return
|
||||
}
|
||||
|
||||
const rooms = this.channel2Rooms.get(channelId)
|
||||
if (rooms) {
|
||||
for (const jid of rooms.keys()) {
|
||||
// checking the consistency... only removing if the channel is the current one
|
||||
if (this.room2Channel.get(jid) === channelId) {
|
||||
this.room2Channel.delete(jid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.channel2Rooms.delete(channelId)
|
||||
|
||||
this.scheduleSync()
|
||||
}
|
||||
|
||||
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 {
|
||||
RoomChannel
|
||||
}
|
||||
|
||||
// TODO: schedule rebuild every X hours/days
|
||||
// TODO: write to disk, debouncing writes
|
||||
// TODO: only write if there is data changes
|
@ -1,114 +0,0 @@
|
||||
import { RegisterServerOptions } from '@peertube/peertube-types'
|
||||
import { getProsodyDomain } from '../prosody/config/domain'
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs'
|
||||
|
||||
/**
|
||||
* Stores that given room is related to given channel.
|
||||
* Can throw an exception.
|
||||
* @param options server options
|
||||
* @param channelId channel ID
|
||||
* @param roomJIDLocalPart room JID (only the local part)
|
||||
*/
|
||||
async function setChannel2Room (
|
||||
options: RegisterServerOptions,
|
||||
channelId: number,
|
||||
roomJIDLocalPart: string
|
||||
): Promise<void> {
|
||||
const logger = options.peertubeHelpers.logger
|
||||
logger.info(`Calling setChannel2Room for channel ${channelId} and room ${roomJIDLocalPart}...`)
|
||||
|
||||
_checkParameters(channelId, roomJIDLocalPart)
|
||||
|
||||
const prosodyDomain = await getProsodyDomain(options)
|
||||
|
||||
{
|
||||
const [channel2roomDir, channel2room] = await _getFilePath(
|
||||
options, channelId, roomJIDLocalPart, prosodyDomain, 'channel2room'
|
||||
)
|
||||
await fs.promises.mkdir(channel2roomDir, {
|
||||
recursive: true
|
||||
})
|
||||
await fs.promises.writeFile(
|
||||
channel2room,
|
||||
''
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
const [room2channelDir, room2channel, room2channelFile] = await _getFilePath(
|
||||
options, channelId, roomJIDLocalPart, prosodyDomain, 'room2channel'
|
||||
)
|
||||
await fs.promises.mkdir(room2channelDir, {
|
||||
recursive: true
|
||||
})
|
||||
|
||||
// The video's channel could have changed. We must delete any deprecated file.
|
||||
const previousFiles = await fs.promises.readdir(room2channelDir)
|
||||
for (const filename of previousFiles) {
|
||||
if (filename !== room2channelFile) {
|
||||
const p = path.resolve(room2channelDir, filename)
|
||||
logger.info('Cleaning a deprecated room2channelFile: ' + p)
|
||||
await fs.promises.unlink(p)
|
||||
}
|
||||
}
|
||||
|
||||
await fs.promises.writeFile(
|
||||
room2channel,
|
||||
''
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function _checkParameters (channelId: number | string, roomJIDLocalPart: string): void {
|
||||
channelId = channelId.toString()
|
||||
if (!/^\d+$/.test(channelId)) {
|
||||
throw new Error(`Invalid Channel ID: ${channelId}`)
|
||||
}
|
||||
|
||||
if (!/^[\w-.]+$/.test(roomJIDLocalPart)) { // channel.X or video uuid
|
||||
throw new Error(`Invalid ROOM JID: ${channelId}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function _getFilePath (
|
||||
options: RegisterServerOptions,
|
||||
channelId: number | string,
|
||||
roomJIDLocalPart: string,
|
||||
prosodyDomain: string,
|
||||
way: 'channel2room' | 'room2channel'
|
||||
): Promise<[string, string, string]> {
|
||||
channelId = channelId.toString()
|
||||
|
||||
const roomJID = roomJIDLocalPart + '@' + prosodyDomain
|
||||
|
||||
if (way === 'channel2room') {
|
||||
const dir = path.resolve(
|
||||
options.peertubeHelpers.plugin.getDataDirectoryPath(),
|
||||
'channel2room',
|
||||
channelId
|
||||
)
|
||||
return [
|
||||
dir,
|
||||
path.resolve(dir, roomJID),
|
||||
roomJID
|
||||
]
|
||||
} else if (way === 'room2channel') {
|
||||
const dir = path.resolve(
|
||||
options.peertubeHelpers.plugin.getDataDirectoryPath(),
|
||||
'room2channel',
|
||||
roomJID
|
||||
)
|
||||
return [
|
||||
dir,
|
||||
path.resolve(dir, channelId),
|
||||
channelId
|
||||
]
|
||||
} else {
|
||||
throw new Error('Invalid way parameter')
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
setChannel2Room
|
||||
}
|
@ -6,7 +6,7 @@ import { getCheckAPIKeyMiddleware } from '../../middlewares/apikey'
|
||||
import { Affiliations, getVideoAffiliations, getChannelAffiliations } from '../../prosody/config/affiliations'
|
||||
import { fillVideoCustomFields } from '../../custom-fields'
|
||||
import { getChannelInfosById } from '../../database/channel'
|
||||
import { setChannel2Room } from '../../room/channel'
|
||||
import { RoomChannel } from '../../room-channel'
|
||||
|
||||
// See here for description: https://modules.prosody.im/mod_muc_http_defaults.html
|
||||
interface RoomDefaults {
|
||||
@ -80,7 +80,7 @@ async function initRoomApiRouter (options: RegisterServerOptions, router: Router
|
||||
affiliations: affiliations
|
||||
}
|
||||
|
||||
await setChannel2Room(options, channelId, jid)
|
||||
await RoomChannel.singleton().link(channelId, jid)
|
||||
|
||||
res.json(roomDefaults)
|
||||
} else {
|
||||
@ -132,7 +132,7 @@ async function initRoomApiRouter (options: RegisterServerOptions, router: Router
|
||||
affiliations: affiliations
|
||||
}
|
||||
|
||||
await setChannel2Room(options, video.channelId, jid)
|
||||
await RoomChannel.singleton().link(video.channelId, jid)
|
||||
|
||||
res.json(roomDefaults)
|
||||
}
|
||||
|
@ -4,10 +4,12 @@ import { initSettings } from './lib/settings'
|
||||
import { initCustomFields } from './lib/custom-fields'
|
||||
import { initRouters } from './lib/routers/index'
|
||||
import { initFederation } from './lib/federation/init'
|
||||
import { initChannelConfiguration } from './lib/configuration/channel/init'
|
||||
import { initRSS } from './lib/rss/init'
|
||||
import { prepareProsody, ensureProsodyRunning, ensureProsodyNotRunning } from './lib/prosody/ctl'
|
||||
import { unloadDebugMode } from './lib/debug'
|
||||
import { loadLoc } from './lib/loc'
|
||||
import { RoomChannel } from './lib/room-channel'
|
||||
import decache from 'decache'
|
||||
|
||||
// FIXME: Peertube unregister don't have any parameter.
|
||||
@ -16,6 +18,7 @@ let OPTIONS: RegisterServerOptions | undefined
|
||||
|
||||
async function register (options: RegisterServerOptions): Promise<any> {
|
||||
OPTIONS = options
|
||||
const logger = options.peertubeHelpers.logger
|
||||
|
||||
// This is a trick to check that peertube is at least in version 3.2.0
|
||||
if (!options.peertubeHelpers.plugin) {
|
||||
@ -24,6 +27,10 @@ async function register (options: RegisterServerOptions): Promise<any> {
|
||||
|
||||
// First: load languages files, so we can localize strings.
|
||||
await loadLoc()
|
||||
// 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
|
||||
const roomChannelNeedsDataInit = !await roomChannelSingleton.readData()
|
||||
|
||||
await migrateSettings(options)
|
||||
|
||||
@ -31,11 +38,21 @@ async function register (options: RegisterServerOptions): Promise<any> {
|
||||
await initCustomFields(options)
|
||||
await initRouters(options)
|
||||
await initFederation(options)
|
||||
await initChannelConfiguration(options)
|
||||
await initRSS(options)
|
||||
|
||||
try {
|
||||
await prepareProsody(options)
|
||||
await ensureProsodyRunning(options)
|
||||
|
||||
if (roomChannelNeedsDataInit) {
|
||||
logger.info('The RoomChannel singleton has not found data, we must rebuild')
|
||||
// no need to wait here, can be done without await.
|
||||
roomChannelSingleton.rebuildData().then(
|
||||
() => { logger.info('RoomChannel singleton rebuild done') },
|
||||
(reason) => { logger.error('RoomChannel singleton rebuild failed: ' + (reason as string)) }
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
options.peertubeHelpers.logger.error('Error when launching Prosody: ' + (error as string))
|
||||
}
|
||||
@ -52,6 +69,8 @@ async function unregister (): Promise<any> {
|
||||
|
||||
unloadDebugMode()
|
||||
|
||||
await RoomChannel.destroySingleton()
|
||||
|
||||
const module = __filename
|
||||
OPTIONS?.peertubeHelpers.logger.info(`Unloading module ${module}...`)
|
||||
// Peertube calls decache(plugin) on register, not unregister.
|
||||
|
@ -55,28 +55,34 @@ The `channelConfigurationOptions` folder contains JSON files describing channels
|
||||
Filenames are like `1.json` where `1` is the channel id.
|
||||
The content of the files are similar to the content sent by the front-end when saving these configuration.
|
||||
|
||||
## channel2room and room2channel
|
||||
## room-channel/muc_domain.json
|
||||
|
||||
Some parts of the plugin need a quick way to get the channel id from the room id, or the all room id from a channel id.
|
||||
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 have 2 folders: `channel2room` and `room2channel`.
|
||||
When a chatroom is created, we create 2 empty files:
|
||||
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.
|
||||
|
||||
* `channel2room/channel_id/room_id@muc_domain`
|
||||
* `room2channel/room_id@muc_domain/channel_id`
|
||||
In the JSON object, keys are the channel ID, values are arrays of strings representing the rooms JIDs local part (without the MUC domain).
|
||||
|
||||
Where:
|
||||
When a chatroom is created, the corresponding entry will be added.
|
||||
|
||||
* `muc_domain` is the room's domain (should be `room.your_instance.tld`)
|
||||
* `channel_id` is the channel numerical id
|
||||
* `room_id` is the local part of the room JID
|
||||
Here is a sample file:
|
||||
|
||||
So we can easily list all rooms for a given channel id, just by listing files in `channel2room`.
|
||||
Or get the channel id for a room JID (Jabber ID).
|
||||
```json
|
||||
{
|
||||
1: [
|
||||
"8df24108-6e70-4fc8-b1cc-f2db7fcdd535"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Note: we include muc_domain, 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.
|
||||
This file is loaded at the plugin startup into an object that can manipulate these data.
|
||||
|
||||
So we can easily list all rooms for a given channel id or get the channel id for a room JID (Jabber ID).
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
Loading…
x
Reference in New Issue
Block a user