WIP: store and get relation between rooms and channels:

* rebuildData
* handling video update (to check for channel changes)
This commit is contained in:
John Livingston 2023-09-11 17:38:31 +02:00
parent ea1c008ded
commit e4683cf282
No known key found for this signature in database
GPG Key ID: B17B5640CE66CDBC
6 changed files with 221 additions and 32 deletions

View File

@ -1,5 +1,7 @@
import type { RegisterServerOptions, MVideoFullLight, VideoChannel } from '@peertube/peertube-types'
import { RoomChannel } from '../../room-channel'
import { fillVideoCustomFields } from '../../custom-fields'
import { videoHasWebchat } from '../../../../shared/lib/video'
/**
* Register stuffs related to channel configuration
@ -15,6 +17,7 @@ async function initChannelConfiguration (options: RegisterServerOptions): Promis
// 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
if (video.remote) { return }
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.
@ -34,6 +37,7 @@ async function initChannelConfiguration (options: RegisterServerOptions): Promis
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...
if (!params.channel.isLocal) { return }
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.
@ -48,6 +52,52 @@ async function initChannelConfiguration (options: RegisterServerOptions): Promis
// Note: we don't delete the room. So that admins can check logs afterward, if any doubts.
}
})
registerHook({
target: 'action:api.video.updated',
handler: async (params: { video: MVideoFullLight }) => {
// When a video is updated, the channel could change.
// So we ensure the room-channel link is ok.
// But we can only do this if the video has a chatroom!
const video = params.video
logger.info(`Video ${video.uuid} updated, updating room-channel informations.`)
try {
if (video.remote) { return }
const settings = await options.settingsManager.getSettings([
'chat-per-live-video',
'chat-all-lives',
'chat-all-non-lives',
'chat-videos-list',
'prosody-room-type'
])
await fillVideoCustomFields(options, video)
const hasChat = await videoHasWebchat({
'chat-per-live-video': !!settings['chat-per-live-video'],
'chat-all-lives': !!settings['chat-all-lives'],
'chat-all-non-lives': !!settings['chat-all-non-lives'],
'chat-videos-list': settings['chat-videos-list'] as string
}, video)
if (!hasChat) {
logger.debug(`Video ${video.uuid} has no chat, skipping`)
return
}
let roomLocalPart: string
if (settings['prosody-room-type'] === 'channel') {
roomLocalPart = 'channel.' + video.channelId.toString()
} else {
roomLocalPart = video.uuid
}
logger.debug(`Ensuring a room-channel link between room ${roomLocalPart} and channel ${video.channelId}`)
RoomChannel.singleton().link(video.channelId, roomLocalPart)
} catch (err) {
logger.error(err)
}
}
})
}
export {

View File

@ -0,0 +1,41 @@
interface ProsodyHost {
host: string
port: string
}
let current: ProsodyHost | undefined
/**
* When loading Prosody, keep track of the current host and port.
* @param host host
* @param port port
*/
function setCurrentProsody (host: string, port: string): void {
current = {
host,
port
}
}
/**
* When stopping Prosody, delete current host and port.
*/
function delCurrentProsody (): void {
current = undefined
}
/**
* Get the current Prosody host infos.
* @returns Prosody host info
*/
function getCurrentProsody (): ProsodyHost | null {
// cloning to avoid issues
if (!current) { return null }
return Object.assign({}, current)
}
export {
setCurrentProsody,
delCurrentProsody,
getCurrentProsody
}

View File

@ -0,0 +1,41 @@
import type { RegisterServerOptions } from '@peertube/peertube-types'
import { getCurrentProsody } from './host'
import { getAPIKey } from '../../apikey'
const got = require('got')
interface ProsodyRoomDesc {
jid: string
localpart: string
name: string
lang: string
description: string
lasttimestamp?: number
}
async function listProsodyRooms (options: RegisterServerOptions): Promise<ProsodyRoomDesc[]> {
const logger = options.peertubeHelpers.logger
const currentProsody = getCurrentProsody()
if (!currentProsody) {
throw new Error('It seems that prosody is not binded... Cant list rooms.')
}
// Requesting on localhost, because currentProsody.host does not always resolves correctly (docker use case, ...)
const apiUrl = `http://localhost:${currentProsody.port}/peertubelivechat_list_rooms/list-rooms`
logger.debug('Calling list rooms API on url: ' + apiUrl)
const rooms = await got(apiUrl, {
method: 'GET',
headers: {
authorization: 'Bearer ' + await getAPIKey(options),
host: currentProsody.host
},
responseType: 'json',
resolveBodyOnly: true
})
return rooms
}
export {
listProsodyRooms
}

View File

@ -1,5 +1,7 @@
import type { RegisterServerOptions } from '@peertube/peertube-types'
import { getProsodyDomain } from '../prosody/config/domain'
import { listProsodyRooms } from '../prosody/api/list-rooms'
import { getChannelInfosById } from '../database/channel'
import * as path from 'path'
import * as fs from 'fs'
@ -21,6 +23,7 @@ class RoomChannel {
protected room2Channel: Map<string, number> = new Map<string, number>()
protected channel2Rooms: Map<number, Map<string, true>> = new Map<number, Map<string, true>>()
protected needSync: boolean = false
constructor (params: {
options: RegisterServerOptions
@ -115,6 +118,7 @@ class RoomChannel {
protected _readData (data: any): boolean {
this.room2Channel.clear()
this.channel2Rooms.clear()
this.needSync = true
if (typeof data !== 'object') {
this.logger.error('Invalid room-channel data file content')
@ -152,9 +156,56 @@ class RoomChannel {
/**
* Rebuilt the data from scratch.
* Can be used for the initial migration.
* Can also be scheduled daily, or on an admin action (not sure it will be done, at the time of the writing).
*/
public async rebuildData (): Promise<void> {
this.logger.error('rebuildData Not implemented yet')
const data: any = {}
const rooms = await listProsodyRooms(this.options)
for (const room of rooms) {
let channelId: string | number | undefined
const matches = room.localpart.match(/^channel\.(\d+)$/)
if (matches?.[1]) {
channelId = parseInt(matches[1])
if (isNaN(channelId)) {
this.logger.error(`Invalid room JID '${room.localpart}'`)
continue
}
// Checking that channel still exists
const channelInfos = await getChannelInfosById(this.options, channelId)
if (!channelInfos) {
this.logger.debug(
`Ignoring room ${room.localpart}, because channel ${channelId} seems to not exist anymore`
)
continue
}
} else {
const uuid = room.localpart
const video = await this.options.peertubeHelpers.videos.loadByIdOrUUID(uuid)
if (!video) {
this.logger.debug(
`Ignoring room ${room.localpart}, because video ${uuid} seems to not exist anymore`
)
continue
}
channelId = video.channelId
}
if (!channelId) {
this.logger.error(`Did not find channelId for ${room.localpart}`)
continue
}
channelId = channelId.toString()
if (!(channelId in data)) {
this.logger.debug(`Room ${room.localpart} is associated to channel ${channelId}`)
data[channelId] = [room.localpart]
}
}
// This part must be done atomicly:
this._readData(data)
await this.sync() // FIXME: or maybe scheduleSync ?
}
@ -162,7 +213,9 @@ class RoomChannel {
* Syncs data to disk.
*/
public async sync (): Promise<void> {
if (!this.needSync) { return }
this.logger.error('sync Not implemented yet')
this.needSync = false // Note: must be done at the right moment
}
/**
@ -170,6 +223,7 @@ class RoomChannel {
* 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 {
if (!this.needSync) { return }
this.logger.error('scheduleSync Not implemented yet')
}
@ -195,20 +249,31 @@ class RoomChannel {
// First, if the room was linked to another channel, we must unlink.
const previousChannelId = this.room2Channel.get(roomJID)
if (previousChannelId) {
this.room2Channel.delete(roomJID)
if (this.room2Channel.delete(roomJID)) {
this.needSync = true
}
const previousRooms = this.channel2Rooms.get(previousChannelId)
if (previousRooms) {
previousRooms.delete(roomJID)
if (previousRooms.delete(roomJID)) {
this.needSync = true
}
}
}
this.room2Channel.set(roomJID, channelId)
if (this.room2Channel.get(roomJID) !== channelId) {
this.room2Channel.set(roomJID, channelId)
this.needSync = true
}
let rooms = this.channel2Rooms.get(channelId)
if (!rooms) {
rooms = new Map<string, true>()
this.channel2Rooms.set(channelId, rooms)
this.needSync = true
}
if (!rooms.has(roomJID)) {
rooms.set(roomJID, true)
this.needSync = true
}
rooms.set(roomJID, true)
this.scheduleSync()
}
@ -228,11 +293,15 @@ class RoomChannel {
if (channelId) {
const rooms = this.channel2Rooms.get(channelId)
if (rooms) {
rooms.delete(roomJID)
if (rooms.delete(roomJID)) {
this.needSync = true
}
}
}
this.room2Channel.delete(roomJID)
if (this.room2Channel.delete(roomJID)) {
this.needSync = true
}
this.scheduleSync()
}
@ -254,11 +323,14 @@ class RoomChannel {
// checking the consistency... only removing if the channel is the current one
if (this.room2Channel.get(jid) === channelId) {
this.room2Channel.delete(jid)
this.needSync = true
}
}
}
this.channel2Rooms.delete(channelId)
if (this.channel2Rooms.delete(channelId)) {
this.needSync = true
}
this.scheduleSync()
}
@ -287,4 +359,3 @@ export {
// TODO: schedule rebuild every X hours/days
// TODO: write to disk, debouncing writes
// TODO: only write if there is data changes

View File

@ -4,13 +4,13 @@ import type { ProsodyListRoomsResult, ProsodyListRoomsResultRoom } from '../../.
import { createProxyServer } from 'http-proxy'
import { RegisterServerOptionsV5, isUserAdmin } from '../helpers'
import { asyncMiddleware } from '../middlewares/async'
import { getAPIKey } from '../apikey'
import { getChannelInfosById } from '../database/channel'
import { isAutoColorsAvailable, areAutoColorsValid, AutoColors } from '../../../shared/lib/autocolors'
import { fetchMissingRemoteServerInfos } from '../federation/fetch-infos'
import { getConverseJSParams } from '../conversejs/params'
import { setCurrentProsody, delCurrentProsody } from '../prosody/api/host'
import { getChannelInfosById } from '../database/channel'
import { listProsodyRooms } from '../prosody/api/list-rooms'
import * as path from 'path'
const got = require('got')
const fs = require('fs').promises
@ -18,7 +18,6 @@ interface ProsodyProxyInfo {
host: string
port: string
}
let currentProsodyProxyInfo: ProsodyProxyInfo | null = null
let currentHttpBindProxy: ReturnType<typeof createProxyServer> | null = null
let currentWebsocketProxy: ReturnType<typeof createProxyServer> | null = null
let currentS2SWebsocketProxy: ReturnType<typeof createProxyServer> | null = null
@ -218,21 +217,8 @@ async function initWebchatRouter (options: RegisterServerOptionsV5): Promise<Rou
return
}
if (!currentProsodyProxyInfo) {
throw new Error('It seems that prosody is not binded... Cant list rooms.')
}
const apiUrl = `http://localhost:${currentProsodyProxyInfo.port}/peertubelivechat_list_rooms/list-rooms`
peertubeHelpers.logger.debug('Calling list rooms API on url: ' + apiUrl)
const rooms = await got(apiUrl, {
method: 'GET',
headers: {
authorization: 'Bearer ' + await getAPIKey(options),
host: currentProsodyProxyInfo.host
},
responseType: 'json',
resolveBodyOnly: true
})
const rooms = await listProsodyRooms(options)
// For the frontend, we are adding channel data if the room is channel specific
if (Array.isArray(rooms)) {
for (let i = 0; i < rooms.length; i++) {
const room: ProsodyListRoomsResultRoom = rooms[i]
@ -268,7 +254,7 @@ async function disableProxyRoute ({ peertubeHelpers }: RegisterServerOptions): P
// But this seems to never happen, and stucked the plugin uninstallation.
// So I don't wait.
try {
currentProsodyProxyInfo = null
delCurrentProsody()
if (currentHttpBindProxy) {
peertubeHelpers.logger.info('Closing the http bind proxy...')
currentHttpBindProxy.close()
@ -299,7 +285,7 @@ async function enableProxyRoute (
logger.error(`Port '${prosodyProxyInfo.port}' is not valid. Aborting.`)
return
}
currentProsodyProxyInfo = prosodyProxyInfo
setCurrentProsody(prosodyProxyInfo.host, prosodyProxyInfo.port)
logger.info('Creating a new http bind proxy')
currentHttpBindProxy = createProxyServer({

View File

@ -63,7 +63,7 @@ We won't use SQL queries, because we only want such information for video that h
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.
In the JSON object, keys are the channel ID, values are arrays of strings representing the rooms JIDs local part (without the MUC domain).
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).
When a chatroom is created, the corresponding entry will be added.
@ -71,7 +71,7 @@ Here is a sample file:
```json
{
1: [
"1": [
"8df24108-6e70-4fc8-b1cc-f2db7fcdd535"
]
}