Changing defaults MUC affiliation (#385):

* video/channel owner is MUC owner
* the bot is MUC owner
* the bot is admin on the MUC component
* Peertube moderators/admins have no more special access (by default)
* migration script to update all existing rooms
This commit is contained in:
John Livingston 2024-05-17 11:47:37 +02:00
parent ebcbe8227b
commit 5745e8c8a3
No known key found for this signature in database
GPG Key ID: B17B5640CE66CDBC
6 changed files with 166 additions and 27 deletions

View File

@ -7,6 +7,8 @@ TODO: tag custom ConverseJS, and update build-conversejs.sh.
### New features ### New features
* #177: streamer's task/to-do lists: streamers, and their room's moderators, can handle task lists directly. This can be used to handle viewers questions, moderation actions, ... More info in the [tasks documentation](https://livingston.frama.io/peertube-plugin-livechat/fr/documentation/user/streamers/tasks/). * #177: streamer's task/to-do lists: streamers, and their room's moderators, can handle task lists directly. This can be used to handle viewers questions, moderation actions, ... More info in the [tasks documentation](https://livingston.frama.io/peertube-plugin-livechat/fr/documentation/user/streamers/tasks/).
* #385: new way of managing chat access rights. Now streamers are owner of their chat rooms. Peertube admins/moderators are not by default, so that their identities are not leaking. But they have a button to promote as chat room owner, if they need to take action. Please note that there is a migration script that will remove all Peertube admins/moderators affiliations (unless they are video/channel's owner). They can get this access back using the button.
* #385: the slow mode duration on the channel option page is now a default value for new rooms. Streamers can change the value room per room in the room's configuration.
### Minor changes and fixes ### Minor changes and fixes

View File

@ -1,5 +1,6 @@
local json = require "util.json"; local json = require "util.json";
local jid_split = require"util.jid".split; local jid_split = require"util.jid".split;
local jid_prep = require"util.jid".prep;
local array = require "util.array"; local array = require "util.array";
local st = require "util.stanza"; local st = require "util.stanza";
@ -89,6 +90,30 @@ local function update_room(event)
must104 = true; must104 = true;
end end
end end
if type(config.removeAffiliationsFor) == "table" then
-- array of jids
for _, jid in ipairs(config.removeAffiliationsFor) do
room:set_affiliation(true, jid, "none");
end
end
if type(config.addAffiliations) == "table" then
-- map of jid:affiliation
for user_jid, aff in pairs(config.addAffiliations) do
if type(user_jid) == "string" and type(aff) == "string" then
local prepped_jid = jid_prep(user_jid);
if prepped_jid then
local ok, err = room:set_affiliation(true, prepped_jid, aff);
if not ok then
module:log("error", "Could not set affiliation in %s: %s", room.jid, err);
-- FIXME: fail?
end
else
module:log("error", "Invalid JID: %q", aff.jid);
-- FIXME: fail?
end
end
end
end
if must104 then if must104 then
-- we must broadcast a status 104 message, so that clients can update room info -- we must broadcast a status 104 message, so that clients can update room info

View File

@ -1,4 +1,5 @@
import type { RegisterServerOptions } from '@peertube/peertube-types' import type { RegisterServerOptions } from '@peertube/peertube-types'
import type { Affiliations } from '../config/affiliations'
import { getCurrentProsody } from './host' import { getCurrentProsody } from './host'
import { getAPIKey } from '../../apikey' import { getAPIKey } from '../../apikey'
import { getProsodyDomain } from '../config/domain' import { getProsodyDomain } from '../config/domain'
@ -59,6 +60,8 @@ async function updateProsodyRoom (
data: { data: {
name?: string name?: string
slow_mode_duration?: number slow_mode_duration?: number
addAffiliations?: Affiliations
removeAffiliationsFor?: string[]
} }
): Promise<boolean> { ): Promise<boolean> {
const logger = options.peertubeHelpers.logger const logger = options.peertubeHelpers.logger
@ -79,12 +82,18 @@ async function updateProsodyRoom (
const apiData = { const apiData = {
jid jid
} as any } as any
if ('name' in data) { if (('name' in data) && data.name !== undefined) {
apiData.name = data.name apiData.name = data.name
} }
if ('slow_mode_duration' in data) { if (('slow_mode_duration' in data) && data.slow_mode_duration !== undefined) {
apiData.slow_mode_duration = data.slow_mode_duration apiData.slow_mode_duration = data.slow_mode_duration
} }
if (('addAffiliations' in data) && data.addAffiliations !== undefined) {
apiData.addAffiliations = data.addAffiliations
}
if (('removeAffiliationsFor' in data) && data.removeAffiliationsFor !== undefined) {
apiData.removeAffiliationsFor = data.removeAffiliationsFor
}
try { try {
logger.debug('Calling update room API on url: ' + apiUrl + ', with data: ' + JSON.stringify(apiData)) logger.debug('Calling update room API on url: ' + apiUrl + ', with data: ' + JSON.stringify(apiData))
const result = await got(apiUrl, { const result = await got(apiUrl, {

View File

@ -5,29 +5,10 @@ import { BotConfiguration } from '../../configuration/bot'
interface Affiliations { [jid: string]: 'outcast' | 'none' | 'member' | 'admin' | 'owner' } interface Affiliations { [jid: string]: 'outcast' | 'none' | 'member' | 'admin' | 'owner' }
async function _getCommonAffiliations (options: RegisterServerOptions, prosodyDomain: string): Promise<Affiliations> { async function _getCommonAffiliations (options: RegisterServerOptions, _prosodyDomain: string): Promise<Affiliations> {
// Get all admins and moderators
const [results] = await options.peertubeHelpers.database.query(
'SELECT "username" FROM "user"' +
' WHERE "user"."role" IN (0, 1)'
)
if (!Array.isArray(results)) {
throw new Error('getVideoAffiliations: query result is not an array.')
}
const r: Affiliations = {} const r: Affiliations = {}
for (let i = 0; i < results.length; i++) {
const result = results[i]
if (typeof result !== 'object') {
throw new Error('getVideoAffiliations: query result is not an object')
}
if (!('username' in result)) {
throw new Error('getVideoAffiliations: no username field in result')
}
const jid = (result.username as string) + '@' + prosodyDomain
r[jid] = 'owner'
}
// Adding the moderation bot JID as room owner. // Adding the moderation bot JID as room owner if feature is enabled.
const settings = await options.settingsManager.getSettings([ const settings = await options.settingsManager.getSettings([
'disable-channel-configuration' 'disable-channel-configuration'
]) ])
@ -52,9 +33,7 @@ async function _addAffiliationByChannelId (
options.peertubeHelpers.logger.error(`Failed to get the username for channelId '${channelId}'.`) options.peertubeHelpers.logger.error(`Failed to get the username for channelId '${channelId}'.`)
} else { } else {
const userJid = username + '@' + prosodyDomain const userJid = username + '@' + prosodyDomain
if (!(userJid in r)) { // don't override if already owner! r[userJid] = 'owner'
r[userJid] = 'admin'
}
} }
} catch (error) { } catch (error) {
options.peertubeHelpers.logger.error('Failed to get channel owner informations:', error) options.peertubeHelpers.logger.error('Failed to get channel owner informations:', error)
@ -78,7 +57,7 @@ async function getChannelAffiliations (options: RegisterServerOptions, channelId
const prosodyDomain = await getProsodyDomain(options) const prosodyDomain = await getProsodyDomain(options)
const r = await _getCommonAffiliations(options, prosodyDomain) const r = await _getCommonAffiliations(options, prosodyDomain)
// Adding an 'admin' affiliation for channel owner // Adding an affiliation for channel owner
// NB: remote channel can't be found, there are not in the videoChannel table. // NB: remote channel can't be found, there are not in the videoChannel table.
await _addAffiliationByChannelId(options, prosodyDomain, r, channelId) await _addAffiliationByChannelId(options, prosodyDomain, r, channelId)

View File

@ -0,0 +1,113 @@
import type { RegisterServerOptions } from '@peertube/peertube-types'
import { listProsodyRooms, updateProsodyRoom } from '../api/manage-rooms'
import { Affiliations, getVideoAffiliations, getChannelAffiliations } from '../config/affiliations'
import { getProsodyDomain } from '../config/domain'
import * as path from 'path'
import * as fs from 'fs'
/**
* Livechat v10.0.0: we change the way MUC affiliations are handled.
* So we must remove all affiliations to peertube admin/owner (unless there are video/channel owners).
* For more info, see https://github.com/JohnXLivingston/peertube-plugin-livechat/issues/385
*
* This script will only be launched one time.
*/
async function migrateMUCAffiliations (options: RegisterServerOptions): Promise<void> {
const logger = options.peertubeHelpers.logger
// First, detect if we already run this script.
const doneFilePath = path.resolve(options.peertubeHelpers.plugin.getDataDirectoryPath(), 'fix-v10-affiliations')
if (fs.existsSync(doneFilePath)) {
logger.debug('[migratev10MUCAffiliations] MUC affiliations for v10 already migrated.')
return
}
logger.info('[migratev10MUCAffiliations] Migrating MUC affiliations for livechat v10...')
const prosodyDomain = await getProsodyDomain(options)
const rooms = await listProsodyRooms(options)
logger.debug('[migratev10MUCAffiliations] Found ' + rooms.length.toString() + ' rooms.')
logger.debug('[migratev10MUCAffiliations] loading peertube admins and moderators...')
const peertubeAff = await _getPeertubeAdminsAndModerators(options, prosodyDomain)
for (const room of rooms) {
try {
let affiliations: Affiliations
logger.info('[migratev10MUCAffiliations] Must migrate affiliations for room ' + room.localpart)
const matches = room.localpart.match(/^channel\.(\d+)$/)
if (matches?.[1]) {
// room associated to a channel
const channelId: number = parseInt(matches[1])
if (isNaN(channelId)) { throw new Error('Invalid channelId ' + room.localpart) }
affiliations = await getChannelAffiliations(options, channelId)
} else {
// room associated to a video
const video = await options.peertubeHelpers.videos.loadByIdOrUUID(room.localpart)
if (!video || video.remote) {
logger.info('[migratev10MUCAffiliations] Video ' + room.localpart + ' not found or remote, skipping')
continue
}
affiliations = await getVideoAffiliations(options, video)
}
const affiliationsToRemove: string[] = []
for (const jid in peertubeAff) {
if (jid in affiliations) {
continue
}
affiliationsToRemove.push(jid)
}
logger.debug(
'[migratev10MUCAffiliations] Room ' + room.localpart + ', affiliations to set: ' + JSON.stringify(affiliations)
)
logger.debug(
'[migratev10MUCAffiliations] Room ' +
room.localpart + ', affilations to remove: ' + JSON.stringify(affiliationsToRemove)
)
await updateProsodyRoom(options, room.jid, {
addAffiliations: affiliations,
removeAffiliationsFor: affiliationsToRemove
})
} catch (err) {
logger.error(
'[migratev10MUCAffiliations] Failed to handle room ' + room.localpart + ', skipping. Error: ' + (err as string)
)
continue
}
}
await fs.promises.writeFile(doneFilePath, '')
}
async function _getPeertubeAdminsAndModerators (
options: RegisterServerOptions,
prosodyDomain: string
): Promise<Affiliations> {
// Get all admins and moderators
const [results] = await options.peertubeHelpers.database.query(
'SELECT "username" FROM "user"' +
' WHERE "user"."role" IN (0, 1)'
)
if (!Array.isArray(results)) {
throw new Error('_getPeertubeAdminsAndModerators: query result is not an array.')
}
const r: Affiliations = {}
for (let i = 0; i < results.length; i++) {
const result = results[i]
if (typeof result !== 'object') {
throw new Error('_getPeertubeAdminsAndModerators: query result is not an object')
}
if (!('username' in result)) {
throw new Error('_getPeertubeAdminsAndModerators: no username field in result')
}
const jid = (result.username as string) + '@' + prosodyDomain
r[jid] = 'member' // member, but in fact the migration will just remove the affilation.
}
return r
}
export {
migrateMUCAffiliations
}

View File

@ -13,6 +13,7 @@ import { RoomChannel } from './lib/room-channel'
import { BotConfiguration } from './lib/configuration/bot' import { BotConfiguration } from './lib/configuration/bot'
import { BotsCtl } from './lib/bots/ctl' import { BotsCtl } from './lib/bots/ctl'
import { ExternalAuthOIDC } from './lib/external-auth/oidc' import { ExternalAuthOIDC } from './lib/external-auth/oidc'
import { migrateMUCAffiliations } from './lib/prosody/migration/migrateV10'
import decache from 'decache' import decache from 'decache'
// FIXME: Peertube unregister don't have any parameter. // FIXME: Peertube unregister don't have any parameter.
@ -72,6 +73,16 @@ async function register (options: RegisterServerOptions): Promise<any> {
}, },
() => {} () => {}
) )
// livechat v10.0.0: we must migrate MUC affiliations (but we don't have to wait)
// we do this after the preBotPromise, just to avoid doing both at the same time.
preBotPromise.then(() => {
migrateMUCAffiliations(options).then(
() => {},
(err) => {
logger.error(err)
})
}, () => {})
} catch (error) { } catch (error) {
options.peertubeHelpers.logger.error('Error when launching Prosody: ' + (error as string)) options.peertubeHelpers.logger.error('Error when launching Prosody: ' + (error as string))
} }