Builtin Prosody: adding the prosody-room-type settings to allow rooms to be per channel or per video. WIP.

This commit is contained in:
John Livingston
2021-08-05 15:41:49 +02:00
parent 5237c20332
commit 5c0b274f39
9 changed files with 203 additions and 70 deletions

View File

@ -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<ChannelInfos | null> {
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
}

View File

@ -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, {

View File

@ -69,6 +69,7 @@ interface ProsodyConfig {
host: string
port: string
baseApiUrl: string
roomType: 'video' | 'channel'
}
async function getProsodyConfig (options: RegisterServerOptions): Promise<ProsodyConfig> {
const logger = options.peertubeHelpers.logger
@ -81,6 +82,7 @@ async function getProsodyConfig (options: RegisterServerOptions): Promise<Prosod
const enableC2s = (await options.settingsManager.getSetting('prosody-c2s') as boolean) || false
const prosodyDomain = await getProsodyDomain(options)
const paths = await getProsodyFilePaths(options)
const roomType = (await options.settingsManager.getSetting('prosody-room-type')) === 'channel' ? 'channel' : 'video'
const apikey = await getAPIKey(options)
let baseApiUrl = await options.settingsManager.getSetting('prosody-peertube-uri') as string
@ -139,7 +141,8 @@ async function getProsodyConfig (options: RegisterServerOptions): Promise<Prosod
paths,
port,
baseApiUrl,
host: prosodyDomain
host: prosodyDomain,
roomType
}
}

View File

@ -3,11 +3,9 @@ import { getUserNameByChannelId } from '../../database/channel'
interface Affiliations { [jid: string]: 'outcast' | 'none' | 'member' | 'admin' | 'owner' }
async function getVideoAffiliations (options: RegisterServerOptions, video: MVideoThumbnail): Promise<Affiliations> {
const peertubeHelpers = options.peertubeHelpers
const prosodyDomain = await getProsodyDomain(options)
async function _getCommonAffiliations (options: RegisterServerOptions, prosodyDomain: string): Promise<Affiliations> {
// 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<void> {
// 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<Affiliations> {
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<string> {
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<Affiliations> {
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
}

View File

@ -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<Router> {
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)
}
]))

View File

@ -37,11 +37,12 @@ async function initWebchatRouter (options: RegisterServerOptions): Promise<Route
const settings = await settingsManager.getSettings([
'chat-type', 'chat-room', 'chat-server',
'chat-bosh-uri', 'chat-ws-uri'
'chat-bosh-uri', 'chat-ws-uri',
'prosody-room-type'
])
const chatType: ChatType = (settings['chat-type'] ?? 'disabled') as ChatType
let server: string
let jid: string
let room: string
let boshUri: string
let wsUri: string
@ -49,8 +50,12 @@ async function initWebchatRouter (options: RegisterServerOptions): Promise<Route
let advancedControls: boolean = false
if (chatType === 'builtin-prosody') {
const prosodyDomain = await getProsodyDomain(options)
server = 'anon.' + prosodyDomain
room = '{{VIDEO_UUID}}@room.' + prosodyDomain
jid = 'anon.' + prosodyDomain
if (settings['prosody-room-type'] === 'channel') {
room = 'channel.{{CHANNEL_ID}}@room.' + prosodyDomain
} else {
room = '{{VIDEO_UUID}}@room.' + prosodyDomain
}
boshUri = getBaseRouterRoute(options) + 'webchat/http-bind'
wsUri = ''
authenticationUrl = options.peertubeHelpers.config.getWebserverUrl() +
@ -67,7 +72,7 @@ async function initWebchatRouter (options: RegisterServerOptions): Promise<Route
if (!settings['chat-bosh-uri'] && !settings['chat-ws-uri']) {
throw new Error('Missing BOSH or Websocket uri.')
}
server = settings['chat-server'] as string
jid = settings['chat-server'] as string
room = settings['chat-room'] as string
boshUri = settings['chat-bosh-uri'] as string
wsUri = settings['chat-ws-uri'] as string
@ -84,7 +89,7 @@ async function initWebchatRouter (options: RegisterServerOptions): Promise<Route
let page = '' + (converseJSIndex as string)
const baseStaticUrl = getBaseStaticRoute(options)
page = page.replace(/{{BASE_STATIC_URL}}/g, baseStaticUrl)
page = page.replace(/{{JID}}/g, server)
page = page.replace(/{{JID}}/g, jid)
// Computing the room name...
room = room.replace(/{{VIDEO_UUID}}/g, video.uuid)
room = room.replace(/{{CHANNEL_ID}}/g, `${video.channelId}`)

View File

@ -98,6 +98,20 @@ Please read the
descriptionHTML: '<a class="peertube-plugin-livechat-prosody-list-rooms">List rooms</a>',
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',