Changing defaults MUC affiliation (#385):
* For Peertube moderators/admins, we add a button "Promote". Clicking on it will promote them as MUC owner.
This commit is contained in:
parent
5745e8c8a3
commit
da75765bdb
@ -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/).
|
* #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: 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
|
||||||
|
|
||||||
|
147
assets/images/moderator.svg
Normal file
147
assets/images/moderator.svg
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 4.2333332 4.2333334"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1428"
|
||||||
|
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
||||||
|
sodipodi:docname="moderator.svg"
|
||||||
|
inkscape:export-xdpi="9.6000004"
|
||||||
|
inkscape:export-ydpi="9.6000004"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:osb="http://www.openswatchbook.org/uri/2009/osb">
|
||||||
|
<defs
|
||||||
|
id="defs1422">
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient5158"
|
||||||
|
osb:paint="solid">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#7d7d7d;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop5156" />
|
||||||
|
</linearGradient>
|
||||||
|
<filter
|
||||||
|
style="color-interpolation-filters:sRGB"
|
||||||
|
inkscape:label="Drop Shadow"
|
||||||
|
id="filter1150">
|
||||||
|
<feFlood
|
||||||
|
flood-opacity="1"
|
||||||
|
flood-color="rgb(0,0,0)"
|
||||||
|
result="flood"
|
||||||
|
id="feFlood1140" />
|
||||||
|
<feComposite
|
||||||
|
in="flood"
|
||||||
|
in2="SourceGraphic"
|
||||||
|
operator="in"
|
||||||
|
result="composite1"
|
||||||
|
id="feComposite1142" />
|
||||||
|
<feGaussianBlur
|
||||||
|
in="composite1"
|
||||||
|
stdDeviation="0.2"
|
||||||
|
result="blur"
|
||||||
|
id="feGaussianBlur1144" />
|
||||||
|
<feOffset
|
||||||
|
dx="0"
|
||||||
|
dy="0"
|
||||||
|
result="offset"
|
||||||
|
id="feOffset1146" />
|
||||||
|
<feComposite
|
||||||
|
in="SourceGraphic"
|
||||||
|
in2="offset"
|
||||||
|
operator="over"
|
||||||
|
result="composite2"
|
||||||
|
id="feComposite1148" />
|
||||||
|
</filter>
|
||||||
|
<filter
|
||||||
|
style="color-interpolation-filters:sRGB"
|
||||||
|
inkscape:label="Drop Shadow"
|
||||||
|
id="filter1174">
|
||||||
|
<feFlood
|
||||||
|
flood-opacity="1"
|
||||||
|
flood-color="rgb(0,0,0)"
|
||||||
|
result="flood"
|
||||||
|
id="feFlood1164" />
|
||||||
|
<feComposite
|
||||||
|
in="flood"
|
||||||
|
in2="SourceGraphic"
|
||||||
|
operator="in"
|
||||||
|
result="composite1"
|
||||||
|
id="feComposite1166" />
|
||||||
|
<feGaussianBlur
|
||||||
|
in="composite1"
|
||||||
|
stdDeviation="0.2"
|
||||||
|
result="blur"
|
||||||
|
id="feGaussianBlur1168" />
|
||||||
|
<feOffset
|
||||||
|
dx="0"
|
||||||
|
dy="0"
|
||||||
|
result="offset"
|
||||||
|
id="feOffset1170" />
|
||||||
|
<feComposite
|
||||||
|
in="SourceGraphic"
|
||||||
|
in2="offset"
|
||||||
|
operator="over"
|
||||||
|
result="composite2"
|
||||||
|
id="feComposite1172" />
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="22.627417"
|
||||||
|
inkscape:cx="11.203223"
|
||||||
|
inkscape:cy="12.683728"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
inkscape:current-layer="g1910"
|
||||||
|
inkscape:document-rotation="0"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:window-width="1842"
|
||||||
|
inkscape:window-height="1011"
|
||||||
|
inkscape:window-x="1920"
|
||||||
|
inkscape:window-y="1082"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
units="px"
|
||||||
|
showguides="true"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1" />
|
||||||
|
<metadata
|
||||||
|
id="metadata1425">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Calque 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<g
|
||||||
|
id="g1910"
|
||||||
|
transform="matrix(0.45207703,0,0,0.45207703,-0.52645128,1.334609)"
|
||||||
|
style="stroke-width:1.00021;stroke-miterlimit:4;stroke-dasharray:none">
|
||||||
|
<path
|
||||||
|
id="path1141"
|
||||||
|
style="opacity:0.998;fill:#deddda;fill-opacity:1;stroke:#deddda;stroke-width:1.17052;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
inkscape:label="rect1876-6"
|
||||||
|
d="m 2.4029499,0.58273862 0,-0.77222952 c 0,-0.5219815 0.4202233,-0.9422049 0.9422049,-0.9422049 l 1.9614175,-0.023585 1.9614174,0.010033 c 0.5219816,0 0.9422049,0.42022334 0.9422049,0.94220484 v 0.77222958 c 0,0 -1.6246328,4.32927978 -2.9036223,4.34980548 C 4.0275827,4.939518 2.4029499,0.58273862 2.4029499,0.58273862 Z"
|
||||||
|
sodipodi:nodetypes="cscccsczc" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 4.4 KiB |
@ -53,8 +53,11 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
[peertube-plugin-livechat-state="closed"] .peertube-plugin-livechat-button-close {
|
[peertube-plugin-livechat-state="closed"] {
|
||||||
display: none;
|
.peertube-plugin-livechat-button-promote,
|
||||||
|
.peertube-plugin-livechat-button-close {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[peertube-plugin-livechat-state]:not([peertube-plugin-livechat-state="open"]) {
|
[peertube-plugin-livechat-state]:not([peertube-plugin-livechat-state="open"]) {
|
||||||
|
2
client/@types/global.d.ts
vendored
2
client/@types/global.d.ts
vendored
@ -77,3 +77,5 @@ declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FOR_MORE_INFO: string
|
|||||||
|
|
||||||
declare const LOC_INVALID_VALUE: string
|
declare const LOC_INVALID_VALUE: string
|
||||||
declare const LOC_CHATROOM_NOT_ACCESSIBLE: string
|
declare const LOC_CHATROOM_NOT_ACCESSIBLE: string
|
||||||
|
|
||||||
|
declare const LOC_PROMOTE: string
|
||||||
|
@ -121,6 +121,7 @@ function register (clientOptions: RegisterClientOptions): void {
|
|||||||
titleChannelConfiguration.textContent = labels.channelConfiguration
|
titleChannelConfiguration.textContent = labels.channelConfiguration
|
||||||
titleLineEl.append(titleChannelConfiguration)
|
titleLineEl.append(titleChannelConfiguration)
|
||||||
}
|
}
|
||||||
|
titleLineEl.append(document.createElement('th'))
|
||||||
table.append(titleLineEl)
|
table.append(titleLineEl)
|
||||||
rooms.forEach(room => {
|
rooms.forEach(room => {
|
||||||
const localpart = room.localpart
|
const localpart = room.localpart
|
||||||
@ -137,6 +138,35 @@ function register (clientOptions: RegisterClientOptions): void {
|
|||||||
const date = new Date(room.lasttimestamp * 1000)
|
const date = new Date(room.lasttimestamp * 1000)
|
||||||
lastActivityEl.textContent = date.toLocaleDateString() + ' ' + date.toLocaleTimeString()
|
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 = `<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 4.233 4.233"
|
||||||
|
>
|
||||||
|
<g style="stroke-width:1.00021;stroke-miterlimit:4;stroke-dasharray:none">
|
||||||
|
<path
|
||||||
|
style="opacity:.998;fill:currentColor;fill-opacity:1;stroke:currentColor;stroke-width:1.17052;
|
||||||
|
stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M2.403.583V-.19a.94.94 0 0 1 .942-.943l1.962-.023 1.961.01a.94.94 0 0 1
|
||||||
|
.942.942v.772S6.586 4.9 5.307 4.92C4.027 4.939 2.403.583 2.403.583Z"
|
||||||
|
transform="matrix(.45208 0 0 .45208 -.526 1.335)"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>`
|
||||||
|
const promoteEl = document.createElement('td')
|
||||||
|
promoteEl.append(promoteButton)
|
||||||
|
|
||||||
const channelConfigurationEl = document.createElement('td')
|
const channelConfigurationEl = document.createElement('td')
|
||||||
nameEl.append(aEl)
|
nameEl.append(aEl)
|
||||||
lineEl.append(nameEl)
|
lineEl.append(nameEl)
|
||||||
@ -146,6 +176,7 @@ function register (clientOptions: RegisterClientOptions): void {
|
|||||||
if (useChannelConfiguration) {
|
if (useChannelConfiguration) {
|
||||||
lineEl.append(channelConfigurationEl) // else the element will just be dropped.
|
lineEl.append(channelConfigurationEl) // else the element will just be dropped.
|
||||||
}
|
}
|
||||||
|
lineEl.append(promoteEl)
|
||||||
table.append(lineEl)
|
table.append(lineEl)
|
||||||
|
|
||||||
const writeChannelConfigurationLink = (channelId: number | string): void => {
|
const writeChannelConfigurationLink = (channelId: number | string): void => {
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
import type { Video } from '@peertube/peertube-types'
|
import type { Video } from '@peertube/peertube-types'
|
||||||
import type { RegisterClientOptions } from '@peertube/peertube-types/client'
|
import type { RegisterClientOptions } from '@peertube/peertube-types/client'
|
||||||
|
import type { InitConverseJSParams } from 'shared/lib/types'
|
||||||
import { videoHasWebchat, videoHasRemoteWebchat } from 'shared/lib/video'
|
import { videoHasWebchat, videoHasRemoteWebchat } from 'shared/lib/video'
|
||||||
import { localizedHelpUrl } from './utils/help'
|
import { localizedHelpUrl } from './utils/help'
|
||||||
import { logger } from './utils/logger'
|
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 { displayButton, displayButtonOptions } from './videowatch/button'
|
||||||
import { shareChatUrl } from './videowatch/share'
|
import { shareChatUrl } from './videowatch/share'
|
||||||
import { displayConverseJS } from './utils/conversejs'
|
import { displayConverseJS } from './utils/conversejs'
|
||||||
|
import { getBaseRoute } from './utils/uri'
|
||||||
|
|
||||||
interface VideoWatchLoadedHookOptions {
|
interface VideoWatchLoadedHookOptions {
|
||||||
videojs: any
|
videojs: any
|
||||||
@ -71,7 +75,11 @@ function register (registerOptions: RegisterClientOptions): void {
|
|||||||
let settings: any = {} // will be loaded later
|
let settings: any = {} // will be loaded later
|
||||||
|
|
||||||
async function insertChatDom (
|
async function insertChatDom (
|
||||||
container: HTMLElement, video: Video, showOpenBlank: boolean, showShareUrlButton: boolean
|
container: HTMLElement,
|
||||||
|
video: Video,
|
||||||
|
showOpenBlank: boolean,
|
||||||
|
showShareUrlButton: boolean,
|
||||||
|
showPromote: boolean
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
logger.log('Adding livechat in the DOM...')
|
logger.log('Adding livechat in the DOM...')
|
||||||
const viewersDocumentationHelpUrl = await localizedHelpUrl(registerOptions, {
|
const viewersDocumentationHelpUrl = await localizedHelpUrl(registerOptions, {
|
||||||
@ -84,13 +92,15 @@ function register (registerOptions: RegisterClientOptions): void {
|
|||||||
peertubeHelpers.translate(LOC_OPEN_CHAT_NEW_WINDOW),
|
peertubeHelpers.translate(LOC_OPEN_CHAT_NEW_WINDOW),
|
||||||
peertubeHelpers.translate(LOC_CLOSE_CHAT),
|
peertubeHelpers.translate(LOC_CLOSE_CHAT),
|
||||||
peertubeHelpers.translate(LOC_SHARE_CHAT_LINK),
|
peertubeHelpers.translate(LOC_SHARE_CHAT_LINK),
|
||||||
peertubeHelpers.translate(LOC_ONLINE_HELP)
|
peertubeHelpers.translate(LOC_ONLINE_HELP),
|
||||||
|
peertubeHelpers.translate(LOC_PROMOTE)
|
||||||
]).then(labels => {
|
]).then(labels => {
|
||||||
const labelOpen = labels[0]
|
const labelOpen = labels[0]
|
||||||
const labelOpenBlank = labels[1]
|
const labelOpenBlank = labels[1]
|
||||||
const labelClose = labels[2]
|
const labelClose = labels[2]
|
||||||
const labelShareUrl = labels[3]
|
const labelShareUrl = labels[3]
|
||||||
const labelHelp = labels[4]
|
const labelHelp = labels[4]
|
||||||
|
const labelPromote = labels[5]
|
||||||
|
|
||||||
const buttonContainer = document.createElement('div')
|
const buttonContainer = document.createElement('div')
|
||||||
buttonContainer.classList.add('peertube-plugin-livechat-buttons')
|
buttonContainer.classList.add('peertube-plugin-livechat-buttons')
|
||||||
@ -133,6 +143,45 @@ function register (registerOptions: RegisterClientOptions): void {
|
|||||||
additionalClasses: []
|
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({
|
groupButtons.push({
|
||||||
buttonContainer,
|
buttonContainer,
|
||||||
name: 'help',
|
name: 'help',
|
||||||
@ -287,6 +336,7 @@ function register (registerOptions: RegisterClientOptions): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let showShareUrlButton: boolean = false
|
let showShareUrlButton: boolean = false
|
||||||
|
let showPromote: boolean = false
|
||||||
if (video.isLocal) { // No need for shareButton on remote chats.
|
if (video.isLocal) { // No need for shareButton on remote chats.
|
||||||
const chatShareUrl = settings['chat-share-url'] ?? ''
|
const chatShareUrl = settings['chat-share-url'] ?? ''
|
||||||
if (chatShareUrl === 'everyone') {
|
if (chatShareUrl === 'everyone') {
|
||||||
@ -296,9 +346,19 @@ function register (registerOptions: RegisterClientOptions): void {
|
|||||||
} else if (chatShareUrl === 'owner+moderators') {
|
} else if (chatShareUrl === 'owner+moderators') {
|
||||||
showShareUrlButton = guessIsMine(registerOptions, video) || guessIamIModerator(registerOptions)
|
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']) {
|
if (settings['chat-auto-display']) {
|
||||||
await openChat(video)
|
await openChat(video)
|
||||||
} else if (container) {
|
} else if (container) {
|
||||||
|
@ -10,7 +10,7 @@ interface displayButtonOptionsBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface displayButtonOptionsCallback extends displayButtonOptionsBase {
|
interface displayButtonOptionsCallback extends displayButtonOptionsBase {
|
||||||
callback: () => void | boolean
|
callback: () => void | boolean | Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface displayButtonOptionsHref extends displayButtonOptionsBase {
|
interface displayButtonOptionsHref extends displayButtonOptionsBase {
|
||||||
|
@ -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 `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 4.233 4.233">
|
||||||
|
<g style="stroke-width:1.00021;stroke-miterlimit:4;stroke-dasharray:none">
|
||||||
|
<path style="opacity:.998;fill:currentColor;fill-opacity:1;stroke:currentColor;stroke-width:1.17052;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" d="M2.403.583V-.19a.94.94 0 0 1 .942-.943l1.962-.023 1.961.01a.94.94 0 0 1 .942.942v.772S6.586 4.9 5.307 4.92C4.027 4.939 2.403.583 2.403.583Z" transform="matrix(.45208 0 0 .45208 -.526 1.335)"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
closeSVG,
|
closeSVG,
|
||||||
openChatSVG,
|
openChatSVG,
|
||||||
openBlankChatSVG,
|
openBlankChatSVG,
|
||||||
shareChatUrlSVG,
|
shareChatUrlSVG,
|
||||||
helpButtonSVG
|
helpButtonSVG,
|
||||||
|
promoteSVG
|
||||||
}
|
}
|
||||||
export type {
|
export type {
|
||||||
SVGButton
|
SVGButton
|
||||||
|
@ -452,3 +452,5 @@ task_list_pick_message: |
|
|||||||
Once you have chosen a task list, a new task will be created.
|
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.
|
To see the task, open the task application using the top menu.
|
||||||
More information in the livechat plugin documentation.
|
More information in the livechat plugin documentation.
|
||||||
|
|
||||||
|
promote: 'Become moderator'
|
||||||
|
@ -376,6 +376,8 @@ async function _localRoomJID (
|
|||||||
}
|
}
|
||||||
room = room.replace(/{{CHANNEL_ID}}/g, `${channelId}`)
|
room = room.replace(/{{CHANNEL_ID}}/g, `${channelId}`)
|
||||||
if (room.includes('{{CHANNEL_NAME}}')) {
|
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)
|
const channelName = await getChannelNameById(options, channelId)
|
||||||
if (channelName === null) {
|
if (channelName === null) {
|
||||||
throw new Error('Channel not found')
|
throw new Error('Channel not found')
|
||||||
|
@ -8,6 +8,7 @@ import { initRoomApiRouter } from './api/room'
|
|||||||
import { initAuthApiRouter, initUserAuthApiRouter } from './api/auth'
|
import { initAuthApiRouter, initUserAuthApiRouter } from './api/auth'
|
||||||
import { initFederationServerInfosApiRouter } from './api/federation-server-infos'
|
import { initFederationServerInfosApiRouter } from './api/federation-server-infos'
|
||||||
import { initConfigurationApiRouter } from './api/configuration'
|
import { initConfigurationApiRouter } from './api/configuration'
|
||||||
|
import { initPromoteApiRouter } from './api/promote'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initiate API routes
|
* Initiate API routes
|
||||||
@ -36,6 +37,7 @@ async function initApiRouter (options: RegisterServerOptions): Promise<Router> {
|
|||||||
await initFederationServerInfosApiRouter(options, router)
|
await initFederationServerInfosApiRouter(options, router)
|
||||||
|
|
||||||
await initConfigurationApiRouter(options, router)
|
await initConfigurationApiRouter(options, router)
|
||||||
|
await initPromoteApiRouter(options, router)
|
||||||
|
|
||||||
if (isDebugMode(options)) {
|
if (isDebugMode(options)) {
|
||||||
// Only add this route if the debug mode is enabled at time of the server launch.
|
// Only add this route if the debug mode is enabled at time of the server launch.
|
||||||
|
53
server/lib/routers/api/promote.ts
Normal file
53
server/lib/routers/api/promote.ts
Normal file
@ -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<void> {
|
||||||
|
const logger = options.peertubeHelpers.logger
|
||||||
|
|
||||||
|
router.put('/promote/:roomJID', asyncMiddleware(
|
||||||
|
async (req: Request, res: Response, _next: NextFunction): Promise<void> => {
|
||||||
|
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
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user