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
No known key found for this signature in database
GPG Key ID: B17B5640CE66CDBC
9 changed files with 203 additions and 70 deletions

View File

@ -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':

View File

@ -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.

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',