From 02728bb38de6714f5071664ebce9af47ad181ea7 Mon Sep 17 00:00:00 2001 From: John Livingston Date: Tue, 8 Aug 2023 18:26:40 +0200 Subject: [PATCH] Moderation configuration screen: WIP. --- client/@types/global.d.ts | 2 + client/common/moderation/register.ts | 14 +++++ client/common/moderation/templates/channel.ts | 58 +++++++++++++++++ client/common/moderation/templates/home.ts | 10 +-- languages/en.yml | 2 + server/lib/database/channel.ts | 7 ++- server/lib/helpers.ts | 15 +++++ server/lib/routers/api.ts | 3 + server/lib/routers/api/moderation.ts | 62 +++++++++++++++++++ shared/lib/types.ts | 11 +++- 10 files changed, 177 insertions(+), 7 deletions(-) create mode 100644 client/common/moderation/templates/channel.ts create mode 100644 server/lib/routers/api/moderation.ts diff --git a/client/@types/global.d.ts b/client/@types/global.d.ts index c42f3f32..255dd21c 100644 --- a/client/@types/global.d.ts +++ b/client/@types/global.d.ts @@ -35,3 +35,5 @@ declare const LOC_MENU_MODERATION_LABEL: string declare const LOC_LIVECHAT_MODERATION_TITLE: string declare const LOC_LIVECHAT_MODERATION_DESC: string declare const LOC_LIVECHAT_MODERATION_PLEASE_SELECT: string +declare const LOC_LIVECHAT_MODERATION_CHANNEL_TITLE: string +declare const LOC_LIVECHAT_MODERATION_CHANNEL_DESC: string diff --git a/client/common/moderation/register.ts b/client/common/moderation/register.ts index 15e17812..a72bd365 100644 --- a/client/common/moderation/register.ts +++ b/client/common/moderation/register.ts @@ -1,6 +1,11 @@ import type { RegisterClientOptions } from '@peertube/peertube-types/client' import { renderModerationHome } from './templates/home' +import { renderModerationChannel } from './templates/channel' +/** + * Registers stuff related to the moderation settings. + * @param clientOptions Peertube client options + */ async function registerModeration (clientOptions: RegisterClientOptions): Promise { const { peertubeHelpers, registerClientRoute, registerHook } = clientOptions @@ -11,6 +16,15 @@ async function registerModeration (clientOptions: RegisterClientOptions): Promis } }) + registerClientRoute({ + route: 'livechat/moderation/channel', + onMount: async ({ rootEl }) => { + const urlParams = new URLSearchParams(window.location.search) + const channelId = urlParams.get('channelId') ?? '' + rootEl.innerHTML = await renderModerationChannel(clientOptions, channelId) + } + }) + registerHook({ target: 'filter:left-menu.links.create.result', handler: async (links: any) => { diff --git a/client/common/moderation/templates/channel.ts b/client/common/moderation/templates/channel.ts new file mode 100644 index 00000000..a2fa4dbc --- /dev/null +++ b/client/common/moderation/templates/channel.ts @@ -0,0 +1,58 @@ +import type { RegisterClientOptions } from '@peertube/peertube-types/client' +import type { ChannelModerationOptions } from 'shared/lib/types' +import { getBaseRoute } from '../../../videowatch/uri' +// Must use require for mustache, import seems buggy. +const Mustache = require('mustache') + +/** + * Renders the moderation settings page for a given channel. + * @param registerClientOptions Peertube client options + * @param channelId The channel id + * @returns The page content + */ +async function renderModerationChannel ( + registerClientOptions: RegisterClientOptions, + channelId: string +): Promise { + const { peertubeHelpers } = registerClientOptions + + try { + if (!channelId || !/^\d+$/.test(channelId)) { + throw new Error('Missing or invalid channel id.') + } + + const channelModerationOptions: ChannelModerationOptions = await (await fetch( + getBaseRoute(registerClientOptions) + '/api/moderation/channel/' + encodeURIComponent(channelId), + { + method: 'GET', + headers: peertubeHelpers.getAuthHeader() + } + )).json() + + // Basic testing that channelModerationOptions has the correct format + if ((typeof channelModerationOptions !== 'object') || !channelModerationOptions.channel) { + throw new Error('Can\'t get channel moderation options.') + } + + const view = { + title: + await peertubeHelpers.translate(LOC_LIVECHAT_MODERATION_CHANNEL_TITLE) + + ' ' + channelModerationOptions.channel.displayName, + description: await peertubeHelpers.translate(LOC_LIVECHAT_MODERATION_CHANNEL_DESC) + } + + return Mustache.render(` +
+

{{title}}

+

{{description}}

+
+ `, view) as string + } catch (err: any) { + peertubeHelpers.notifier.error(err.toString()) + return '' + } +} + +export { + renderModerationChannel +} diff --git a/client/common/moderation/templates/home.ts b/client/common/moderation/templates/home.ts index 0d8704e0..ab8ea2c6 100644 --- a/client/common/moderation/templates/home.ts +++ b/client/common/moderation/templates/home.ts @@ -2,6 +2,11 @@ import type { RegisterClientOptions } from '@peertube/peertube-types/client' // Must use require for mustache, import seems buggy. const Mustache = require('mustache') +/** + * Renders the livechat moderation setup home page. + * @param registerClientOptions Peertube client options + * @returns The page content + */ async function renderModerationHome (registerClientOptions: RegisterClientOptions): Promise { const { peertubeHelpers } = registerClientOptions @@ -24,7 +29,7 @@ async function renderModerationHome (registerClientOptions: RegisterClientOption } for (const channel of channels.data) { - channel.livechatModerationUri = '/p/livechat/moderation/channel?id=' + encodeURIComponent(channel.id) + channel.livechatModerationUri = '/p/livechat/moderation/channel?channelId=' + encodeURIComponent(channel.id) } const view = { @@ -34,9 +39,6 @@ async function renderModerationHome (registerClientOptions: RegisterClientOption channels: channels.data } - // TODO: remove this line - console.log('Rendering the moderation home with view:', view) - return Mustache.render(`

{{title}}

diff --git a/languages/en.yml b/languages/en.yml index bb53c83e..f8a5ea85 100644 --- a/languages/en.yml +++ b/languages/en.yml @@ -283,3 +283,5 @@ menu_moderation_label: "Chatrooms" livechat_moderation_title: "Configure your live's chatrooms moderation policies" livechat_moderation_desc: "Here you can configure some advanced options for chatrooms associated to your live streams." livechat_moderation_please_select: "Please select bellow one of your channel, to setup its chatting options." +livechat_moderation_channel_title: "Moderation policies for channel:" +livechat_moderation_channel_desc: "You can setup here your moderation policies for this channel." diff --git a/server/lib/database/channel.ts b/server/lib/database/channel.ts index 142d1f24..d7a76d69 100644 --- a/server/lib/database/channel.ts +++ b/server/lib/database/channel.ts @@ -51,6 +51,7 @@ interface ChannelInfos { id: number name: string displayName: string + ownerAccountId: number } async function getChannelInfosById (options: RegisterServerOptions, channelId: number): Promise { @@ -64,7 +65,8 @@ async function getChannelInfosById (options: RegisterServerOptions, channelId: n 'SELECT' + ' "actor"."preferredUsername" as "channelName",' + ' "videoChannel"."id" as "channelId",' + - ' "videoChannel"."name" as "channelDisplayName"' + + ' "videoChannel"."name" as "channelDisplayName",' + + ' "videoChannel"."accountId" as "ownerAccountId"' + ' FROM "videoChannel"' + ' RIGHT JOIN "actor" ON "actor"."id" = "videoChannel"."actorId"' + ' WHERE "videoChannel"."id" = ' + channelId.toString() @@ -79,7 +81,8 @@ async function getChannelInfosById (options: RegisterServerOptions, channelId: n return { id: results[0].channelId, name: results[0].channelName ?? '', - displayName: results[0].channelDisplayName ?? '' + displayName: results[0].channelDisplayName ?? '', + ownerAccountId: results[0].ownerAccountId } } diff --git a/server/lib/helpers.ts b/server/lib/helpers.ts index 81a4cd44..66901ca9 100644 --- a/server/lib/helpers.ts +++ b/server/lib/helpers.ts @@ -75,6 +75,20 @@ async function isUserAdmin (options: RegisterServerOptions, res: Response): Prom return true } +async function isUserAdminOrModerator (options: RegisterServerOptions, res: Response): Promise { + const user = await options.peertubeHelpers.user.getAuthUser(res) + if (!user) { + return false + } + if (user.blocked) { + return false + } + if (user.role !== 0 && user.role !== 1) { + return false + } + return true +} + type Unpack = T extends Promise ? U : T type AuthUser = Unpack> @@ -95,6 +109,7 @@ export { getBaseWebSocketRoute, getBaseStaticRoute, isUserAdmin, + isUserAdminOrModerator, getUserNickname, pluginName, pluginShortName, diff --git a/server/lib/routers/api.ts b/server/lib/routers/api.ts index 6395ac34..4bed1709 100644 --- a/server/lib/routers/api.ts +++ b/server/lib/routers/api.ts @@ -7,6 +7,7 @@ import { isDebugMode } from '../debug' import { initRoomApiRouter } from './api/room' import { initAuthApiRouter, initUserAuthApiRouter } from './api/auth' import { initFederationServerInfosApiRouter } from './api/federation-server-infos' +import { initModerationApiRouter } from './api/moderation' /** * Initiate API routes @@ -52,6 +53,8 @@ async function initApiRouter (options: RegisterServerOptions): Promise { )) } + router.use('/moderation', await initModerationApiRouter(options)) + return router } diff --git a/server/lib/routers/api/moderation.ts b/server/lib/routers/api/moderation.ts new file mode 100644 index 00000000..102e7a3a --- /dev/null +++ b/server/lib/routers/api/moderation.ts @@ -0,0 +1,62 @@ +import type { RegisterServerOptions } from '@peertube/peertube-types' +import type { Router, Request, Response, NextFunction } from 'express' +import type { ChannelModerationOptions } from '../../../../shared/lib/types' +import { asyncMiddleware } from '../../middlewares/async' +import { getChannelInfosById } from '../../database/channel' +import { isUserAdminOrModerator } from '../../helpers' + +async function initModerationApiRouter (options: RegisterServerOptions): Promise { + const router = options.getRouter() + const logger = options.peertubeHelpers.logger + + router.get('/channel/:channelId', asyncMiddleware( + async (req: Request, res: Response, _next: NextFunction): Promise => { + const channelId = req.params.channelId + const currentUser = await options.peertubeHelpers.user.getAuthUser(res) + + if (!channelId || !/^\d+$/.test(channelId)) { + res.sendStatus(400) + return + } + + const channelInfos = await getChannelInfosById(options, parseInt(channelId)) + if (!channelInfos) { + logger.warn(`Channel ${channelId} not found`) + res.sendStatus(404) + return + } + + // To access this page, you must either be: + // - the channel owner, + // - an instance modo/admin + // - TODO: a channel chat moderator, as defined in this page. + if (channelInfos.ownerAccountId === currentUser.Account.id) { + logger.debug('Current user is the channel owner') + } else if (await isUserAdminOrModerator(options, res)) { + logger.debug('Current user is an instance moderator or admin') + } else { + logger.warn('Current user tries to access a channel for which he has no right.') + res.sendStatus(403) + return + } + + logger.debug('User can access the moderation channel api.') + + const result: ChannelModerationOptions = { + channel: { + id: channelInfos.id, + name: channelInfos.name, + displayName: channelInfos.displayName + } + } + res.status(200) + res.json(result) + } + )) + + return router +} + +export { + initModerationApiRouter +} diff --git a/shared/lib/types.ts b/shared/lib/types.ts index b89311f3..c55d0292 100644 --- a/shared/lib/types.ts +++ b/shared/lib/types.ts @@ -46,9 +46,18 @@ interface ProsodyListRoomsResultSuccess { type ProsodyListRoomsResult = ProsodyListRoomsResultError | ProsodyListRoomsResultSuccess +interface ChannelModerationOptions { + channel: { + id: number + name: string + displayName: string + } +} + export type { ConverseJSTheme, InitConverseJSParams, ProsodyListRoomsResult, - ProsodyListRoomsResultRoom + ProsodyListRoomsResultRoom, + ChannelModerationOptions }