Moderation configuration screen: WIP.
This commit is contained in:
		
							
								
								
									
										1
									
								
								client/@types/global.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								client/@types/global.d.ts
									
									
									
									
										vendored
									
									
								
							@ -33,6 +33,7 @@ declare const LOC_CONNECT_USING_XMPP_HELP: string
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
declare const LOC_SAVE: string
 | 
					declare const LOC_SAVE: string
 | 
				
			||||||
declare const LOC_CANCEL: string
 | 
					declare const LOC_CANCEL: string
 | 
				
			||||||
 | 
					declare const LOC_SUCCESSFULLY_SAVED: string
 | 
				
			||||||
declare const LOC_MENU_MODERATION_LABEL: string
 | 
					declare const LOC_MENU_MODERATION_LABEL: string
 | 
				
			||||||
declare const LOC_LIVECHAT_MODERATION_TITLE: string
 | 
					declare const LOC_LIVECHAT_MODERATION_TITLE: string
 | 
				
			||||||
declare const LOC_LIVECHAT_MODERATION_DESC: string
 | 
					declare const LOC_LIVECHAT_MODERATION_DESC: string
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,6 @@
 | 
				
			|||||||
import type { RegisterClientOptions } from '@peertube/peertube-types/client'
 | 
					import type { RegisterClientOptions } from '@peertube/peertube-types/client'
 | 
				
			||||||
 | 
					import type { ChannelModerationOptions } from 'shared/lib/types'
 | 
				
			||||||
 | 
					import { getBaseRoute } from '../../../videowatch/uri'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Adds the front-end logic on the generated html for the channel moderation options.
 | 
					 * Adds the front-end logic on the generated html for the channel moderation options.
 | 
				
			||||||
@ -7,10 +9,13 @@ import type { RegisterClientOptions } from '@peertube/peertube-types/client'
 | 
				
			|||||||
 */
 | 
					 */
 | 
				
			||||||
async function vivifyModerationChannel (
 | 
					async function vivifyModerationChannel (
 | 
				
			||||||
  clientOptions: RegisterClientOptions,
 | 
					  clientOptions: RegisterClientOptions,
 | 
				
			||||||
  rootEl: HTMLElement
 | 
					  rootEl: HTMLElement,
 | 
				
			||||||
 | 
					  channelId: string
 | 
				
			||||||
): Promise<void> {
 | 
					): Promise<void> {
 | 
				
			||||||
  const form = rootEl.querySelector('form[livechat-moderation-channel-options]') as HTMLFormElement
 | 
					  const form = rootEl.querySelector('form[livechat-moderation-channel-options]') as HTMLFormElement
 | 
				
			||||||
  if (!form) { return }
 | 
					  if (!form) { return }
 | 
				
			||||||
 | 
					  const labelSaved = await clientOptions.peertubeHelpers.translate(LOC_SUCCESSFULLY_SAVED)
 | 
				
			||||||
 | 
					  const labelError = await clientOptions.peertubeHelpers.translate(LOC_ERROR)
 | 
				
			||||||
  const enableBotCB = form.querySelector('input[name=bot]') as HTMLInputElement
 | 
					  const enableBotCB = form.querySelector('input[name=bot]') as HTMLInputElement
 | 
				
			||||||
  const botEnabledEl = form.querySelector('[livechat-moderation-channel-options-bot-enabled]') as HTMLElement
 | 
					  const botEnabledEl = form.querySelector('[livechat-moderation-channel-options-bot-enabled]') as HTMLElement
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -18,9 +23,59 @@ async function vivifyModerationChannel (
 | 
				
			|||||||
    botEnabledEl.style.display = enableBotCB.checked ? 'initial' : 'none'
 | 
					    botEnabledEl.style.display = enableBotCB.checked ? 'initial' : 'none'
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const submitForm: Function = async () => {
 | 
				
			||||||
 | 
					    const data = new FormData(form)
 | 
				
			||||||
 | 
					    const channelModerationOptions: ChannelModerationOptions = {
 | 
				
			||||||
 | 
					      bot: data.get('bot') === '1',
 | 
				
			||||||
 | 
					      bannedJIDs: (data.get('banned_jids')?.toString() ?? '').split(/\r?\n|\r|\n/g),
 | 
				
			||||||
 | 
					      forbiddenWords: (data.get('forbidden_words')?.toString() ?? '').split(/\r?\n|\r|\n/g)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const headers: any = clientOptions.peertubeHelpers.getAuthHeader() ?? {}
 | 
				
			||||||
 | 
					    headers['content-type'] = 'application/json;charset=UTF-8'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const response = await fetch(
 | 
				
			||||||
 | 
					      getBaseRoute(clientOptions) + '/api/moderation/channel/' + encodeURIComponent(channelId),
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        method: 'POST',
 | 
				
			||||||
 | 
					        headers,
 | 
				
			||||||
 | 
					        body: JSON.stringify(channelModerationOptions)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!response.ok) {
 | 
				
			||||||
 | 
					      throw new Error('Failed to save moderation options.')
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  const toggleSubmit: Function = (disabled: boolean) => {
 | 
				
			||||||
 | 
					    form.querySelectorAll('input[type=submit], input[type=reset]').forEach((el) => {
 | 
				
			||||||
 | 
					      if (disabled) {
 | 
				
			||||||
 | 
					        el.setAttribute('disabled', 'disabled')
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        el.removeAttribute('disabled')
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  enableBotCB.onclick = () => refresh()
 | 
					  enableBotCB.onclick = () => refresh()
 | 
				
			||||||
  form.onsubmit = () => false
 | 
					  form.onsubmit = () => {
 | 
				
			||||||
  form.onreset = () => refresh()
 | 
					    toggleSubmit(true)
 | 
				
			||||||
 | 
					    submitForm().then(
 | 
				
			||||||
 | 
					      () => {
 | 
				
			||||||
 | 
					        clientOptions.peertubeHelpers.notifier.success(labelSaved)
 | 
				
			||||||
 | 
					        toggleSubmit(false)
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      () => {
 | 
				
			||||||
 | 
					        clientOptions.peertubeHelpers.notifier.error(labelError)
 | 
				
			||||||
 | 
					        toggleSubmit(false)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    return false
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  form.onreset = () => {
 | 
				
			||||||
 | 
					    // Must refresh in a setTimeout, otherwise the checkbox state is not up to date.
 | 
				
			||||||
 | 
					    setTimeout(() => refresh(), 1)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
  refresh()
 | 
					  refresh()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -29,7 +29,7 @@ async function registerModeration (clientOptions: RegisterClientOptions): Promis
 | 
				
			|||||||
        return
 | 
					        return
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      rootEl.innerHTML = html
 | 
					      rootEl.innerHTML = html
 | 
				
			||||||
      await vivifyModerationChannel(clientOptions, rootEl)
 | 
					      await vivifyModerationChannel(clientOptions, rootEl, channelId)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,5 @@
 | 
				
			|||||||
import type { RegisterClientOptions } from '@peertube/peertube-types/client'
 | 
					import type { RegisterClientOptions } from '@peertube/peertube-types/client'
 | 
				
			||||||
import type { ChannelModerationOptions } from 'shared/lib/types'
 | 
					import type { ChannelModeration } from 'shared/lib/types'
 | 
				
			||||||
import { getBaseRoute } from '../../../videowatch/uri'
 | 
					import { getBaseRoute } from '../../../videowatch/uri'
 | 
				
			||||||
// Must use require for mustache, import seems buggy.
 | 
					// Must use require for mustache, import seems buggy.
 | 
				
			||||||
const Mustache = require('mustache')
 | 
					const Mustache = require('mustache')
 | 
				
			||||||
@ -21,18 +21,22 @@ async function renderModerationChannel (
 | 
				
			|||||||
      throw new Error('Missing or invalid channel id.')
 | 
					      throw new Error('Missing or invalid channel id.')
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const channelModerationOptions: ChannelModerationOptions = await (await fetch(
 | 
					    const response = await fetch(
 | 
				
			||||||
      getBaseRoute(registerClientOptions) + '/api/moderation/channel/' + encodeURIComponent(channelId),
 | 
					      getBaseRoute(registerClientOptions) + '/api/moderation/channel/' + encodeURIComponent(channelId),
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
        method: 'GET',
 | 
					        method: 'GET',
 | 
				
			||||||
        headers: peertubeHelpers.getAuthHeader()
 | 
					        headers: peertubeHelpers.getAuthHeader()
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    )).json()
 | 
					    )
 | 
				
			||||||
 | 
					    if (!response.ok) {
 | 
				
			||||||
    // Basic testing that channelModerationOptions has the correct format
 | 
					 | 
				
			||||||
    if ((typeof channelModerationOptions !== 'object') || !channelModerationOptions.channel) {
 | 
					 | 
				
			||||||
      throw new Error('Can\'t get channel moderation options.')
 | 
					      throw new Error('Can\'t get channel moderation options.')
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    const channelModeration: ChannelModeration = await (response).json()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Basic testing that channelModeration has the correct format
 | 
				
			||||||
 | 
					    if ((typeof channelModeration !== 'object') || !channelModeration.channel) {
 | 
				
			||||||
 | 
					      throw new Error('Invalid channel moderation options.')
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const view = {
 | 
					    const view = {
 | 
				
			||||||
      title: await peertubeHelpers.translate(LOC_LIVECHAT_MODERATION_CHANNEL_TITLE),
 | 
					      title: await peertubeHelpers.translate(LOC_LIVECHAT_MODERATION_CHANNEL_TITLE),
 | 
				
			||||||
@ -43,12 +47,12 @@ async function renderModerationChannel (
 | 
				
			|||||||
      bannedJIDs: await peertubeHelpers.translate(LOC_LIVECHAT_MODERATION_CHANNEL_BANNED_JIDS_LABEL),
 | 
					      bannedJIDs: await peertubeHelpers.translate(LOC_LIVECHAT_MODERATION_CHANNEL_BANNED_JIDS_LABEL),
 | 
				
			||||||
      save: await peertubeHelpers.translate(LOC_SAVE),
 | 
					      save: await peertubeHelpers.translate(LOC_SAVE),
 | 
				
			||||||
      cancel: await peertubeHelpers.translate(LOC_CANCEL),
 | 
					      cancel: await peertubeHelpers.translate(LOC_CANCEL),
 | 
				
			||||||
      channelModerationOptions
 | 
					      channelModeration
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Mustache.render(`
 | 
					    return Mustache.render(`
 | 
				
			||||||
      <div class="margin-content">
 | 
					      <div class="margin-content">
 | 
				
			||||||
        <h1>{{title}} {{channelModerationOptions.channel.displayName}}</h1>
 | 
					        <h1>{{title}} {{channelModeration.moderation.channel.displayName}}</h1>
 | 
				
			||||||
        <p>{{description}}</p>
 | 
					        <p>{{description}}</p>
 | 
				
			||||||
        <form livechat-moderation-channel-options>
 | 
					        <form livechat-moderation-channel-options>
 | 
				
			||||||
          <fieldset>
 | 
					          <fieldset>
 | 
				
			||||||
@ -56,7 +60,7 @@ async function renderModerationChannel (
 | 
				
			|||||||
              <input
 | 
					              <input
 | 
				
			||||||
                type="checkbox" name="bot"
 | 
					                type="checkbox" name="bot"
 | 
				
			||||||
                value="1"
 | 
					                value="1"
 | 
				
			||||||
                {{#channelModerationOptions.bot}} checked="checked" {{/channelModerationOptions.bot}}
 | 
					                {{#channelModeration.moderation.bot}} checked="checked" {{/channelModeration.moderation.bot}}
 | 
				
			||||||
              />
 | 
					              />
 | 
				
			||||||
              {{enableBot}}
 | 
					              {{enableBot}}
 | 
				
			||||||
            </label>
 | 
					            </label>
 | 
				
			||||||
@ -66,15 +70,15 @@ async function renderModerationChannel (
 | 
				
			|||||||
            <label>
 | 
					            <label>
 | 
				
			||||||
              {{forbiddenWords}}
 | 
					              {{forbiddenWords}}
 | 
				
			||||||
<textarea name="forbidden_words">
 | 
					<textarea name="forbidden_words">
 | 
				
			||||||
{{#channelModerationOptions.forbiddenWords}}{{.}}
 | 
					{{#channelModeration.moderation.forbiddenWords}}{{.}}
 | 
				
			||||||
{{/channelModerationOptions.forbiddenWords}}
 | 
					{{/channelModeration.moderation.forbiddenWords}}
 | 
				
			||||||
</textarea>
 | 
					</textarea>
 | 
				
			||||||
            </label>
 | 
					            </label>
 | 
				
			||||||
            <label>
 | 
					            <label>
 | 
				
			||||||
              {{bannedJIDs}}
 | 
					              {{bannedJIDs}}
 | 
				
			||||||
<textarea name="banned_jids">
 | 
					<textarea name="banned_jids">
 | 
				
			||||||
{{#channelModerationOptions.bannedJIDs}}{{.}}
 | 
					{{#channelModeration.moderation.bannedJIDs}}{{.}}
 | 
				
			||||||
{{/channelModerationOptions.bannedJIDs}}
 | 
					{{/channelModeration.moderation.bannedJIDs}}
 | 
				
			||||||
</textarea>
 | 
					</textarea>
 | 
				
			||||||
            </label>
 | 
					            </label>
 | 
				
			||||||
          </fieldset>
 | 
					          </fieldset>
 | 
				
			||||||
 | 
				
			|||||||
@ -281,6 +281,7 @@ prosody_components_list_description: |
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
save: "Save"
 | 
					save: "Save"
 | 
				
			||||||
cancel: "Cancel"
 | 
					cancel: "Cancel"
 | 
				
			||||||
 | 
					successfully_saved: "Successfully saved"
 | 
				
			||||||
menu_moderation_label: "Chatrooms"
 | 
					menu_moderation_label: "Chatrooms"
 | 
				
			||||||
livechat_moderation_title: "Configure your live's chatrooms moderation policies"
 | 
					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_desc: "Here you can configure some advanced options for chatrooms associated to your live streams."
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										53
									
								
								server/lib/middlewares/moderation/channel.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								server/lib/middlewares/moderation/channel.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,53 @@
 | 
				
			|||||||
 | 
					import type { RegisterServerOptions } from '@peertube/peertube-types'
 | 
				
			||||||
 | 
					import type { Request, Response, NextFunction } from 'express'
 | 
				
			||||||
 | 
					import type { RequestPromiseHandler } from '../async'
 | 
				
			||||||
 | 
					import { getChannelInfosById } from '../../database/channel'
 | 
				
			||||||
 | 
					import { isUserAdminOrModerator } from '../../helpers'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Returns a middleware handler to get the channelInfos from the channel parameter.
 | 
				
			||||||
 | 
					 * This is used in api related to channel moderation options.
 | 
				
			||||||
 | 
					 * @param options Peertube server options
 | 
				
			||||||
 | 
					 * @returns middleware function
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					function getCheckModerationChannelMiddleware (options: RegisterServerOptions): RequestPromiseHandler {
 | 
				
			||||||
 | 
					  return async (req: Request, res: Response, next: NextFunction) => {
 | 
				
			||||||
 | 
					    const logger = options.peertubeHelpers.logger
 | 
				
			||||||
 | 
					    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), true)
 | 
				
			||||||
 | 
					    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.')
 | 
				
			||||||
 | 
					    res.locals.channelInfos = channelInfos
 | 
				
			||||||
 | 
					    next()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export {
 | 
				
			||||||
 | 
					  getCheckModerationChannelMiddleware
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										62
									
								
								server/lib/moderation/channel/sanitize.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								server/lib/moderation/channel/sanitize.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,62 @@
 | 
				
			|||||||
 | 
					import type { RegisterServerOptions } from '@peertube/peertube-types'
 | 
				
			||||||
 | 
					import type { ChannelModerationOptions, ChannelInfos } from '../../../../shared/lib/types'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Sanitize data so that they can safely be used/stored for channel moderation configuration.
 | 
				
			||||||
 | 
					 * Throw an error if the format is obviously wrong.
 | 
				
			||||||
 | 
					 * Cleans data (removing empty values, ...)
 | 
				
			||||||
 | 
					 * @param options Peertube server options
 | 
				
			||||||
 | 
					 * @param _channelInfos Channel infos
 | 
				
			||||||
 | 
					 * @param data Input data
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					async function sanitizeChannelModerationOptions (
 | 
				
			||||||
 | 
					  _options: RegisterServerOptions,
 | 
				
			||||||
 | 
					  _channelInfos: ChannelInfos,
 | 
				
			||||||
 | 
					  data: any
 | 
				
			||||||
 | 
					): Promise<ChannelModerationOptions> {
 | 
				
			||||||
 | 
					  const result = {
 | 
				
			||||||
 | 
					    bot: false,
 | 
				
			||||||
 | 
					    bannedJIDs: [],
 | 
				
			||||||
 | 
					    forbiddenWords: []
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (typeof data !== 'object') {
 | 
				
			||||||
 | 
					    throw new Error('Invalid data type')
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  // boolean fields
 | 
				
			||||||
 | 
					  for (const f of ['bot']) {
 | 
				
			||||||
 | 
					    if (!(f in data) || (typeof data[f] !== 'boolean')) {
 | 
				
			||||||
 | 
					      throw new Error('Invalid data type for field ' + f)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    result[f as keyof ChannelModerationOptions] = data[f]
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  // value/regexp array fields
 | 
				
			||||||
 | 
					  for (const f of ['bannedJIDs', 'forbiddenWords']) {
 | 
				
			||||||
 | 
					    if (!(f in data) || !Array.isArray(data[f])) {
 | 
				
			||||||
 | 
					      throw new Error('Invalid data type for field ' + f)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    for (const v of data[f]) {
 | 
				
			||||||
 | 
					      if (typeof v !== 'string') {
 | 
				
			||||||
 | 
					        throw new Error('Invalid data type in a value of field ' + f)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (v === '' || /^\s+$/.test(v)) {
 | 
				
			||||||
 | 
					        // ignore empty values
 | 
				
			||||||
 | 
					        continue
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      // value must be a valid regexp
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        // eslint-disable-next-line no-new
 | 
				
			||||||
 | 
					        new RegExp(v)
 | 
				
			||||||
 | 
					      } catch (_err) {
 | 
				
			||||||
 | 
					        throw new Error('Invalid value in field ' + f)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      (result[f as keyof ChannelModerationOptions] as string[]).push(v)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return result
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export {
 | 
				
			||||||
 | 
					  sanitizeChannelModerationOptions
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										28
									
								
								server/lib/moderation/channel/storage.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								server/lib/moderation/channel/storage.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,28 @@
 | 
				
			|||||||
 | 
					import type { RegisterServerOptions } from '@peertube/peertube-types'
 | 
				
			||||||
 | 
					import type { ChannelModeration, ChannelInfos } from '../../../../shared/lib/types'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function getChannelModerationOptions (
 | 
				
			||||||
 | 
					  options: RegisterServerOptions,
 | 
				
			||||||
 | 
					  channelInfos: ChannelInfos
 | 
				
			||||||
 | 
					): Promise<ChannelModeration> {
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    channel: channelInfos,
 | 
				
			||||||
 | 
					    moderation: {
 | 
				
			||||||
 | 
					      bot: false,
 | 
				
			||||||
 | 
					      bannedJIDs: [],
 | 
				
			||||||
 | 
					      forbiddenWords: []
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function storeChannelModerationOptions (
 | 
				
			||||||
 | 
					  _options: RegisterServerOptions,
 | 
				
			||||||
 | 
					  _channelModeration: ChannelModeration
 | 
				
			||||||
 | 
					): Promise<void> {
 | 
				
			||||||
 | 
					  throw new Error('Not implemented yet')
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export {
 | 
				
			||||||
 | 
					  getChannelModerationOptions,
 | 
				
			||||||
 | 
					  storeChannelModerationOptions
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,61 +1,60 @@
 | 
				
			|||||||
import type { RegisterServerOptions } from '@peertube/peertube-types'
 | 
					import type { RegisterServerOptions } from '@peertube/peertube-types'
 | 
				
			||||||
import type { Router, Request, Response, NextFunction } from 'express'
 | 
					import type { Router, Request, Response, NextFunction } from 'express'
 | 
				
			||||||
import type { ChannelModerationOptions } from '../../../../shared/lib/types'
 | 
					import type { ChannelInfos } from '../../../../shared/lib/types'
 | 
				
			||||||
import { asyncMiddleware } from '../../middlewares/async'
 | 
					import { asyncMiddleware } from '../../middlewares/async'
 | 
				
			||||||
import { getChannelInfosById } from '../../database/channel'
 | 
					import { getCheckModerationChannelMiddleware } from '../../middlewares/moderation/channel'
 | 
				
			||||||
import { isUserAdminOrModerator } from '../../helpers'
 | 
					import { getChannelModerationOptions, storeChannelModerationOptions } from '../../moderation/channel/storage'
 | 
				
			||||||
 | 
					import { sanitizeChannelModerationOptions } from '../../moderation/channel/sanitize'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function initModerationApiRouter (options: RegisterServerOptions): Promise<Router> {
 | 
					async function initModerationApiRouter (options: RegisterServerOptions): Promise<Router> {
 | 
				
			||||||
  const router = options.getRouter()
 | 
					  const router = options.getRouter()
 | 
				
			||||||
  const logger = options.peertubeHelpers.logger
 | 
					  const logger = options.peertubeHelpers.logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  router.get('/channel/:channelId', asyncMiddleware(
 | 
					  router.get('/channel/:channelId', asyncMiddleware([
 | 
				
			||||||
 | 
					    getCheckModerationChannelMiddleware(options),
 | 
				
			||||||
    async (req: Request, res: Response, _next: NextFunction): Promise<void> => {
 | 
					    async (req: Request, res: Response, _next: NextFunction): Promise<void> => {
 | 
				
			||||||
      const channelId = req.params.channelId
 | 
					      if (!res.locals.channelInfos) {
 | 
				
			||||||
      const currentUser = await options.peertubeHelpers.user.getAuthUser(res)
 | 
					        logger.error('Missing channelInfos in res.locals, should not happen')
 | 
				
			||||||
 | 
					        res.sendStatus(500)
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      const channelInfos = res.locals.channelInfos as ChannelInfos
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (!channelId || !/^\d+$/.test(channelId)) {
 | 
					      const result = await getChannelModerationOptions(options, channelInfos)
 | 
				
			||||||
 | 
					      res.status(200)
 | 
				
			||||||
 | 
					      res.json(result)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  ]))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  router.post('/channel/:channelId', asyncMiddleware([
 | 
				
			||||||
 | 
					    getCheckModerationChannelMiddleware(options),
 | 
				
			||||||
 | 
					    async (req: Request, res: Response, _next: NextFunction): Promise<void> => {
 | 
				
			||||||
 | 
					      if (!res.locals.channelInfos) {
 | 
				
			||||||
 | 
					        logger.error('Missing channelInfos in res.locals, should not happen')
 | 
				
			||||||
 | 
					        res.sendStatus(500)
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      const channelInfos = res.locals.channelInfos as ChannelInfos
 | 
				
			||||||
 | 
					      logger.debug('Trying to save ChannelModerationOptions')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      let moderation
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        moderation = await sanitizeChannelModerationOptions(options, channelInfos, req.body)
 | 
				
			||||||
 | 
					      } catch (err) {
 | 
				
			||||||
 | 
					        logger.warn(err)
 | 
				
			||||||
        res.sendStatus(400)
 | 
					        res.sendStatus(400)
 | 
				
			||||||
        return
 | 
					        return
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const channelInfos = await getChannelInfosById(options, parseInt(channelId), true)
 | 
					      logger.debug('Data seems ok, storing them.')
 | 
				
			||||||
      if (!channelInfos) {
 | 
					      const result = await storeChannelModerationOptions(options, {
 | 
				
			||||||
        logger.warn(`Channel ${channelId} not found`)
 | 
					        channel: channelInfos,
 | 
				
			||||||
        res.sendStatus(404)
 | 
					        moderation
 | 
				
			||||||
        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
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        bot: false,
 | 
					 | 
				
			||||||
        forbiddenWords: [],
 | 
					 | 
				
			||||||
        bannedJIDs: []
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      res.status(200)
 | 
					      res.status(200)
 | 
				
			||||||
      res.json(result)
 | 
					      res.json(result)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  ))
 | 
					  ]))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return router
 | 
					  return router
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -46,21 +46,29 @@ interface ProsodyListRoomsResultSuccess {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
type ProsodyListRoomsResult = ProsodyListRoomsResultError | ProsodyListRoomsResultSuccess
 | 
					type ProsodyListRoomsResult = ProsodyListRoomsResultError | ProsodyListRoomsResultSuccess
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ChannelInfos {
 | 
				
			||||||
 | 
					  id: number
 | 
				
			||||||
 | 
					  name: string
 | 
				
			||||||
 | 
					  displayName: string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface ChannelModerationOptions {
 | 
					interface ChannelModerationOptions {
 | 
				
			||||||
  channel: {
 | 
					 | 
				
			||||||
    id: number
 | 
					 | 
				
			||||||
    name: string
 | 
					 | 
				
			||||||
    displayName: string
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  bot: boolean
 | 
					  bot: boolean
 | 
				
			||||||
  forbiddenWords: string[]
 | 
					  forbiddenWords: string[]
 | 
				
			||||||
  bannedJIDs: string[]
 | 
					  bannedJIDs: string[]
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ChannelModeration {
 | 
				
			||||||
 | 
					  channel: ChannelInfos
 | 
				
			||||||
 | 
					  moderation: ChannelModerationOptions
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type {
 | 
					export type {
 | 
				
			||||||
  ConverseJSTheme,
 | 
					  ConverseJSTheme,
 | 
				
			||||||
  InitConverseJSParams,
 | 
					  InitConverseJSParams,
 | 
				
			||||||
  ProsodyListRoomsResult,
 | 
					  ProsodyListRoomsResult,
 | 
				
			||||||
  ProsodyListRoomsResultRoom,
 | 
					  ProsodyListRoomsResultRoom,
 | 
				
			||||||
  ChannelModerationOptions
 | 
					  ChannelInfos,
 | 
				
			||||||
 | 
					  ChannelModerationOptions,
 | 
				
			||||||
 | 
					  ChannelModeration
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user