diff --git a/CHANGELOG.md b/CHANGELOG.md index 039927b3..45ba6ac7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ TODO: tag custom ConverseJS, and update build-conversejs.sh. ### 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/). +* #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 diff --git a/prosody-modules/mod_http_peertubelivechat_manage_rooms/mod_http_peertubelivechat_manage_rooms.lua b/prosody-modules/mod_http_peertubelivechat_manage_rooms/mod_http_peertubelivechat_manage_rooms.lua index 7cc7ada9..6302c418 100644 --- a/prosody-modules/mod_http_peertubelivechat_manage_rooms/mod_http_peertubelivechat_manage_rooms.lua +++ b/prosody-modules/mod_http_peertubelivechat_manage_rooms/mod_http_peertubelivechat_manage_rooms.lua @@ -1,5 +1,6 @@ local json = require "util.json"; local jid_split = require"util.jid".split; +local jid_prep = require"util.jid".prep; local array = require "util.array"; local st = require "util.stanza"; @@ -89,6 +90,30 @@ local function update_room(event) must104 = true; 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 -- we must broadcast a status 104 message, so that clients can update room info diff --git a/server/lib/prosody/api/manage-rooms.ts b/server/lib/prosody/api/manage-rooms.ts index 6a4840dc..8d31b1b7 100644 --- a/server/lib/prosody/api/manage-rooms.ts +++ b/server/lib/prosody/api/manage-rooms.ts @@ -1,4 +1,5 @@ import type { RegisterServerOptions } from '@peertube/peertube-types' +import type { Affiliations } from '../config/affiliations' import { getCurrentProsody } from './host' import { getAPIKey } from '../../apikey' import { getProsodyDomain } from '../config/domain' @@ -59,6 +60,8 @@ async function updateProsodyRoom ( data: { name?: string slow_mode_duration?: number + addAffiliations?: Affiliations + removeAffiliationsFor?: string[] } ): Promise { const logger = options.peertubeHelpers.logger @@ -79,12 +82,18 @@ async function updateProsodyRoom ( const apiData = { jid } as any - if ('name' in data) { + if (('name' in data) && data.name !== undefined) { 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 } + if (('addAffiliations' in data) && data.addAffiliations !== undefined) { + apiData.addAffiliations = data.addAffiliations + } + if (('removeAffiliationsFor' in data) && data.removeAffiliationsFor !== undefined) { + apiData.removeAffiliationsFor = data.removeAffiliationsFor + } try { logger.debug('Calling update room API on url: ' + apiUrl + ', with data: ' + JSON.stringify(apiData)) const result = await got(apiUrl, { diff --git a/server/lib/prosody/config/affiliations.ts b/server/lib/prosody/config/affiliations.ts index 5638d564..4def14ad 100644 --- a/server/lib/prosody/config/affiliations.ts +++ b/server/lib/prosody/config/affiliations.ts @@ -5,29 +5,10 @@ import { BotConfiguration } from '../../configuration/bot' interface Affiliations { [jid: string]: 'outcast' | 'none' | 'member' | 'admin' | 'owner' } -async function _getCommonAffiliations (options: RegisterServerOptions, prosodyDomain: string): Promise { - // 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.') - } +async function _getCommonAffiliations (options: RegisterServerOptions, _prosodyDomain: string): Promise { 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([ 'disable-channel-configuration' ]) @@ -52,9 +33,7 @@ async function _addAffiliationByChannelId ( 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' - } + r[userJid] = 'owner' } } catch (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 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. await _addAffiliationByChannelId(options, prosodyDomain, r, channelId) diff --git a/server/lib/prosody/migration/migrateV10.ts b/server/lib/prosody/migration/migrateV10.ts new file mode 100644 index 00000000..2738b024 --- /dev/null +++ b/server/lib/prosody/migration/migrateV10.ts @@ -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 { + 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 { + // 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 +} diff --git a/server/main.ts b/server/main.ts index d18bddbb..d9ad602b 100644 --- a/server/main.ts +++ b/server/main.ts @@ -13,6 +13,7 @@ import { RoomChannel } from './lib/room-channel' import { BotConfiguration } from './lib/configuration/bot' import { BotsCtl } from './lib/bots/ctl' import { ExternalAuthOIDC } from './lib/external-auth/oidc' +import { migrateMUCAffiliations } from './lib/prosody/migration/migrateV10' import decache from 'decache' // FIXME: Peertube unregister don't have any parameter. @@ -72,6 +73,16 @@ async function register (options: RegisterServerOptions): Promise { }, () => {} ) + + // 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) { options.peertubeHelpers.logger.error('Error when launching Prosody: ' + (error as string)) }