diff --git a/CHANGELOG.md b/CHANGELOG.md
index 45ba6ac7..b19157e8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,7 +8,6 @@ TODO: tag custom ConverseJS, and update build-conversejs.sh.
* #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/assets/images/moderator.svg b/assets/images/moderator.svg
new file mode 100644
index 00000000..c88e229b
--- /dev/null
+++ b/assets/images/moderator.svg
@@ -0,0 +1,147 @@
+
+
diff --git a/assets/styles/style.scss b/assets/styles/style.scss
index f17b80f8..877438e6 100644
--- a/assets/styles/style.scss
+++ b/assets/styles/style.scss
@@ -53,8 +53,11 @@
display: none;
}
-[peertube-plugin-livechat-state="closed"] .peertube-plugin-livechat-button-close {
- display: none;
+[peertube-plugin-livechat-state="closed"] {
+ .peertube-plugin-livechat-button-promote,
+ .peertube-plugin-livechat-button-close {
+ display: none;
+ }
}
[peertube-plugin-livechat-state]:not([peertube-plugin-livechat-state="open"]) {
diff --git a/client/@types/global.d.ts b/client/@types/global.d.ts
index 1a5cc358..48961d30 100644
--- a/client/@types/global.d.ts
+++ b/client/@types/global.d.ts
@@ -77,3 +77,5 @@ declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FOR_MORE_INFO: string
declare const LOC_INVALID_VALUE: string
declare const LOC_CHATROOM_NOT_ACCESSIBLE: string
+
+declare const LOC_PROMOTE: string
diff --git a/client/admin-plugin-client-plugin.ts b/client/admin-plugin-client-plugin.ts
index faf8cd0f..81b1b708 100644
--- a/client/admin-plugin-client-plugin.ts
+++ b/client/admin-plugin-client-plugin.ts
@@ -121,6 +121,7 @@ function register (clientOptions: RegisterClientOptions): void {
titleChannelConfiguration.textContent = labels.channelConfiguration
titleLineEl.append(titleChannelConfiguration)
}
+ titleLineEl.append(document.createElement('th'))
table.append(titleLineEl)
rooms.forEach(room => {
const localpart = room.localpart
@@ -137,6 +138,35 @@ function register (clientOptions: RegisterClientOptions): void {
const date = new Date(room.lasttimestamp * 1000)
lastActivityEl.textContent = date.toLocaleDateString() + ' ' + date.toLocaleTimeString()
}
+ const promoteButton = document.createElement('a')
+ promoteButton.classList.add('orange-button', 'peertube-button-link')
+ promoteButton.style.margin = '5px'
+ promoteButton.onclick = async () => {
+ await fetch(
+ getBaseRoute(clientOptions) + '/api/promote/' + encodeURIComponent(room.jid.replace(/@.*$/, '')),
+ {
+ method: 'PUT',
+ headers: peertubeHelpers.getAuthHeader()
+ }
+ )
+ }
+ // FIXME: we can use promoteSVG, which is in client scope...
+ promoteButton.innerHTML = ``
+ const promoteEl = document.createElement('td')
+ promoteEl.append(promoteButton)
+
const channelConfigurationEl = document.createElement('td')
nameEl.append(aEl)
lineEl.append(nameEl)
@@ -146,6 +176,7 @@ function register (clientOptions: RegisterClientOptions): void {
if (useChannelConfiguration) {
lineEl.append(channelConfigurationEl) // else the element will just be dropped.
}
+ lineEl.append(promoteEl)
table.append(lineEl)
const writeChannelConfigurationLink = (channelId: number | string): void => {
diff --git a/client/videowatch-client-plugin.ts b/client/videowatch-client-plugin.ts
index e84d26a2..7ffca1e5 100644
--- a/client/videowatch-client-plugin.ts
+++ b/client/videowatch-client-plugin.ts
@@ -1,12 +1,16 @@
import type { Video } from '@peertube/peertube-types'
import type { RegisterClientOptions } from '@peertube/peertube-types/client'
+import type { InitConverseJSParams } from 'shared/lib/types'
import { videoHasWebchat, videoHasRemoteWebchat } from 'shared/lib/video'
import { localizedHelpUrl } from './utils/help'
import { logger } from './utils/logger'
-import { closeSVG, openBlankChatSVG, openChatSVG, shareChatUrlSVG, helpButtonSVG } from './videowatch/buttons'
+import {
+ closeSVG, openBlankChatSVG, openChatSVG, shareChatUrlSVG, helpButtonSVG, promoteSVG
+} from './videowatch/buttons'
import { displayButton, displayButtonOptions } from './videowatch/button'
import { shareChatUrl } from './videowatch/share'
import { displayConverseJS } from './utils/conversejs'
+import { getBaseRoute } from './utils/uri'
interface VideoWatchLoadedHookOptions {
videojs: any
@@ -71,7 +75,11 @@ function register (registerOptions: RegisterClientOptions): void {
let settings: any = {} // will be loaded later
async function insertChatDom (
- container: HTMLElement, video: Video, showOpenBlank: boolean, showShareUrlButton: boolean
+ container: HTMLElement,
+ video: Video,
+ showOpenBlank: boolean,
+ showShareUrlButton: boolean,
+ showPromote: boolean
): Promise {
logger.log('Adding livechat in the DOM...')
const viewersDocumentationHelpUrl = await localizedHelpUrl(registerOptions, {
@@ -84,13 +92,15 @@ function register (registerOptions: RegisterClientOptions): void {
peertubeHelpers.translate(LOC_OPEN_CHAT_NEW_WINDOW),
peertubeHelpers.translate(LOC_CLOSE_CHAT),
peertubeHelpers.translate(LOC_SHARE_CHAT_LINK),
- peertubeHelpers.translate(LOC_ONLINE_HELP)
+ peertubeHelpers.translate(LOC_ONLINE_HELP),
+ peertubeHelpers.translate(LOC_PROMOTE)
]).then(labels => {
const labelOpen = labels[0]
const labelOpenBlank = labels[1]
const labelClose = labels[2]
const labelShareUrl = labels[3]
const labelHelp = labels[4]
+ const labelPromote = labels[5]
const buttonContainer = document.createElement('div')
buttonContainer.classList.add('peertube-plugin-livechat-buttons')
@@ -133,6 +143,45 @@ function register (registerOptions: RegisterClientOptions): void {
additionalClasses: []
})
}
+ if (showPromote) {
+ groupButtons.push({
+ buttonContainer,
+ name: 'promote',
+ label: labelPromote,
+ callback: async () => {
+ try {
+ // First we must get the room JID (can be video.uuid@ or channel.id@)
+ const response = await fetch(
+ getBaseRoute(registerOptions) + '/api/configuration/room/' +
+ encodeURIComponent(video.uuid),
+ {
+ method: 'GET',
+ headers: peertubeHelpers.getAuthHeader()
+ }
+ )
+
+ const converseJSParams: InitConverseJSParams = await (response).json()
+ if (converseJSParams.isRemoteChat) {
+ throw new Error('Cant promote on remote chat.')
+ }
+
+ const roomJIDLocalPart = converseJSParams.room.replace(/@.*$/, '')
+
+ await fetch(
+ getBaseRoute(registerOptions) + '/api/promote/' + encodeURIComponent(roomJIDLocalPart),
+ {
+ method: 'PUT',
+ headers: peertubeHelpers.getAuthHeader()
+ }
+ )
+ } catch (err) {
+ console.error(err)
+ }
+ },
+ icon: promoteSVG,
+ additionalClasses: []
+ })
+ }
groupButtons.push({
buttonContainer,
name: 'help',
@@ -287,6 +336,7 @@ function register (registerOptions: RegisterClientOptions): void {
}
let showShareUrlButton: boolean = false
+ let showPromote: boolean = false
if (video.isLocal) { // No need for shareButton on remote chats.
const chatShareUrl = settings['chat-share-url'] ?? ''
if (chatShareUrl === 'everyone') {
@@ -296,9 +346,19 @@ function register (registerOptions: RegisterClientOptions): void {
} else if (chatShareUrl === 'owner+moderators') {
showShareUrlButton = guessIsMine(registerOptions, video) || guessIamIModerator(registerOptions)
}
+
+ if (guessIamIModerator(registerOptions)) {
+ showPromote = true
+ }
}
- await insertChatDom(container as HTMLElement, video, !!settings['chat-open-blank'], showShareUrlButton)
+ await insertChatDom(
+ container as HTMLElement,
+ video,
+ !!settings['chat-open-blank'],
+ showShareUrlButton,
+ showPromote
+ )
if (settings['chat-auto-display']) {
await openChat(video)
} else if (container) {
diff --git a/client/videowatch/button.ts b/client/videowatch/button.ts
index 0d431b09..70c3091e 100644
--- a/client/videowatch/button.ts
+++ b/client/videowatch/button.ts
@@ -10,7 +10,7 @@ interface displayButtonOptionsBase {
}
interface displayButtonOptionsCallback extends displayButtonOptionsBase {
- callback: () => void | boolean
+ callback: () => void | boolean | Promise
}
interface displayButtonOptionsHref extends displayButtonOptionsBase {
diff --git a/client/videowatch/buttons.ts b/client/videowatch/buttons.ts
index dae35ca5..0c40a343 100644
--- a/client/videowatch/buttons.ts
+++ b/client/videowatch/buttons.ts
@@ -111,12 +111,26 @@ const helpButtonSVG: SVGButton = () => {
`
}
+const promoteSVG: SVGButton = () => {
+ // This content comes from the file assets/images/moderator.svg, after svgo cleaning.
+ // To get the formated content, you can do:
+ // xmllint dist/client/images/moderator.svg --format
+ // Then replace the color by `currentColor`
+ return `
+`
+}
+
export {
closeSVG,
openChatSVG,
openBlankChatSVG,
shareChatUrlSVG,
- helpButtonSVG
+ helpButtonSVG,
+ promoteSVG
}
export type {
SVGButton
diff --git a/languages/en.yml b/languages/en.yml
index a8e8b333..7c5f924f 100644
--- a/languages/en.yml
+++ b/languages/en.yml
@@ -452,3 +452,5 @@ task_list_pick_message: |
Once you have chosen a task list, a new task will be created.
To see the task, open the task application using the top menu.
More information in the livechat plugin documentation.
+
+promote: 'Become moderator'
diff --git a/server/lib/conversejs/params.ts b/server/lib/conversejs/params.ts
index 758cafd7..784e88d9 100644
--- a/server/lib/conversejs/params.ts
+++ b/server/lib/conversejs/params.ts
@@ -376,6 +376,8 @@ async function _localRoomJID (
}
room = room.replace(/{{CHANNEL_ID}}/g, `${channelId}`)
if (room.includes('{{CHANNEL_NAME}}')) {
+ // FIXME: this should no more exists, since we removed options to include other chat server.
+ // So we should remove this code. (and simplify the above code)
const channelName = await getChannelNameById(options, channelId)
if (channelName === null) {
throw new Error('Channel not found')
diff --git a/server/lib/routers/api.ts b/server/lib/routers/api.ts
index 5ce0257b..ebd0c8e3 100644
--- a/server/lib/routers/api.ts
+++ b/server/lib/routers/api.ts
@@ -8,6 +8,7 @@ import { initRoomApiRouter } from './api/room'
import { initAuthApiRouter, initUserAuthApiRouter } from './api/auth'
import { initFederationServerInfosApiRouter } from './api/federation-server-infos'
import { initConfigurationApiRouter } from './api/configuration'
+import { initPromoteApiRouter } from './api/promote'
/**
* Initiate API routes
@@ -36,6 +37,7 @@ async function initApiRouter (options: RegisterServerOptions): Promise {
await initFederationServerInfosApiRouter(options, router)
await initConfigurationApiRouter(options, router)
+ await initPromoteApiRouter(options, router)
if (isDebugMode(options)) {
// Only add this route if the debug mode is enabled at time of the server launch.
diff --git a/server/lib/routers/api/promote.ts b/server/lib/routers/api/promote.ts
new file mode 100644
index 00000000..6f2f0fef
--- /dev/null
+++ b/server/lib/routers/api/promote.ts
@@ -0,0 +1,53 @@
+import type { RegisterServerOptions } from '@peertube/peertube-types'
+import type { Router, Request, Response, NextFunction } from 'express'
+import type { Affiliations } from '../../prosody/config/affiliations'
+import { asyncMiddleware } from '../../middlewares/async'
+import { isUserAdminOrModerator } from '../../helpers'
+import { getProsodyDomain } from '../../prosody/config/domain'
+import { updateProsodyRoom } from '../../prosody/api/manage-rooms'
+
+async function initPromoteApiRouter (options: RegisterServerOptions, router: Router): Promise {
+ const logger = options.peertubeHelpers.logger
+
+ router.put('/promote/:roomJID', asyncMiddleware(
+ async (req: Request, res: Response, _next: NextFunction): Promise => {
+ try {
+ const roomJIDLocalPart = req.params.roomJID
+ const user = await options.peertubeHelpers.user.getAuthUser(res)
+
+ if (!user || !await isUserAdminOrModerator(options, res)) {
+ logger.warn('Current user tries to access the promote API for which he has no right.')
+ res.sendStatus(403)
+ return
+ }
+
+ if (!/^(channel\.\d+|(\w|-)+)$/.test(roomJIDLocalPart)) { // just check if it looks alright.
+ logger.warn('Current user tries to access the promote API using an invalid room key.')
+ res.sendStatus(400)
+ return
+ }
+
+ const normalizedUsername = user.username.toLowerCase()
+ const prosodyDomain = await getProsodyDomain(options)
+ const jid = normalizedUsername + '@' + prosodyDomain
+
+ const mucJID = roomJIDLocalPart + '@' + 'room.' + prosodyDomain
+
+ logger.info('We must give owner affiliation to ' + jid + ' on ' + mucJID)
+ const addAffiliations: Affiliations = {}
+ addAffiliations[jid] = 'owner'
+ await updateProsodyRoom(options, mucJID, {
+ addAffiliations
+ })
+ res.sendStatus(200)
+ } catch (err) {
+ logger.error(err)
+ res.sendStatus(500)
+ }
+ }
+ ))
+}
+
+export {
+ initPromoteApiRouter
+}