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) { switch (name) {
case 'chat-type-help-disabled': case 'chat-type-help-disabled':
return options.formValues['chat-type'] !== ('disabled' as ChatType) return options.formValues['chat-type'] !== ('disabled' as ChatType)
case 'prosody-room-type':
case 'prosody-port': case 'prosody-port':
case 'prosody-peertube-uri': case 'prosody-peertube-uri':
case 'chat-type-help-builtin-prosody': 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. 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 #### 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. 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 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 { export {
getChannelNameById, 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 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. 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`) result.messages.push(`The prosody configuration file (${filePath}) exists`)
const actualContent = await fs.promises.readFile(filePath, { const actualContent = await fs.promises.readFile(filePath, {

View File

@ -69,6 +69,7 @@ interface ProsodyConfig {
host: string host: string
port: string port: string
baseApiUrl: string baseApiUrl: string
roomType: 'video' | 'channel'
} }
async function getProsodyConfig (options: RegisterServerOptions): Promise<ProsodyConfig> { async function getProsodyConfig (options: RegisterServerOptions): Promise<ProsodyConfig> {
const logger = options.peertubeHelpers.logger 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 enableC2s = (await options.settingsManager.getSetting('prosody-c2s') as boolean) || false
const prosodyDomain = await getProsodyDomain(options) const prosodyDomain = await getProsodyDomain(options)
const paths = await getProsodyFilePaths(options) const paths = await getProsodyFilePaths(options)
const roomType = (await options.settingsManager.getSetting('prosody-room-type')) === 'channel' ? 'channel' : 'video'
const apikey = await getAPIKey(options) const apikey = await getAPIKey(options)
let baseApiUrl = await options.settingsManager.getSetting('prosody-peertube-uri') as string let baseApiUrl = await options.settingsManager.getSetting('prosody-peertube-uri') as string
@ -139,7 +141,8 @@ async function getProsodyConfig (options: RegisterServerOptions): Promise<Prosod
paths, paths,
port, port,
baseApiUrl, 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' } interface Affiliations { [jid: string]: 'outcast' | 'none' | 'member' | 'admin' | 'owner' }
async function getVideoAffiliations (options: RegisterServerOptions, video: MVideoThumbnail): Promise<Affiliations> { async function _getCommonAffiliations (options: RegisterServerOptions, prosodyDomain: string): Promise<Affiliations> {
const peertubeHelpers = options.peertubeHelpers
const prosodyDomain = await getProsodyDomain(options)
// Get all admins and moderators // Get all admins and moderators
const [results] = await peertubeHelpers.database.query( const [results] = await options.peertubeHelpers.database.query(
'SELECT "username" FROM "user"' + 'SELECT "username" FROM "user"' +
' WHERE "user"."role" IN (0, 1)' ' WHERE "user"."role" IN (0, 1)'
) )
@ -27,33 +25,57 @@ async function getVideoAffiliations (options: RegisterServerOptions, video: MVid
r[jid] = 'owner' 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... // NB: if it fails, we want previous results to be returned...
try { try {
if (!video.remote) { const username = await getUserNameByChannelId(options, channelId)
// don't add the video owner if it is a remote video! if (username === null) {
const userName = await _getVideoOwnerUsername(options, video) options.peertubeHelpers.logger.error(`Failed to get the username for channelId '${channelId}'.`)
const userJid = userName + '@' + prosodyDomain } else {
const userJid = username + '@' + prosodyDomain
if (!(userJid in r)) { // don't override if already owner! if (!(userJid in r)) { // don't override if already owner!
r[userJid] = 'admin' r[userJid] = 'admin'
} }
} }
} catch (error) { } 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 return r
} }
async function _getVideoOwnerUsername (options: RegisterServerOptions, video: MVideoThumbnail): Promise<string> { async function getChannelAffiliations (options: RegisterServerOptions, channelId: number): Promise<Affiliations> {
const username = await getUserNameByChannelId(options, video.channelId) const prosodyDomain = await getProsodyDomain(options)
if (username === null) { const r = await _getCommonAffiliations(options, prosodyDomain)
throw new Error('Username not found')
} // Adding an 'admin' affiliation for channel owner
return username // NB: remote channel can't be found, there are not in the videoChannel table.
await _addAffiliationByChannelId(options, prosodyDomain, r, channelId)
return r
} }
export { export {
Affiliations, Affiliations,
getVideoAffiliations getVideoAffiliations,
getChannelAffiliations
} }

View File

@ -4,10 +4,11 @@ import { asyncMiddleware } from '../middlewares/async'
import { getCheckAPIKeyMiddleware } from '../middlewares/apikey' import { getCheckAPIKeyMiddleware } from '../middlewares/apikey'
import { prosodyCheckUserPassword, prosodyRegisterUser, prosodyUserRegistered } from '../prosody/auth' import { prosodyCheckUserPassword, prosodyRegisterUser, prosodyUserRegistered } from '../prosody/auth'
import { getUserNickname } from '../helpers' 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 { getProsodyDomain } from '../prosody/config/domain'
import type { ChatType } from '../../../shared/lib/types' import type { ChatType } from '../../../shared/lib/types'
import { fillVideoCustomFields } from '../custom-fields' import { fillVideoCustomFields } from '../custom-fields'
import { getChannelInfosById } from '../database/channel'
// See here for description: https://modules.prosody.im/mod_muc_http_defaults.html // See here for description: https://modules.prosody.im/mod_muc_http_defaults.html
interface RoomDefaults { interface RoomDefaults {
@ -48,6 +49,50 @@ async function initApiRouter (options: RegisterServerOptions): Promise<Router> {
const jid: string = req.query.jid as string || '' const jid: string = req.query.jid as string || ''
logger.info(`Requesting room information for room '${jid}'.`) logger.info(`Requesting room information for room '${jid}'.`)
const settings = await options.settingsManager.getSettings([
'chat-type',
'prosody-room-type'
])
if (settings['chat-type'] !== ('builtin-prosody' as ChatType)) {
logger.warn('Prosody chat is not active')
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 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: channelInfos.displayName,
description: '',
subject: channelInfos.displayName
},
affiliations: affiliations
}
res.json(roomDefaults)
} else {
const video = await peertubeHelpers.videos.loadByIdOrUUID(jid) const video = await peertubeHelpers.videos.loadByIdOrUUID(jid)
if (!video) { if (!video) {
logger.warn(`Video ${jid} not found`) logger.warn(`Video ${jid} not found`)
@ -104,6 +149,7 @@ async function initApiRouter (options: RegisterServerOptions): Promise<Router> {
} }
res.json(roomDefaults) res.json(roomDefaults)
} }
}
])) ]))
router.get('/auth', asyncMiddleware( router.get('/auth', asyncMiddleware(

View File

@ -37,11 +37,12 @@ async function initWebchatRouter (options: RegisterServerOptions): Promise<Route
const settings = await settingsManager.getSettings([ const settings = await settingsManager.getSettings([
'chat-type', 'chat-room', 'chat-server', '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 const chatType: ChatType = (settings['chat-type'] ?? 'disabled') as ChatType
let server: string let jid: string
let room: string let room: string
let boshUri: string let boshUri: string
let wsUri: string let wsUri: string
@ -49,8 +50,12 @@ async function initWebchatRouter (options: RegisterServerOptions): Promise<Route
let advancedControls: boolean = false let advancedControls: boolean = false
if (chatType === 'builtin-prosody') { if (chatType === 'builtin-prosody') {
const prosodyDomain = await getProsodyDomain(options) const prosodyDomain = await getProsodyDomain(options)
server = 'anon.' + prosodyDomain jid = 'anon.' + prosodyDomain
if (settings['prosody-room-type'] === 'channel') {
room = 'channel.{{CHANNEL_ID}}@room.' + prosodyDomain
} else {
room = '{{VIDEO_UUID}}@room.' + prosodyDomain room = '{{VIDEO_UUID}}@room.' + prosodyDomain
}
boshUri = getBaseRouterRoute(options) + 'webchat/http-bind' boshUri = getBaseRouterRoute(options) + 'webchat/http-bind'
wsUri = '' wsUri = ''
authenticationUrl = options.peertubeHelpers.config.getWebserverUrl() + 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']) { if (!settings['chat-bosh-uri'] && !settings['chat-ws-uri']) {
throw new Error('Missing BOSH or Websocket 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 room = settings['chat-room'] as string
boshUri = settings['chat-bosh-uri'] as string boshUri = settings['chat-bosh-uri'] as string
wsUri = settings['chat-ws-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) let page = '' + (converseJSIndex as string)
const baseStaticUrl = getBaseStaticRoute(options) const baseStaticUrl = getBaseStaticRoute(options)
page = page.replace(/{{BASE_STATIC_URL}}/g, baseStaticUrl) page = page.replace(/{{BASE_STATIC_URL}}/g, baseStaticUrl)
page = page.replace(/{{JID}}/g, server) page = page.replace(/{{JID}}/g, jid)
// Computing the room name... // Computing the room name...
room = room.replace(/{{VIDEO_UUID}}/g, video.uuid) room = room.replace(/{{VIDEO_UUID}}/g, video.uuid)
room = room.replace(/{{CHANNEL_ID}}/g, `${video.channelId}`) 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>', descriptionHTML: '<a class="peertube-plugin-livechat-prosody-list-rooms">List rooms</a>',
private: true 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({ registerSetting({
name: 'prosody-port', name: 'prosody-port',
label: 'Prosody port', label: 'Prosody port',