diff --git a/client/admin-plugin-client-plugin.ts b/client/admin-plugin-client-plugin.ts index ea5b6cbd..d591f940 100644 --- a/client/admin-plugin-client-plugin.ts +++ b/client/admin-plugin-client-plugin.ts @@ -167,6 +167,7 @@ function register ({ registerHook, registerSettingsScript, peertubeHelpers }: Re switch (name) { case 'chat-type-help-disabled': return options.formValues['chat-type'] !== ('disabled' as ChatType) + case 'prosody-room-type': case 'prosody-port': case 'prosody-peertube-uri': case 'chat-type-help-builtin-prosody': diff --git a/documentation/prosody.md b/documentation/prosody.md index e7aaed4e..20cb4536 100644 --- a/documentation/prosody.md +++ b/documentation/prosody.md @@ -52,6 +52,10 @@ You can find the source for this Dockerfile [here](../docker/Dockerfile.buster). Just select «Prosody server controlled by Peertube» as chat mode. +### Room type + +You can choose here to have separate rooms for each video, or to group them by channel. + #### Prosody port This is the port that the Prosody server will use. By default it is set to 52800. If you want to use another port, just change the value here. diff --git a/server/lib/database/channel.ts b/server/lib/database/channel.ts index f71fa2b7..542d8a5e 100644 --- a/server/lib/database/channel.ts +++ b/server/lib/database/channel.ts @@ -45,7 +45,43 @@ async function getUserNameByChannelId (options: RegisterServerOptions, channelId return results[0].username ?? null } +interface ChannelInfos { + id: number + name: string + displayName: string +} + +async function getChannelInfosById (options: RegisterServerOptions, channelId: number): Promise { + if (!channelId) { + throw new Error('Missing channelId') + } + if (!Number.isInteger(channelId)) { + throw new Error('Invalid channelId: not an integer') + } + const [results] = await options.peertubeHelpers.database.query( + 'SELECT' + + ' "actor"."preferredUsername" as "channelName", ' + + ' "videoChannel"."name" as "channelDisplayName"' + + ' FROM "videoChannel"' + + ' RIGHT JOIN "actor" ON "actor"."id" = "videoChannel"."actorId"' + + ' WHERE "videoChannel"."id" = ' + channelId.toString() + ) + if (!Array.isArray(results)) { + throw new Error('getChannelInfosById: query result is not an array.') + } + if (!results[0]) { + options.peertubeHelpers.logger.debug(`getChannelInfosById: channel ${channelId} not found.`) + return null + } + return { + id: channelId, + name: results[0].channelName ?? '', + displayName: results[0].channelDisplayName ?? '' + } +} + export { getChannelNameById, - getUserNameByChannelId + getUserNameByChannelId, + getChannelInfosById } diff --git a/server/lib/diagnostic/prosody.ts b/server/lib/diagnostic/prosody.ts index 64b0fe70..5e05bbdb 100644 --- a/server/lib/diagnostic/prosody.ts +++ b/server/lib/diagnostic/prosody.ts @@ -34,6 +34,8 @@ export async function diagProsody (test: string, options: RegisterServerOptions) result.messages.push(`Prosody modules path will be '${wantedConfig.paths.modules}'`) + result.messages.push(`Prosody rooms will be grouped by '${wantedConfig.roomType}'.`) + 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 ac7d4819..dd7e9b15 100644 --- a/server/lib/prosody/config.ts +++ b/server/lib/prosody/config.ts @@ -69,6 +69,7 @@ interface ProsodyConfig { host: string port: string baseApiUrl: string + roomType: 'video' | 'channel' } async function getProsodyConfig (options: RegisterServerOptions): Promise { const logger = options.peertubeHelpers.logger @@ -81,6 +82,7 @@ async function getProsodyConfig (options: RegisterServerOptions): Promise { - const peertubeHelpers = options.peertubeHelpers - const prosodyDomain = await getProsodyDomain(options) +async function _getCommonAffiliations (options: RegisterServerOptions, prosodyDomain: string): Promise { // Get all admins and moderators - const [results] = await peertubeHelpers.database.query( + const [results] = await options.peertubeHelpers.database.query( 'SELECT "username" FROM "user"' + ' WHERE "user"."role" IN (0, 1)' ) @@ -27,33 +25,57 @@ async function getVideoAffiliations (options: RegisterServerOptions, video: MVid r[jid] = 'owner' } - // Adding an 'admin' affiliation for video owner + return r +} + +async function _addAffiliationByChannelId ( + options: RegisterServerOptions, + prosodyDomain: string, + r: Affiliations, + channelId: number +): Promise { // NB: if it fails, we want previous results to be returned... try { - if (!video.remote) { - // don't add the video owner if it is a remote video! - const userName = await _getVideoOwnerUsername(options, video) - const userJid = userName + '@' + prosodyDomain + const username = await getUserNameByChannelId(options, channelId) + if (username === null) { + options.peertubeHelpers.logger.error(`Failed to get the username for channelId '${channelId}'.`) + } else { + const userJid = username + '@' + prosodyDomain if (!(userJid in r)) { // don't override if already owner! r[userJid] = 'admin' } } } catch (error) { - peertubeHelpers.logger.error('Failed to get video owner informations:', error) + options.peertubeHelpers.logger.error('Failed to get channel owner informations:', error) + } +} + +async function getVideoAffiliations (options: RegisterServerOptions, video: MVideoThumbnail): Promise { + const prosodyDomain = await getProsodyDomain(options) + const r = await _getCommonAffiliations(options, prosodyDomain) + + // Adding an 'admin' affiliation for video owner + if (!video.remote) { + // don't add the video owner if it is a remote video! + await _addAffiliationByChannelId(options, prosodyDomain, r, video.channelId) } return r } -async function _getVideoOwnerUsername (options: RegisterServerOptions, video: MVideoThumbnail): Promise { - const username = await getUserNameByChannelId(options, video.channelId) - if (username === null) { - throw new Error('Username not found') - } - return username +async function getChannelAffiliations (options: RegisterServerOptions, channelId: number): Promise { + const prosodyDomain = await getProsodyDomain(options) + const r = await _getCommonAffiliations(options, prosodyDomain) + + // Adding an 'admin' affiliation for channel owner + // NB: remote channel can't be found, there are not in the videoChannel table. + await _addAffiliationByChannelId(options, prosodyDomain, r, channelId) + + return r } export { Affiliations, - getVideoAffiliations + getVideoAffiliations, + getChannelAffiliations } diff --git a/server/lib/routers/api.ts b/server/lib/routers/api.ts index 219795c7..9c6b2674 100644 --- a/server/lib/routers/api.ts +++ b/server/lib/routers/api.ts @@ -4,10 +4,11 @@ import { asyncMiddleware } from '../middlewares/async' import { getCheckAPIKeyMiddleware } from '../middlewares/apikey' import { prosodyCheckUserPassword, prosodyRegisterUser, prosodyUserRegistered } from '../prosody/auth' import { getUserNickname } from '../helpers' -import { Affiliations, getVideoAffiliations } from '../prosody/config/affiliations' +import { Affiliations, getVideoAffiliations, getChannelAffiliations } from '../prosody/config/affiliations' import { getProsodyDomain } from '../prosody/config/domain' import type { ChatType } from '../../../shared/lib/types' import { fillVideoCustomFields } from '../custom-fields' +import { getChannelInfosById } from '../database/channel' // See here for description: https://modules.prosody.im/mod_muc_http_defaults.html interface RoomDefaults { @@ -48,61 +49,106 @@ async function initApiRouter (options: RegisterServerOptions): Promise { const jid: string = req.query.jid as string || '' logger.info(`Requesting room information for room '${jid}'.`) - const video = await peertubeHelpers.videos.loadByIdOrUUID(jid) - if (!video) { - logger.warn(`Video ${jid} not found`) - res.sendStatus(403) - return - } - - // Adding the custom fields: - await fillVideoCustomFields(options, video) - - // check settings (chat enabled for this video?) const settings = await options.settingsManager.getSettings([ 'chat-type', - 'chat-only-locals', - 'chat-per-live-video', - 'chat-all-lives', - 'chat-all-non-lives', - 'chat-videos-list' + 'prosody-room-type' ]) if (settings['chat-type'] !== ('builtin-prosody' as ChatType)) { logger.warn('Prosody chat is not active') res.sendStatus(403) return } - if (!videoHasWebchat({ - 'chat-only-locals': !!settings['chat-only-locals'], - '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)) { - logger.warn(`Video ${jid} has not chat activated`) - res.sendStatus(403) - return - } + // Now, we have two different room type: per video or per channel. + if (settings['prosody-room-type'] === 'channel') { + const matches = jid.match(/^channel\.(\d+)$/) + if (!matches || !matches[1]) { + logger.warn(`Invalid channel room jid '${jid}'.`) + res.sendStatus(403) + return + } + const channelId = parseInt(matches[1]) + const channelInfos = await getChannelInfosById(options, channelId) + if (!channelInfos) { + logger.warn(`Channel ${channelId} not found`) + res.sendStatus(403) + return + } - let affiliations: Affiliations - try { - affiliations = await getVideoAffiliations(options, video) - } catch (error) { - logger.error(`Failed to get video affiliations for ${video.uuid}:`, error) - // affiliations: should at least be {}, so that the first user will not be moderator/admin - affiliations = {} - } + let affiliations: Affiliations + try { + affiliations = await getChannelAffiliations(options, channelId) + } catch (error) { + logger.error(`Failed to get channel affiliations for ${channelId}:`, error) + // affiliations: should at least be {}, so that the first user will not be moderator/admin + affiliations = {} + } - const roomDefaults: RoomDefaults = { - config: { - name: video.name, - description: '', - language: video.language, - subject: video.name - }, - affiliations: affiliations + const roomDefaults: RoomDefaults = { + config: { + name: channelInfos.displayName, + description: '', + subject: channelInfos.displayName + }, + affiliations: affiliations + } + res.json(roomDefaults) + } else { + const video = await peertubeHelpers.videos.loadByIdOrUUID(jid) + if (!video) { + logger.warn(`Video ${jid} not found`) + res.sendStatus(403) + return + } + + // Adding the custom fields: + await fillVideoCustomFields(options, video) + + // check settings (chat enabled for this video?) + const settings = await options.settingsManager.getSettings([ + 'chat-type', + 'chat-only-locals', + 'chat-per-live-video', + 'chat-all-lives', + 'chat-all-non-lives', + 'chat-videos-list' + ]) + if (settings['chat-type'] !== ('builtin-prosody' as ChatType)) { + logger.warn('Prosody chat is not active') + res.sendStatus(403) + return + } + if (!videoHasWebchat({ + 'chat-only-locals': !!settings['chat-only-locals'], + '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)) { + logger.warn(`Video ${jid} has not chat activated`) + res.sendStatus(403) + return + } + + let affiliations: Affiliations + try { + affiliations = await getVideoAffiliations(options, video) + } catch (error) { + logger.error(`Failed to get video affiliations for ${video.uuid}:`, error) + // affiliations: should at least be {}, so that the first user will not be moderator/admin + affiliations = {} + } + + const roomDefaults: RoomDefaults = { + config: { + name: video.name, + description: '', + language: video.language, + subject: video.name + }, + affiliations: affiliations + } + res.json(roomDefaults) } - res.json(roomDefaults) } ])) diff --git a/server/lib/routers/webchat.ts b/server/lib/routers/webchat.ts index 179226d4..f3544105 100644 --- a/server/lib/routers/webchat.ts +++ b/server/lib/routers/webchat.ts @@ -37,11 +37,12 @@ async function initWebchatRouter (options: RegisterServerOptions): PromiseList rooms', private: true }) + + registerSetting({ + name: 'prosody-room-type', + label: 'Room type', + type: 'select', + descriptionHTML: 'You can choose here to have separate rooms for each video, or to group them by channel.', + private: false, + default: 'video', + options: [ + { value: 'video', label: 'Each video has its own webchat room' }, + { value: 'channel', label: 'Webchat rooms are grouped by channel' } + ] + }) + registerSetting({ name: 'prosody-port', label: 'Prosody port',