Compare commits
14 Commits
v10.3.2
...
cb52a55895
Author | SHA1 | Date | |
---|---|---|---|
cb52a55895 | |||
4148444e91 | |||
de14b95f9a | |||
4f80119c83 | |||
80b2093202 | |||
3d4afc4341 | |||
49a87237ec | |||
0737e14472 | |||
226ea38e4d | |||
559fe731e0 | |||
e8eb56d0b7 | |||
1b97366cd8 | |||
1f3eee9889 | |||
772c1c1d14 |
43
CHANGELOG.md
43
CHANGELOG.md
@ -1,42 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## 10.3.2
|
||||
|
||||
### Minor changes and fixes
|
||||
|
||||
* Fix #477: ended polls never disappear when archiving is disabled (and no more than 20 new messages).
|
||||
|
||||
## 10.3.1
|
||||
|
||||
### Minor changes and fixes
|
||||
|
||||
* Moderation delay: fix accessibility on the timer shown to moderators.
|
||||
* Fix «create new poll» icon.
|
||||
|
||||
## 10.3.0
|
||||
|
||||
### New features
|
||||
|
||||
* #132: [moderation delay](https://livingston.frama.io/peertube-plugin-livechat/documentation/user/streamers/moderation_delay/).
|
||||
|
||||
### Minor changes and fixes
|
||||
|
||||
* Translations updates: german.
|
||||
* Performance: don't send markers, even if requested by the sender.
|
||||
|
||||
## 10.2.0
|
||||
|
||||
### New features
|
||||
|
||||
* #231: [polls](https://livingston.frama.io/peertube-plugin-livechat/documentation/user/streamers/polls/).
|
||||
* #233: new option to [mute anonymous users](https://livingston.frama.io/peertube-plugin-livechat/documentation/user/streamers/moderation/).
|
||||
* #18: terms & conditions. You can configure terms&conditions on your instance that will be shown to each joining users. Streamers can also add [terms&conditions in their channels options](https://livingston.frama.io/peertube-plugin-livechat/documentation/user/streamers/terms/).
|
||||
|
||||
### Minor changes and fixes
|
||||
|
||||
* Fix #449: Remove the constraint for custom emojis shortnames to have ":" at the beginning and at the end.
|
||||
* Translations updates: french, german, crotian, polish, slovak.
|
||||
|
||||
## 10.1.2
|
||||
|
||||
* Fix: clicking on the import custom emojis button, without selected any file, was resulting in a state with all action button disabled.
|
||||
@ -82,7 +45,7 @@
|
||||
|
||||
### 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/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: 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.
|
||||
|
||||
@ -773,7 +736,7 @@ Moreover, they don't seem to be used much.
|
||||
### Features
|
||||
|
||||
* Builtin prosody use a working dir provided by Peertube (needs Peertube >= 3.2.0)
|
||||
* Starting with Peertube 3.2.0, builtin prosody save room history on server. So when a user connects, they can get previously send messages.
|
||||
* Starting with Peertube 3.2.0, builtin prosody save room history on server. So when a user connects, he can get previously send messages.
|
||||
* Starting with Peertube 3.2.0, builtin prosody also activate mod_muc_moderation, enabling moderators to moderate messages.
|
||||
* Prosody log level will be the same as the Peertube's one.
|
||||
* Prosody log rotation every 24 hour.
|
||||
@ -810,7 +773,7 @@ Moreover, they don't seem to be used much.
|
||||
## v2.1.3
|
||||
|
||||
* Fix: 2.1.0 was in fact correct... Did not work on my preprod env because of... a Livebox bug...
|
||||
* Fix: if the video owner is already owner of the chatroom, they should not be downgraded to admin.
|
||||
* Fix: if the video owner is already owner of the chatroom, he should not be downgraded to admin.
|
||||
|
||||
## v2.1.2
|
||||
|
||||
|
@ -31,3 +31,40 @@
|
||||
min-height: max(30vh, 200px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Media query for mobile devices */
|
||||
@media only screen and (max-width: 50vw) {
|
||||
#peertube-plugin-livechat-container converse-root {
|
||||
|
||||
converse-muc {
|
||||
min-height: 62vh;
|
||||
/* 100vh - 30vh for video = 70vh remaining */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Media query for tablets in portrait mode */
|
||||
@media only screen and (min-width: 50vw) and (max-width: 75vw) {
|
||||
#peertube-plugin-livechat-container converse-root {
|
||||
converse-muc {
|
||||
min-height: 62vh;
|
||||
/* Slightly less to account for other elements */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Media query for tablets in landscape mode */
|
||||
@media only screen and (min-width: 76vw) and (max-width: 100vw) {
|
||||
#peertube-plugin-livechat-container converse-root {
|
||||
converse-muc {
|
||||
min-height: 62vh;
|
||||
/* Assuming more height can be used */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* custom toolbar CSS */
|
||||
|
||||
.send-button {
|
||||
border-radius: 0.25rem !important;
|
||||
}
|
@ -171,7 +171,7 @@ async function generateAvatars (part) {
|
||||
|
||||
async function generateBotsAvatars () {
|
||||
{
|
||||
// Moderation bot avatar: choosing some parts, and turning it so it is facing left.
|
||||
// Moderation bot avatar: choosing some parts, and turning it so he is facing left.
|
||||
const inputDir = path.join('./assets/images/avatars/', 'sepia')
|
||||
const botOutputDir = './dist/server/bot_avatars/sepia/'
|
||||
fs.mkdirSync(botOutputDir, { recursive: true })
|
||||
@ -196,7 +196,7 @@ async function generateBotsAvatars () {
|
||||
}
|
||||
|
||||
{
|
||||
// Moderation bot avatar: choosing some parts, and turning it so it is facing left.
|
||||
// Moderation bot avatar: choosing some parts, and turning it so he is facing left.
|
||||
const inputDir = path.join('./assets/images/avatars/', 'cat')
|
||||
const botOutputDir = './dist/server/bot_avatars/cat/'
|
||||
fs.mkdirSync(botOutputDir, { recursive: true })
|
||||
@ -220,7 +220,7 @@ async function generateBotsAvatars () {
|
||||
}
|
||||
|
||||
{
|
||||
// Moderation bot avatar: choosing some parts, and turning it so it is facing left.
|
||||
// Moderation bot avatar: choosing some parts, and turning it so he is facing left.
|
||||
const inputDir = path.join('./assets/images/avatars/', 'bird')
|
||||
const botOutputDir = './dist/server/bot_avatars/bird/'
|
||||
fs.mkdirSync(botOutputDir, { recursive: true })
|
||||
@ -246,7 +246,7 @@ async function generateBotsAvatars () {
|
||||
}
|
||||
|
||||
{
|
||||
// Moderation bot avatar: choosing some parts, and turning it so it is facing left.
|
||||
// Moderation bot avatar: choosing some parts, and turning it so he is facing left.
|
||||
const inputDir = './assets/images/avatars/fenec'
|
||||
const botOutputDir = './dist/server/bot_avatars/fenec/'
|
||||
fs.mkdirSync(botOutputDir, { recursive: true })
|
||||
@ -273,7 +273,7 @@ async function generateBotsAvatars () {
|
||||
}
|
||||
|
||||
{
|
||||
// Moderation bot avatar: choosing some parts, and turning it so it is facing left.
|
||||
// Moderation bot avatar: choosing some parts, and turning it so he is facing left.
|
||||
const inputDir = './assets/images/avatars/abstract'
|
||||
const botOutputDir = './dist/server/bot_avatars/abstract/'
|
||||
fs.mkdirSync(botOutputDir, { recursive: true })
|
||||
|
10
client/@types/global.d.ts
vendored
10
client/@types/global.d.ts
vendored
@ -81,10 +81,6 @@ declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_QUOTE_DELAY_DESC: string
|
||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_BANNED_JIDS_LABEL: string
|
||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_BOT_NICKNAME: string
|
||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FOR_MORE_INFO: string
|
||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_MUTE_ANONYMOUS_LABEL: string
|
||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_MUTE_ANONYMOUS_DESC: string
|
||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_TERMS_LABEL: string
|
||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_TERMS_DESC: string
|
||||
|
||||
declare const LOC_VALIDATION_ERROR: string
|
||||
declare const LOC_TOO_MANY_ENTRIES: string
|
||||
@ -95,7 +91,6 @@ declare const LOC_INVALID_VALUE_WRONG_FORMAT: string
|
||||
declare const LOC_INVALID_VALUE_NOT_IN_RANGE: string
|
||||
declare const LOC_INVALID_VALUE_FILE_TOO_BIG: string
|
||||
declare const LOC_INVALID_VALUE_DUPLICATE: string
|
||||
declare const LOC_INVALID_VALUE_TOO_LONG: string
|
||||
|
||||
declare const LOC_CHATROOM_NOT_ACCESSIBLE: string
|
||||
|
||||
@ -128,8 +123,3 @@ declare const LOC_TOKEN_ACTION_CREATE: string
|
||||
declare const LOC_TOKEN_ACTION_REVOKE: string
|
||||
declare const LOC_TOKEN_DEFAULT_LABEL: string
|
||||
declare const LOC_TOKEN_ACTION_REVOKE_CONFIRM: string
|
||||
|
||||
declare const LOC_POLL_VOTE_OK: string
|
||||
|
||||
declare const LOC_MODERATION_DELAY: string
|
||||
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_MODERATION_DELAY_DESC: string
|
||||
|
@ -14,7 +14,6 @@ import { customElement, property, state } from 'lit/decorators.js'
|
||||
import { ptTr } from '../../lib/directives/translation'
|
||||
import { Task } from '@lit/task'
|
||||
import { provide } from '@lit/context'
|
||||
import { channelTermsMaxLength } from 'shared/lib/constants'
|
||||
|
||||
@customElement('livechat-channel-configuration')
|
||||
export class ChannelConfigurationElement extends LivechatElement {
|
||||
@ -52,10 +51,6 @@ export class ChannelConfigurationElement extends LivechatElement {
|
||||
})
|
||||
}
|
||||
|
||||
public termsMaxLength (): number {
|
||||
return channelTermsMaxLength
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the form by reloading data from backend.
|
||||
*/
|
||||
@ -125,11 +120,7 @@ export class ChannelConfigurationElement extends LivechatElement {
|
||||
const validationErrorTypes: ValidationErrorType[] | undefined =
|
||||
this.validationError?.properties[`${propertyName}`] ?? undefined
|
||||
|
||||
// FIXME: this code is duplicated in dymamic table form
|
||||
if (validationErrorTypes && validationErrorTypes.length !== 0) {
|
||||
if (validationErrorTypes.includes(ValidationErrorType.Missing)) {
|
||||
errorMessages.push(html`${ptTr(LOC_INVALID_VALUE_MISSING)}`)
|
||||
}
|
||||
if (validationErrorTypes.includes(ValidationErrorType.WrongType)) {
|
||||
errorMessages.push(html`${ptTr(LOC_INVALID_VALUE_WRONG_TYPE)}`)
|
||||
}
|
||||
@ -139,9 +130,6 @@ export class ChannelConfigurationElement extends LivechatElement {
|
||||
if (validationErrorTypes.includes(ValidationErrorType.NotInRange)) {
|
||||
errorMessages.push(html`${ptTr(LOC_INVALID_VALUE_NOT_IN_RANGE)}`)
|
||||
}
|
||||
if (validationErrorTypes.includes(ValidationErrorType.TooLong)) {
|
||||
errorMessages.push(html`${ptTr(LOC_INVALID_VALUE_TOO_LONG)}`)
|
||||
}
|
||||
|
||||
return html`<div id=${feedbackId} class="invalid-feedback">${errorMessages}</div>`
|
||||
} else {
|
||||
|
@ -193,7 +193,9 @@ export class ChannelEmojisElement extends LivechatElement {
|
||||
}
|
||||
|
||||
const url = await this._convertImageToDataUrl(entry.url)
|
||||
const sn = entry.sn as string
|
||||
let sn = entry.sn as string
|
||||
if (!sn.startsWith(':')) { sn = ':' + sn }
|
||||
if (!sn.endsWith(':')) { sn += ':' }
|
||||
|
||||
const item: ChannelEmojisConfiguration['emojis']['customEmojis'][0] = {
|
||||
sn,
|
||||
|
@ -127,63 +127,6 @@ export function tplChannelConfiguration (el: ChannelConfigurationElement): Templ
|
||||
</p>
|
||||
|
||||
<form livechat-configuration-channel-options role="form" @submit=${el.saveConfig} @change=${el.resetValidation}>
|
||||
|
||||
<livechat-configuration-section-header
|
||||
.label=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_TERMS_LABEL)}
|
||||
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_TERMS_DESC, true)}
|
||||
.helpPage=${'documentation/user/streamers/terms'}>
|
||||
</livechat-configuration-section-header>
|
||||
<div class="form-group">
|
||||
<textarea
|
||||
name="terms"
|
||||
id="peertube-livechat-terms"
|
||||
.value=${el.channelConfiguration?.configuration.terms ?? ''}
|
||||
maxlength=${el.termsMaxLength()}
|
||||
class=${classMap(
|
||||
Object.assign(
|
||||
{ 'form-control': true },
|
||||
el.getInputValidationClass('terms')
|
||||
)
|
||||
)}
|
||||
@change=${(event: Event) => {
|
||||
if (event?.target && el.channelConfiguration) {
|
||||
let value: string | undefined = (event.target as HTMLTextAreaElement).value
|
||||
if (value === '') { value = undefined }
|
||||
el.channelConfiguration.configuration.terms = value
|
||||
}
|
||||
el.requestUpdate('channelConfiguration')
|
||||
}
|
||||
}
|
||||
></textarea>
|
||||
${el.renderFeedback('peertube-livechat-terms-feedback', 'terms')}
|
||||
</div>
|
||||
|
||||
<livechat-configuration-section-header
|
||||
.label=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_MUTE_ANONYMOUS_LABEL)}
|
||||
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_MUTE_ANONYMOUS_DESC, true)}
|
||||
.helpPage=${'documentation/user/streamers/moderation'}>
|
||||
</livechat-configuration-section-header>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="bot"
|
||||
id="peertube-livechat-mute-anonymous"
|
||||
@input=${(event: InputEvent) => {
|
||||
if (event?.target && el.channelConfiguration) {
|
||||
el.channelConfiguration.configuration.mute.anonymous =
|
||||
(event.target as HTMLInputElement).checked
|
||||
}
|
||||
el.requestUpdate('channelConfiguration')
|
||||
}
|
||||
}
|
||||
value="1"
|
||||
?checked=${el.channelConfiguration?.configuration.mute.anonymous}
|
||||
/>
|
||||
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_MUTE_ANONYMOUS_LABEL)}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<livechat-configuration-section-header
|
||||
.label=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_SLOW_MODE_LABEL)}
|
||||
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_SLOW_MODE_DESC, true)}
|
||||
@ -219,41 +162,6 @@ export function tplChannelConfiguration (el: ChannelConfigurationElement): Templ
|
||||
${el.renderFeedback('peertube-livechat-slowmode-duration-feedback', 'slowMode.duration')}
|
||||
</div>
|
||||
|
||||
<livechat-configuration-section-header
|
||||
.label=${ptTr(LOC_MODERATION_DELAY)}
|
||||
.description=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_MODERATION_DELAY_DESC, true)}
|
||||
.helpPage=${'documentation/user/streamers/moderation_delay'}>
|
||||
</livechat-configuration-section-header>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
${ptTr(LOC_MODERATION_DELAY)}
|
||||
<input
|
||||
type="number"
|
||||
name="moderation_delay"
|
||||
class=${classMap(
|
||||
Object.assign(
|
||||
{ 'form-control': true },
|
||||
el.getInputValidationClass('moderation.delay')
|
||||
)
|
||||
)}
|
||||
min="0"
|
||||
max="60"
|
||||
id="peertube-livechat-moderation-delay"
|
||||
aria-describedby="peertube-livechat-moderation-delay-feedback"
|
||||
@input=${(event: InputEvent) => {
|
||||
if (event?.target && el.channelConfiguration) {
|
||||
el.channelConfiguration.configuration.moderation.delay =
|
||||
Number((event.target as HTMLInputElement).value)
|
||||
}
|
||||
el.requestUpdate('channelConfiguration')
|
||||
}
|
||||
}
|
||||
value="${el.channelConfiguration?.configuration.moderation.delay ?? ''}"
|
||||
/>
|
||||
</label>
|
||||
${el.renderFeedback('peertube-livechat-moderation-delay-feedback', 'moderation.delay')}
|
||||
</div>
|
||||
|
||||
<livechat-configuration-section-header
|
||||
.label=${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_BOT_OPTIONS_TITLE)}
|
||||
.description=${''}
|
||||
|
@ -91,6 +91,12 @@ export function tplChannelEmojis (el: ChannelEmojisElement): TemplateResult {
|
||||
el.resetValidation(e)
|
||||
if (el.channelEmojisConfiguration) {
|
||||
el.channelEmojisConfiguration.emojis.customEmojis = e.detail
|
||||
// Fixing missing ':' for shortnames:
|
||||
for (const desc of el.channelEmojisConfiguration.emojis.customEmojis) {
|
||||
if (desc.sn === '') { continue }
|
||||
if (!desc.sn.startsWith(':')) { desc.sn = ':' + desc.sn }
|
||||
if (!desc.sn.endsWith(':')) { desc.sn += ':' }
|
||||
}
|
||||
el.requestUpdate('channelEmojisConfiguration')
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,6 @@ import type {
|
||||
import { ValidationError, ValidationErrorType } from '../../lib/models/validation'
|
||||
import { getBaseRoute } from '../../../utils/uri'
|
||||
import { maxEmojisPerChannel } from 'shared/lib/emojis'
|
||||
import { channelTermsMaxLength } from 'shared/lib/constants'
|
||||
|
||||
export class ChannelDetailsService {
|
||||
public _registerClientOptions: RegisterClientOptions
|
||||
@ -28,16 +27,10 @@ export class ChannelDetailsService {
|
||||
validateOptions = async (channelConfigurationOptions: ChannelConfigurationOptions): Promise<boolean> => {
|
||||
const propertiesError: ValidationError['properties'] = {}
|
||||
|
||||
if (channelConfigurationOptions.terms && channelConfigurationOptions.terms.length > channelTermsMaxLength) {
|
||||
propertiesError.terms = [ValidationErrorType.TooLong]
|
||||
}
|
||||
|
||||
const botConf = channelConfigurationOptions.bot
|
||||
const slowModeDuration = channelConfigurationOptions.slowMode.duration
|
||||
const moderationDelay = channelConfigurationOptions.moderation.delay
|
||||
|
||||
propertiesError['slowMode.duration'] = []
|
||||
propertiesError['moderation.delay'] = []
|
||||
|
||||
if (
|
||||
(typeof slowModeDuration !== 'number') ||
|
||||
@ -51,18 +44,6 @@ export class ChannelDetailsService {
|
||||
propertiesError['slowMode.duration'].push(ValidationErrorType.NotInRange)
|
||||
}
|
||||
|
||||
if (
|
||||
(typeof moderationDelay !== 'number') ||
|
||||
isNaN(moderationDelay)
|
||||
) {
|
||||
propertiesError['moderation.delay'].push(ValidationErrorType.WrongType)
|
||||
} else if (
|
||||
moderationDelay < 0 ||
|
||||
moderationDelay > 60
|
||||
) {
|
||||
propertiesError['moderation.delay'].push(ValidationErrorType.NotInRange)
|
||||
}
|
||||
|
||||
// If !bot.enabled, we don't have to validate these fields:
|
||||
// The backend will ignore those values.
|
||||
if (botConf.enabled) {
|
||||
@ -215,7 +196,7 @@ export class ChannelDetailsService {
|
||||
propertiesError[`emojis.${i}.sn`] = []
|
||||
if (e.sn === '') {
|
||||
propertiesError[`emojis.${i}.sn`].push(ValidationErrorType.Missing)
|
||||
} else if (!/^:?[\w-]+:?$/.test(e.sn)) { // optional ':' at the beggining and at the end
|
||||
} else if (!/^:[\w-]+:$/.test(e.sn)) {
|
||||
propertiesError[`emojis.${i}.sn`].push(ValidationErrorType.WrongFormat)
|
||||
} else if (seen.has(e.sn)) {
|
||||
propertiesError[`emojis.${i}.sn`].push(ValidationErrorType.Duplicate)
|
||||
|
@ -672,7 +672,6 @@ export class DynamicTableFormElement extends LivechatElement {
|
||||
const validationErrorTypes: ValidationErrorType[] | undefined =
|
||||
this.validation?.[`${this.validationPrefix}.${originalIndex}.${propertyName}`]
|
||||
|
||||
// FIXME: this code is duplicated in channel-configuration
|
||||
if (validationErrorTypes !== undefined && validationErrorTypes.length !== 0) {
|
||||
if (validationErrorTypes.includes(ValidationErrorType.Missing)) {
|
||||
errorMessages.push(html`${ptTr(LOC_INVALID_VALUE_MISSING)}`)
|
||||
@ -689,9 +688,6 @@ export class DynamicTableFormElement extends LivechatElement {
|
||||
if (validationErrorTypes.includes(ValidationErrorType.Duplicate)) {
|
||||
errorMessages.push(html`${ptTr(LOC_INVALID_VALUE_DUPLICATE)}`)
|
||||
}
|
||||
if (validationErrorTypes.includes(ValidationErrorType.TooLong)) {
|
||||
errorMessages.push(html`${ptTr(LOC_INVALID_VALUE_TOO_LONG)}`)
|
||||
}
|
||||
|
||||
return html`<div id="${inputId}-feedback" class="invalid-feedback">${errorMessages}</div>`
|
||||
} else {
|
||||
|
@ -7,8 +7,7 @@ export enum ValidationErrorType {
|
||||
WrongType,
|
||||
WrongFormat,
|
||||
NotInRange,
|
||||
Duplicate,
|
||||
TooLong
|
||||
Duplicate
|
||||
}
|
||||
|
||||
export class ValidationError extends Error {
|
||||
|
@ -17,8 +17,6 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
let pollListenerInitiliazed: boolean = false
|
||||
|
||||
/**
|
||||
* load the ConverseJS CSS.
|
||||
* @param url CSS url
|
||||
@ -166,15 +164,6 @@ async function displayConverseJS (
|
||||
}
|
||||
const converseJSParams: InitConverseJSParams = await (response).json()
|
||||
|
||||
if (!pollListenerInitiliazed) {
|
||||
// First time we got here, initiliaze this event:
|
||||
const i18nVoteOk = await clientOptions.peertubeHelpers.translate(LOC_POLL_VOTE_OK)
|
||||
pollListenerInitiliazed = true
|
||||
document.addEventListener('livechat-poll-vote', () => {
|
||||
clientOptions.peertubeHelpers.notifier.success(i18nVoteOk)
|
||||
})
|
||||
}
|
||||
|
||||
await loadConverseJS(converseJSParams)
|
||||
await window.initConverse(converseJSParams, chatIncludeMode, authHeader ?? null)
|
||||
}
|
||||
|
@ -20,7 +20,6 @@ import { livechatSpecificsPlugin } from './lib/plugins/livechat-specific'
|
||||
import { livechatViewerModePlugin } from './lib/plugins/livechat-viewer-mode'
|
||||
import { livechatMiniMucHeadPlugin } from './lib/plugins/livechat-mini-muc-head'
|
||||
import { livechatEmojisPlugin } from './lib/plugins/livechat-emojis'
|
||||
import { moderationDelayPlugin } from './lib/plugins/moderation-delay'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@ -31,10 +30,6 @@ declare global {
|
||||
}
|
||||
emojis: any
|
||||
livechatDisconnect?: Function
|
||||
env: {
|
||||
html: Function
|
||||
sizzle: Function
|
||||
}
|
||||
}
|
||||
initConversePlugins: typeof initConversePlugins
|
||||
initConverse: typeof initConverse
|
||||
@ -71,8 +66,6 @@ function initConversePlugins (peertubeEmbedded: boolean): void {
|
||||
|
||||
// Viewer mode (anonymous accounts, before they have chosen their nickname).
|
||||
converse.plugins.add('livechatViewerModePlugin', livechatViewerModePlugin)
|
||||
|
||||
converse.plugins.add('converse-moderation-delay', moderationDelayPlugin)
|
||||
}
|
||||
window.initConversePlugins = initConversePlugins
|
||||
|
||||
@ -192,7 +185,6 @@ async function initConverse (
|
||||
params.livechat_enable_viewer_mode = autoViewerMode && !isAuthenticated && !isRemoteWithNicknameSet
|
||||
|
||||
params.livechat_specific_external_authent = isAuthenticatedWithExternalAccount
|
||||
params.livechat_specific_is_anonymous = !isAuthenticated
|
||||
|
||||
if (tryOIDC && !isAuthenticated) {
|
||||
params.livechat_external_auth_oidc_buttons = initConverseParams.externalAuthOIDC
|
||||
|
@ -46,20 +46,12 @@ import './plugins/fullscreen/index.js'
|
||||
|
||||
import '../custom/plugins/size/index.js'
|
||||
import '../custom/plugins/tasks/index.js'
|
||||
import '../custom/plugins/terms/index.js'
|
||||
import '../custom/plugins/poll/index.js'
|
||||
/* END: Removable components */
|
||||
|
||||
import { CORE_PLUGINS } from './headless/shared/constants.js'
|
||||
import { ROOM_FEATURES } from './headless/plugins/muc/constants.js'
|
||||
// We must add our custom plugins to CORE_PLUGINS (so it is white listed):
|
||||
import { CORE_PLUGINS } from './headless/shared/constants.js'
|
||||
CORE_PLUGINS.push('livechat-converse-size')
|
||||
CORE_PLUGINS.push('livechat-converse-tasks')
|
||||
CORE_PLUGINS.push('livechat-converse-terms')
|
||||
CORE_PLUGINS.push('livechat-converse-poll')
|
||||
// We must also add our custom ROOM_FEATURES, so that they correctly resets
|
||||
// (see headless/plugins/muc, getDiscoInfoFeatures, which loops on this const)
|
||||
ROOM_FEATURES.push('x_peertubelivechat_mute_anonymous')
|
||||
|
||||
_converse.CustomElement = CustomElement
|
||||
|
||||
|
@ -1,136 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import { XMLNS_POLL } from '../constants.js'
|
||||
import { tplPollForm } from '../templates/poll-form.js'
|
||||
import { CustomElement } from 'shared/components/element.js'
|
||||
import { converse, api } from '@converse/headless/core'
|
||||
import { webForm2xForm } from '@converse/headless/utils/form'
|
||||
import { __ } from 'i18n'
|
||||
import '../styles/poll-form.scss'
|
||||
const $iq = converse.env.$iq
|
||||
const u = converse.env.utils
|
||||
const sizzle = converse.env.sizzle
|
||||
const Strophe = converse.env.Strophe
|
||||
|
||||
export default class MUCPollFormView extends CustomElement {
|
||||
static get properties () {
|
||||
return {
|
||||
model: { type: Object, attribute: true },
|
||||
modal: { type: Object, attribute: true },
|
||||
form_fields: { type: Object, attribute: false },
|
||||
alert_message: { type: Object, attribute: false },
|
||||
title: { type: String, attribute: false },
|
||||
instructions: { type: String, attribute: false }
|
||||
}
|
||||
}
|
||||
|
||||
_fieldTranslationMap = new Map()
|
||||
|
||||
async initialize () {
|
||||
this.alert_message = undefined
|
||||
if (!this.model) {
|
||||
this.alert_message = __('Error')
|
||||
return
|
||||
}
|
||||
try {
|
||||
this._initFieldTranslations()
|
||||
const stanza = await this._fetchPollForm()
|
||||
const query = stanza.querySelector('query')
|
||||
const xform = sizzle(`x[xmlns="${Strophe.NS.XFORM}"]`, query)[0]
|
||||
if (!xform) {
|
||||
throw Error('Missing xform in stanza')
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
this.title = __(LOC_poll_title) // xform.querySelector('title')?.textContent ?? ''
|
||||
// eslint-disable-next-line no-undef
|
||||
this.instructions = __(LOC_poll_instructions) // xform.querySelector('instructions')?.textContent ?? ''
|
||||
this.form_fields = Array.from(xform.querySelectorAll('field')).map(field => {
|
||||
this._translateField(field)
|
||||
return u.xForm2TemplateResult(field, stanza)
|
||||
})
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
this.alert_message = __('Error')
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
return tplPollForm(this)
|
||||
}
|
||||
|
||||
_fetchPollForm () {
|
||||
return api.sendIQ(
|
||||
$iq({
|
||||
to: this.model.get('jid'),
|
||||
type: 'get'
|
||||
}).c('query', { xmlns: XMLNS_POLL })
|
||||
)
|
||||
}
|
||||
|
||||
_initFieldTranslations () {
|
||||
// eslint-disable-next-line no-undef
|
||||
this._fieldTranslationMap.set('muc#roompoll_question', __(LOC_poll_question))
|
||||
// eslint-disable-next-line no-undef
|
||||
this._fieldTranslationMap.set('muc#roompoll_duration', __(LOC_poll_duration))
|
||||
// eslint-disable-next-line no-undef
|
||||
this._fieldTranslationMap.set('muc#roompoll_anonymous_results', __(LOC_poll_anonymous_results))
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
this._fieldTranslationMap.set(
|
||||
'muc#roompoll_choice' + i.toString(),
|
||||
// eslint-disable-next-line no-undef
|
||||
__(LOC_poll_choice_n).replace('{{N}}', i.toString())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
_translateField (field) {
|
||||
const v = field.getAttribute('var')
|
||||
const label = this._fieldTranslationMap.get(v)
|
||||
if (label) {
|
||||
field.setAttribute('label', label)
|
||||
}
|
||||
}
|
||||
|
||||
async formSubmit (ev) {
|
||||
ev.preventDefault()
|
||||
try {
|
||||
this.alert_message = undefined
|
||||
const form = ev.target
|
||||
const inputs = sizzle(':input:not([type=button]):not([type=submit])', form)
|
||||
|
||||
const iq = $iq({
|
||||
type: 'set',
|
||||
to: this.model.get('jid'),
|
||||
id: u.getUniqueId()
|
||||
}).c('query', { xmlns: XMLNS_POLL })
|
||||
|
||||
iq.c('x', { xmlns: Strophe.NS.XFORM, type: 'submit' })
|
||||
|
||||
const xmlNodes = inputs.map(i => webForm2xForm(i)).filter(n => n)
|
||||
xmlNodes.forEach(n => iq.cnode(n).up())
|
||||
|
||||
await api.sendIQ(iq)
|
||||
|
||||
if (this.modal) {
|
||||
this.modal.onHide()
|
||||
}
|
||||
} catch (err) {
|
||||
if (u.isErrorStanza(err)) {
|
||||
// Checking if there is a text error that we can show to the user.
|
||||
if (sizzle('error bad-request', err).length) {
|
||||
const text = sizzle('error text', err)
|
||||
if (text.length) {
|
||||
this.alert_message = __('Error') + ': ' + text[0].textContent
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
console.error(err)
|
||||
this.alert_message = __('Error')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
api.elements.define('livechat-converse-poll-form', MUCPollFormView)
|
@ -1,82 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { tplPoll } from '../templates/poll.js'
|
||||
import { CustomElement } from 'shared/components/element.js'
|
||||
import { converse, _converse, api } from '@converse/headless/core'
|
||||
import '../styles/poll.scss'
|
||||
|
||||
export default class MUCPollView extends CustomElement {
|
||||
static get properties () {
|
||||
return {
|
||||
model: { type: Object, attribute: true },
|
||||
collapsed: { type: Boolean, attribute: false },
|
||||
buttonDisabled: { type: Boolean, attribute: false }
|
||||
}
|
||||
}
|
||||
|
||||
async initialize () {
|
||||
this.collapsed = false
|
||||
this.buttonDisabled = false
|
||||
if (!this.model) {
|
||||
return
|
||||
}
|
||||
this.listenTo(this.model, 'change:current_poll', () => {
|
||||
this.buttonDisabled = false
|
||||
this.requestUpdate()
|
||||
})
|
||||
this.listenTo(this.model.occupants, 'change:role', occupant => {
|
||||
if (occupant.get('jid') !== _converse.bare_jid) { // only for myself
|
||||
return
|
||||
}
|
||||
// visitors cant vote. So we must refresh the polls results when current occupant role changes.
|
||||
this.requestUpdate()
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
const currentPoll = this.model?.get('current_poll')
|
||||
const entered = this.model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED
|
||||
const canVote = entered && this.model.getOwnRole() !== 'visitor'
|
||||
return tplPoll(this, currentPoll, canVote)
|
||||
}
|
||||
|
||||
toggle (ev) {
|
||||
ev.preventDefault()
|
||||
this.collapsed = !this.collapsed
|
||||
}
|
||||
|
||||
async voteFor (choice) {
|
||||
if (this.buttonDisabled) { return }
|
||||
|
||||
const currentPoll = this.model?.get('current_poll')
|
||||
if (!currentPoll) { return }
|
||||
if (currentPoll.over) { return }
|
||||
|
||||
console.info('User has voted for choice: ', choice)
|
||||
// We disable vote buttons until next refresh:
|
||||
this.buttonDisabled = true
|
||||
this.requestUpdate()
|
||||
|
||||
await this.model.sendMessage({
|
||||
body: '!' + choice.choice
|
||||
})
|
||||
|
||||
// Dispatching an event.
|
||||
// When in Peertube interface, this will open a Peertube notifier with a message.
|
||||
// FIXME: we should only trigger this on the message echo or bounce,
|
||||
// but seems ConverseJs does not provide any promise for that.
|
||||
const event = new Event('livechat-poll-vote', {
|
||||
bubbles: true
|
||||
})
|
||||
this.dispatchEvent(event)
|
||||
}
|
||||
|
||||
closePoll (ev) {
|
||||
ev.preventDefault()
|
||||
this.model.set('current_poll', undefined)
|
||||
}
|
||||
}
|
||||
|
||||
api.elements.define('livechat-converse-muc-poll', MUCPollView)
|
@ -1,9 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
export const XMLNS_POLL = 'http://jabber.org/protocol/muc#x-poll'
|
||||
export const XMLNS_POLL_MESSAGE = 'http://jabber.org/protocol/muc#x-poll-message'
|
||||
export const POLL_MESSAGE_TAG = 'x-poll'
|
||||
export const POLL_QUESTION_TAG = 'x-poll-question'
|
||||
export const POLL_CHOICE_TAG = 'x-poll-choice'
|
@ -1,129 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { _converse, converse } from '../../../src/headless/core.js'
|
||||
import { getHeadingButtons } from './utils.js'
|
||||
import { POLL_MESSAGE_TAG, POLL_QUESTION_TAG, POLL_CHOICE_TAG } from './constants.js'
|
||||
import { __ } from 'i18n'
|
||||
import './modals/poll-form.js'
|
||||
import './components/poll-view.js'
|
||||
import './components/poll-form-view.js'
|
||||
|
||||
const { sizzle } = converse.env
|
||||
|
||||
const delayedTimeout = 2 // for delayed poll message, how long must the be considered as valid.
|
||||
|
||||
converse.plugins.add('livechat-converse-poll', {
|
||||
dependencies: ['converse-muc', 'converse-disco'],
|
||||
|
||||
initialize () {
|
||||
// adding the poll actions in the MUC heading buttons:
|
||||
_converse.api.listen.on('getHeadingButtons', getHeadingButtons)
|
||||
|
||||
_converse.api.listen.on('parseMUCMessage', (stanza, attrs) => {
|
||||
// Localizing specific error messages
|
||||
if (attrs.is_error) {
|
||||
// eslint-disable-next-line no-undef, camelcase
|
||||
if (attrs.error_text === LOC_poll_is_over) {
|
||||
// eslint-disable-next-line no-undef
|
||||
attrs.error_text = __(LOC_poll_is_over)
|
||||
// eslint-disable-next-line no-undef, camelcase
|
||||
} else if (attrs.error_text === LOC_poll_choice_invalid) {
|
||||
// eslint-disable-next-line no-undef
|
||||
attrs.error_text = __(LOC_poll_choice_invalid)
|
||||
// eslint-disable-next-line no-undef, camelcase
|
||||
} else if (attrs.error_text === LOC_poll_anonymous_vote_ok) {
|
||||
// eslint-disable-next-line no-undef
|
||||
attrs.error_text = __(LOC_poll_anonymous_vote_ok)
|
||||
}
|
||||
}
|
||||
|
||||
// Checking if there is any poll data in the message.
|
||||
const poll = sizzle(POLL_MESSAGE_TAG, stanza)?.[0]
|
||||
if (!poll) {
|
||||
return attrs
|
||||
}
|
||||
const question = sizzle(POLL_QUESTION_TAG, poll)?.[0]
|
||||
const choices = sizzle(POLL_CHOICE_TAG, poll)
|
||||
if (!question || !choices.length) {
|
||||
return attrs
|
||||
}
|
||||
|
||||
const endDate = poll.hasAttribute('end')
|
||||
? new Date(1000 * parseInt(poll.getAttribute('end')))
|
||||
: null
|
||||
|
||||
const currentPoll = {
|
||||
question: question.textContent,
|
||||
id: poll.getAttribute('id'),
|
||||
votes: parseInt(poll.getAttribute('votes') ?? 0),
|
||||
over: poll.hasAttribute('over'),
|
||||
endDate: endDate,
|
||||
time: attrs.time, // this is to be sure that we update the custom element (needed to re-enable buttons)
|
||||
choices: choices.map(c => {
|
||||
return {
|
||||
label: c.textContent,
|
||||
choice: c.getAttribute('choice'),
|
||||
votes: parseInt(c.getAttribute('votes') ?? 0)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// We will also translate some strings here.
|
||||
const body = (attrs.body ?? '')
|
||||
// eslint-disable-next-line no-undef
|
||||
.replace(LOC_poll_is_over, __(LOC_poll_is_over))
|
||||
// eslint-disable-next-line no-undef
|
||||
.replace(LOC_poll_vote_instructions_xmpp, __(LOC_poll_vote_instructions)) // changing instructions on the fly
|
||||
|
||||
return Object.assign(
|
||||
attrs,
|
||||
{
|
||||
current_poll: currentPoll,
|
||||
body
|
||||
}
|
||||
)
|
||||
})
|
||||
},
|
||||
|
||||
overrides: {
|
||||
ChatRoom: {
|
||||
onMessage: function onMessage (attrs) {
|
||||
if (!attrs.current_poll) {
|
||||
return this.__super__.onMessage(attrs)
|
||||
}
|
||||
|
||||
// We intercept poll messages, to show the banner.
|
||||
// We just drop archived messages, to not show the banner for finished polls.
|
||||
if (attrs.is_archived) {
|
||||
return this.__super__.onMessage(attrs)
|
||||
}
|
||||
if (attrs.is_delayed) {
|
||||
// When archiving is disabled, the "history" mechanism is still available:
|
||||
// Last X (20 by default) messages will be kept, and sent to users.
|
||||
// The only thing that differentiates such messages is that they are delayed.
|
||||
// We can't just ignore all delayed messages, because if one day we enable SMACKS
|
||||
// (to handle deconnections on poor network), there could be some legitimate delayed messages.
|
||||
// So we will only ignore the poll if it was sent more than X minutes ago.
|
||||
console.debug('Got a delayed poll message, checking if old or not')
|
||||
const d = new Date()
|
||||
d.setMinutes(d.getMinutes() - delayedTimeout)
|
||||
if (attrs.time < d.toISOString()) {
|
||||
console.debug(
|
||||
`Poll message was delayed fore more than ${delayedTimeout} minutes (${attrs.time} < ${d.toISOString()}).`
|
||||
)
|
||||
return this.__super__.onMessage(attrs)
|
||||
}
|
||||
}
|
||||
|
||||
console.info('Got a poll message, setting it as the current_poll')
|
||||
// this will be displayed by the livechat-converse-muc-poll custom element,
|
||||
// which is inserted in the DOM by the muc.js template overload.
|
||||
this.set('current_poll', attrs.current_poll)
|
||||
|
||||
return this.__super__.onMessage(attrs)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
@ -1,49 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { __ } from 'i18n'
|
||||
import BaseModal from 'plugins/modal/modal.js'
|
||||
import { api } from '@converse/headless/core'
|
||||
import { modal_close_button as ModalCloseButton } from 'plugins/modal/templates/buttons.js'
|
||||
import { html } from 'lit'
|
||||
|
||||
class PollFormModal extends BaseModal {
|
||||
initialize () {
|
||||
super.initialize()
|
||||
}
|
||||
|
||||
onHide () {
|
||||
super.onHide()
|
||||
api.modal.remove('livechat-converse-poll-form-modal')
|
||||
}
|
||||
|
||||
renderModal () {
|
||||
return html`<livechat-converse-poll-form .model=${this.model} .modal=${this}></livechat-converse-poll-form>`
|
||||
}
|
||||
|
||||
getModalTitle () {
|
||||
// eslint-disable-next-line no-undef
|
||||
return __(LOC_new_poll)
|
||||
}
|
||||
|
||||
renderModalFooter () {
|
||||
return html`
|
||||
<div class="modal-footer">
|
||||
${ModalCloseButton}
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary"
|
||||
@click=${(ev) => {
|
||||
ev.preventDefault()
|
||||
this.querySelector('livechat-converse-poll-form form')?.requestSubmit()
|
||||
}}
|
||||
>
|
||||
${__('Ok')}
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
api.elements.define('livechat-converse-poll-form-modal', PollFormModal)
|
@ -1,17 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
.conversejs {
|
||||
livechat-converse-poll-form-modal {
|
||||
/* Special case: when the form is in a modal */
|
||||
|
||||
.converse-form {
|
||||
max-height: 50vh;
|
||||
overflow-y: scroll;
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,134 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
.conversejs {
|
||||
livechat-converse-muc-poll {
|
||||
background-color: var(--peertube-main-background);
|
||||
color: var(--peertube-main-foreground);
|
||||
|
||||
& > div {
|
||||
border: 1px solid var(--peertube-menu-background);
|
||||
margin: 5px;
|
||||
padding: 5px;
|
||||
|
||||
.livechat-poll-toggle {
|
||||
background: unset;
|
||||
border: 0;
|
||||
padding-left: 0.25em;
|
||||
padding-right: 0.25em;
|
||||
}
|
||||
|
||||
.livechat-poll-close {
|
||||
background: unset;
|
||||
border: 0;
|
||||
float: right;
|
||||
}
|
||||
|
||||
p.livechat-poll-question {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
|
||||
span {
|
||||
cursor: pointer; // because a click toggles
|
||||
}
|
||||
}
|
||||
|
||||
p.livechat-poll-instructions {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
p.livechat-poll-end {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
table {
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
width: 100%;
|
||||
|
||||
td:first-child {
|
||||
padding-right: 0.5rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
td.livechat-poll-choice-label {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
td:last-child {
|
||||
white-space: nowrap;
|
||||
width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
.livechat-progress-bar {
|
||||
background-color: var(--peertube-menu-background);
|
||||
border: 1px solid var(--peertube-menu-background);
|
||||
color: var(--peertube-menu-foreground);
|
||||
height: 1.25rem;
|
||||
font-size: 0.75rem;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
width: 100px;
|
||||
|
||||
div {
|
||||
background-color: var(--peertube-button-background);
|
||||
float: left;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
p {
|
||||
display: inline;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[livechat-converse-root-height="small"],
|
||||
&[livechat-converse-root-height="medium"] {
|
||||
/* stylelint-disable-next-line no-descending-specificity */
|
||||
livechat-converse-muc-poll > div {
|
||||
max-height: 150px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
body[livechat-viewer-mode="on"] {
|
||||
livechat-converse-muc-poll {
|
||||
/* Dont display the poll before user choose a nickname */
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.livechat-readonly {
|
||||
.conversejs {
|
||||
/* stylelint-disable-next-line no-descending-specificity */
|
||||
livechat-converse-muc-poll > div {
|
||||
// In readonly mode, dont impose max-height
|
||||
max-height: initial !important;
|
||||
overflow-y: visible !important;
|
||||
|
||||
&.livechat-poll-over {
|
||||
// stop showing poll when over in readonly mode
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
p.livechat-poll-instructions {
|
||||
// No need for instruction in readonly mode
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { converseLocalizedHelpUrl } from '../../../shared/lib/help'
|
||||
import { html } from 'lit'
|
||||
import { __ } from 'i18n'
|
||||
export function tplPollForm (el) {
|
||||
const i18nOk = __('Ok')
|
||||
// eslint-disable-next-line no-undef
|
||||
const i18nHelp = __(LOC_online_help)
|
||||
const helpUrl = converseLocalizedHelpUrl({
|
||||
page: 'documentation/user/streamers/polls'
|
||||
})
|
||||
|
||||
return html`
|
||||
${el.alert_message ? html`<div class="error">${el.alert_message}</div>` : ''}
|
||||
${
|
||||
el.form_fields
|
||||
? html`
|
||||
<form class="converse-form" @submit=${ev => el.formSubmit(ev)}>
|
||||
<p class="title">
|
||||
${el.title}
|
||||
<a href="${helpUrl}" target="_blank"><converse-icon
|
||||
class="fa fa-circle-question"
|
||||
size="1em"
|
||||
title="${i18nHelp}"
|
||||
></converse-icon></a>
|
||||
</p>
|
||||
<p class="form-help instructions">${el.instructions}</p>
|
||||
<div class="form-errors hidden"></div>
|
||||
|
||||
${el.form_fields}
|
||||
|
||||
<fieldset class="buttons form-group">
|
||||
<input type="submit" class="btn btn-primary" value="${i18nOk}" />
|
||||
</fieldset>
|
||||
</form>`
|
||||
: ''
|
||||
}`
|
||||
}
|
@ -1,123 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { html } from 'lit'
|
||||
import { repeat } from 'lit/directives/repeat.js'
|
||||
import { __ } from 'i18n'
|
||||
|
||||
function _tplPollInstructions (el, currentPoll, canVote) {
|
||||
if (currentPoll.over || !canVote) {
|
||||
return html``
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const i18nPollInstructions = __(LOC_poll_vote_instructions)
|
||||
return html`<p class="livechat-poll-instructions">
|
||||
${i18nPollInstructions}
|
||||
</p>`
|
||||
}
|
||||
|
||||
function _tplPollEnd (el, currentPoll) {
|
||||
if (!currentPoll.endDate) {
|
||||
return html``
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const i18nPollEnd = __(LOC_poll_end)
|
||||
return html`<p class="livechat-poll-end">
|
||||
${i18nPollEnd}
|
||||
${currentPoll.endDate.toLocaleString()}
|
||||
</p>`
|
||||
}
|
||||
|
||||
function _tplChoice (el, currentPoll, choice, canVote) {
|
||||
// eslint-disable-next-line no-undef
|
||||
const i18nChoiceN = '' + choice.choice + ':'
|
||||
|
||||
const votes = choice.votes
|
||||
const totalVotes = currentPoll.votes
|
||||
const percent = totalVotes ? (100 * votes / totalVotes).toFixed(2) : '0.00'
|
||||
return html`
|
||||
<tr>
|
||||
<td>
|
||||
${
|
||||
currentPoll.over || !canVote
|
||||
? html`${i18nChoiceN}`
|
||||
: html`
|
||||
<button type="button" class="btn btn-primary btn-sm"
|
||||
@click=${ev => {
|
||||
ev.preventDefault()
|
||||
el.voteFor(choice)
|
||||
}}
|
||||
?disabled=${el.buttonDisabled}
|
||||
>
|
||||
${i18nChoiceN}
|
||||
</button>`
|
||||
}
|
||||
</td>
|
||||
<td class="livechat-poll-choice-label">
|
||||
${choice.label}
|
||||
</td>
|
||||
<td>
|
||||
<div class="livechat-progress-bar">
|
||||
<div
|
||||
role="progressbar"
|
||||
style="width: ${percent}%;"
|
||||
aria-valuenow="${percent}" aria-valuemin="0" aria-valuemax="100"
|
||||
></div>
|
||||
<p>
|
||||
${votes}/${totalVotes}
|
||||
(${percent}%)
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>`
|
||||
}
|
||||
|
||||
export function tplPoll (el, currentPoll, canVote) {
|
||||
if (!currentPoll) {
|
||||
return html``
|
||||
}
|
||||
|
||||
return html`<div class="${currentPoll.over ? 'livechat-poll-over' : ''}">
|
||||
<p class="livechat-poll-question">
|
||||
${currentPoll.over
|
||||
? html`<button class="livechat-poll-close" @click=${el.closePoll} title="${__('Close')}">
|
||||
<converse-icon class="fa fa-times" size="1em"></converse-icon>
|
||||
</button>`
|
||||
: ''
|
||||
}
|
||||
${el.collapsed
|
||||
? html`
|
||||
<button @click=${el.toggle} class="livechat-poll-toggle">
|
||||
<converse-icon
|
||||
color="var(--muc-toolbar-btn-color)"
|
||||
class="fa fa-angle-right"
|
||||
size="1em"></converse-icon>
|
||||
</button>`
|
||||
: html`
|
||||
<button @click=${el.toggle} class="livechat-poll-toggle">
|
||||
<converse-icon
|
||||
color="var(--muc-toolbar-btn-color)"
|
||||
class="fa fa-angle-down"
|
||||
size="1em"></converse-icon>
|
||||
</button>`
|
||||
}
|
||||
<span @click=${el.toggle}>
|
||||
${currentPoll.question}
|
||||
</span>
|
||||
</p>
|
||||
${
|
||||
el.collapsed
|
||||
? ''
|
||||
: html`
|
||||
<table><tbody>
|
||||
${repeat(currentPoll.choices ?? [], (c) => c.choice, (c) => _tplChoice(el, currentPoll, c, canVote))}
|
||||
</tbody></table>
|
||||
${_tplPollInstructions(el, currentPoll, canVote)}
|
||||
${_tplPollEnd(el, currentPoll)}
|
||||
`
|
||||
}
|
||||
</div>`
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { XMLNS_POLL } from './constants.js'
|
||||
import { _converse, api } from '../../../src/headless/core.js'
|
||||
import { __ } from 'i18n'
|
||||
|
||||
export function getHeadingButtons (view, buttons) {
|
||||
const muc = view.model
|
||||
if (muc.get('type') !== _converse.CHATROOMS_TYPE) {
|
||||
// only on MUC.
|
||||
return buttons
|
||||
}
|
||||
|
||||
if (!muc.features?.get?.(XMLNS_POLL)) {
|
||||
// Poll feature not available (can happen if the chat is remote, and the plugin not up to date)
|
||||
return buttons
|
||||
}
|
||||
|
||||
const myself = muc.getOwnOccupant()
|
||||
if (!myself || !['admin', 'owner'].includes(myself.get('affiliation'))) {
|
||||
return buttons
|
||||
}
|
||||
|
||||
// Adding a "New poll" button.
|
||||
buttons.unshift({
|
||||
// eslint-disable-next-line no-undef
|
||||
i18n_text: __(LOC_new_poll),
|
||||
handler: async (ev) => {
|
||||
ev.preventDefault()
|
||||
api.modal.show('livechat-converse-poll-form-modal', { model: muc })
|
||||
},
|
||||
a_class: '',
|
||||
icon_class: 'fa-square-poll-horizontal',
|
||||
name: 'muc-create-poll'
|
||||
})
|
||||
|
||||
return buttons
|
||||
}
|
@ -43,25 +43,17 @@ function start () {
|
||||
|
||||
function stop () {
|
||||
rootResizeObserver.disconnect()
|
||||
const root = document.querySelector('converse-root')
|
||||
if (root) {
|
||||
root.removeAttribute('livechat-converse-root-width')
|
||||
root.removeAttribute('livechat-converse-root-height')
|
||||
}
|
||||
document.querySelector('converse-root')?.removeAttribute('livechat-converse-root-width')
|
||||
}
|
||||
|
||||
function handle (el) {
|
||||
const rect = el.getBoundingClientRect()
|
||||
const height = rect.height > 576 ? 'high' : (rect.height > 250 ? 'medium' : 'small')
|
||||
const width = rect.width > 576 ? 'large' : (rect.width > 250 ? 'medium' : 'small')
|
||||
const previousHeight = el.getAttribute('livechat-converse-root-height')
|
||||
const previousWidth = el.getAttribute('livechat-converse-root-width')
|
||||
if (width === previousWidth && height === previousHeight) { return }
|
||||
const previous = el.getAttribute('livechat-converse-root-width')
|
||||
if (width === previous) { return }
|
||||
|
||||
el.setAttribute('livechat-converse-root-width', width)
|
||||
el.setAttribute('livechat-converse-root-height', height)
|
||||
api.trigger('livechatSizeChanged', {
|
||||
height: height,
|
||||
width: width
|
||||
})
|
||||
}
|
||||
|
@ -1,62 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { CustomElement } from 'shared/components/element.js'
|
||||
import { api } from '@converse/headless/core'
|
||||
import { html } from 'lit'
|
||||
import { __ } from 'i18n'
|
||||
|
||||
import '../styles/muc-terms.scss'
|
||||
|
||||
export default class MUCTermsView extends CustomElement {
|
||||
static get properties () {
|
||||
return {
|
||||
model: { type: Object, attribute: true },
|
||||
termstype: { type: String, attribute: true }
|
||||
}
|
||||
}
|
||||
|
||||
async initialize () {
|
||||
if (!this.model) {
|
||||
return
|
||||
}
|
||||
this.listenTo(this.model, 'change:x_livechat_terms_' + this.termstype, () => this.requestUpdate())
|
||||
}
|
||||
|
||||
render () {
|
||||
const terms = this.model?.get('x_livechat_terms_' + this.termstype)
|
||||
return html`
|
||||
${terms && terms.body && !this._hideInfoBox(terms.body)
|
||||
? html`
|
||||
<div>
|
||||
<converse-rich-text text=${terms.body} render_styling></converse-rich-text>
|
||||
<i class="livechat-hide-terms-info-box" @click=${this.closeInfoBox} title=${__('Close')}>
|
||||
<converse-icon class="fa fa-times" size="1em"></converse-icon>
|
||||
</i>
|
||||
</div>`
|
||||
: ''
|
||||
}`
|
||||
}
|
||||
|
||||
closeInfoBox (ev) {
|
||||
ev.preventDefault()
|
||||
const terms = this.model?.get('x_livechat_terms_' + this.termstype)
|
||||
if (terms) {
|
||||
localStorage?.setItem('x_livechat_terms_' + this.termstype + '_hidden', terms.body)
|
||||
}
|
||||
this.requestUpdate()
|
||||
}
|
||||
|
||||
_hideInfoBox (body) {
|
||||
// When hiding the infobox, we store in localStorage the current body, so we will show it again if message change.
|
||||
// Note: for termstype=global we don't store the MUC server, so if user join chat from different instances,
|
||||
// it will show terms again
|
||||
// Note: same for termstype=muc, we don't store the MUC JID, so if user changes channel,
|
||||
// it will probably show terms again
|
||||
const lsHideInfoBox = localStorage?.getItem('x_livechat_terms_' + this.termstype + '_hidden')
|
||||
return lsHideInfoBox === body
|
||||
}
|
||||
}
|
||||
|
||||
api.elements.define('livechat-converse-muc-terms', MUCTermsView)
|
@ -1,53 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { converse, api } from '../../../src/headless/core.js'
|
||||
import './components/muc-terms.js'
|
||||
|
||||
const { sizzle } = converse.env
|
||||
|
||||
converse.plugins.add('livechat-converse-terms', {
|
||||
dependencies: ['converse-muc'],
|
||||
initialize () {
|
||||
api.listen.on('parseMUCMessage', (stanza, attrs) => {
|
||||
const livechatTerms = sizzle('x-livechat-terms', stanza)
|
||||
if (!livechatTerms.length) {
|
||||
return attrs
|
||||
}
|
||||
return Object.assign(
|
||||
attrs,
|
||||
{
|
||||
x_livechat_terms: livechatTerms[0].getAttribute('type')
|
||||
}
|
||||
)
|
||||
})
|
||||
},
|
||||
overrides: {
|
||||
ChatRoom: {
|
||||
onMessage: function onMessage (attrs) {
|
||||
if (!attrs.x_livechat_terms) {
|
||||
return this.__super__.onMessage(attrs)
|
||||
}
|
||||
// We received a x-livechat-terms message, we don't forward it to standard onMessage,
|
||||
// but we just update the room attribute.
|
||||
const type = attrs.x_livechat_terms
|
||||
if (type !== 'global' && type !== 'muc') {
|
||||
console.error('Invalid x-livechat-terms type: ', type)
|
||||
return
|
||||
}
|
||||
if (attrs.is_archived) {
|
||||
// This should not happen, as we add some no-store hints. But, just in case.
|
||||
console.info('Dropping an archived x-livechat-terms message')
|
||||
return
|
||||
}
|
||||
// console.info('Received a x-livechat-terms message', attrs)
|
||||
const options = {}
|
||||
options['x_livechat_terms_' + type] = attrs
|
||||
this.set(options)
|
||||
// this will be displayed by the livechat-converse-muc-terms custom element,
|
||||
// which is inserted in the DOM by the muc.js template overload.
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
@ -1,42 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
.conversejs {
|
||||
livechat-converse-muc-terms {
|
||||
background-color: var(--peertube-main-background);
|
||||
color: var(--peertube-main-foreground);
|
||||
|
||||
div {
|
||||
align-items: center;
|
||||
border: 1px solid var(--peertube-menu-background);
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
justify-content: space-between;
|
||||
margin: 5px;
|
||||
padding: 5px;
|
||||
|
||||
converse-rich-text {
|
||||
flex-grow: 2;
|
||||
max-height: 5em;
|
||||
overflow-y: scroll;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.livechat-hide-terms-info-box {
|
||||
cursor: pointer;
|
||||
font-size: var(--font-size-small);
|
||||
flex-shrink: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.livechat-readonly,
|
||||
body[livechat-viewer-mode="on"] {
|
||||
livechat-converse-muc-terms {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
@ -23,11 +23,6 @@ export default () => {
|
||||
<symbol id="icon-circle-question" viewBox="0 0 448 512">
|
||||
<path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM169.8 165.3c7.9-22.3 29.1-37.3 52.8-37.3h58.3c34.9 0 63.1 28.3 63.1 63.1c0 22.6-12.1 43.5-31.7 54.8L280 264.4c-.2 13-10.9 23.6-24 23.6c-13.3 0-24-10.7-24-24V250.5c0-8.6 4.6-16.5 12.1-20.8l44.3-25.4c4.7-2.7 7.6-7.7 7.6-13.1c0-8.4-6.8-15.1-15.1-15.1H222.6c-3.4 0-6.4 2.1-7.5 5.3l-.4 1.2c-4.4 12.5-18.2 19-30.6 14.6s-19-18.2-14.6-30.6l.4-1.2zM224 352a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"/>
|
||||
</symbol>
|
||||
|
||||
<!--!Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.-->
|
||||
<symbol id="icon-square-poll-horizontal" viewBox="0 0 448 512">
|
||||
<path d="M448 96c0-35.3-28.7-64-64-64L64 32C28.7 32 0 60.7 0 96L0 416c0 35.3 28.7 64 64 64l320 0c35.3 0 64-28.7 64-64l0-320zM256 160c0 17.7-14.3 32-32 32l-96 0c-17.7 0-32-14.3-32-32s14.3-32 32-32l96 0c17.7 0 32 14.3 32 32zm64 64c17.7 0 32 14.3 32 32s-14.3 32-32 32l-192 0c-17.7 0-32-14.3-32-32s14.3-32 32-32l192 0zM192 352c0 17.7-14.3 32-32 32l-32 0c-17.7 0-32-14.3-32-32s14.3-32 32-32l32 0c17.7 0 32 14.3 32 32z"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
`
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ body.converse-embedded converse-root.theme-peertube {
|
||||
--chat-background-color: var(--peertube-main-background);
|
||||
--chat-textarea-color: var(--peertube-input-foreground);
|
||||
--chat-textarea-background-color: var(--peertube-input-background);
|
||||
--chat-textarea-height: 60px;
|
||||
--chat-textarea-height: 38px;
|
||||
--send-button-height: 27px;
|
||||
--send-button-margin: 3px;
|
||||
--inline-action-margin: 0.75em;
|
||||
|
@ -134,8 +134,7 @@ body.livechat-transparent {
|
||||
.chat-body,
|
||||
.conversejs .chatroom .box-flyout,
|
||||
.conversejs .chatbox .chat-content,
|
||||
.conversejs .chatbox .chat-content .chat-content__notifications,
|
||||
livechat-converse-muc-poll {
|
||||
.conversejs .chatbox .chat-content .chat-content__notifications {
|
||||
background-color: rgba(0 0 0 / 0%) !important;
|
||||
}
|
||||
|
||||
@ -171,7 +170,8 @@ body.converse-embedded {
|
||||
#peertube-plugin-livechat-container {
|
||||
converse-muc-message-form {
|
||||
// For an unknown reason, message field in truncated... so adding a bottom margin.
|
||||
margin-bottom: 6px;
|
||||
max-height: 1.25rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -63,7 +63,7 @@ class SlowMode extends CustomElement {
|
||||
LOC_slow_mode_info,
|
||||
this.model.config.get('slow_mode_duration')
|
||||
)}
|
||||
<i class="livechat-hide-slow-mode-info-box" @click=${this.closeSlowModeInfoBox} title=${__('Close')}>
|
||||
<i class="livechat-hide-slow-mode-info-box" @click=${this.closeSlowModeInfoBox}>
|
||||
<converse-icon class="fa fa-times" size="1em"></converse-icon>
|
||||
</i>
|
||||
</div>`
|
||||
@ -79,21 +79,18 @@ class SlowMode extends CustomElement {
|
||||
api.elements.define('livechat-slow-mode', SlowMode)
|
||||
|
||||
const tplSlowMode = (o) => {
|
||||
if (!o.can_edit) { return html`` }
|
||||
return html`<livechat-slow-mode jid=${o.model.get('jid')}>`
|
||||
}
|
||||
|
||||
const tplViewerMode = (o) => {
|
||||
if (!api.settings.get('livechat_enable_viewer_mode')) {
|
||||
return html``
|
||||
}
|
||||
const model = o.model
|
||||
const i18nNickname = __('Nickname')
|
||||
const i18nJoin = __('Enter groupchat')
|
||||
const i18nHeading = __('Choose a nickname to enter')
|
||||
// eslint-disable-next-line no-undef
|
||||
const i18nExternalLogin = __(LOC_login_using_external_account)
|
||||
return html`
|
||||
export default (o) => {
|
||||
if (api.settings.get('livechat_enable_viewer_mode')) {
|
||||
const model = o.model
|
||||
const i18nNickname = __('Nickname')
|
||||
const i18nJoin = __('Enter groupchat')
|
||||
const i18nHeading = __('Choose a nickname to enter')
|
||||
// eslint-disable-next-line no-undef
|
||||
const i18nExternalLogin = __(LOC_login_using_external_account)
|
||||
return html`
|
||||
<div class="livechat-viewer-mode-content chatroom-form-container">
|
||||
<form class="converse-form chatroom-form" @submit=${ev => setNickname(ev, model)}>
|
||||
<label>${i18nHeading}</label>
|
||||
@ -124,38 +121,12 @@ const tplViewerMode = (o) => {
|
||||
</div>
|
||||
`
|
||||
}
|
||||
</div>`
|
||||
}
|
||||
|
||||
export default (o) => {
|
||||
// ConverseJS 10.x does not handle properly the visitor role in unmoderated rooms.
|
||||
// See https://github.com/conversejs/converse.js/issues/3428 for more info.
|
||||
// So we will do a dirty hack here to fix this case.
|
||||
// Note: ConverseJS 11.x has changed the code, and could be fixed more cleanly (or will be fixed if #3428 is fixed).
|
||||
if (o.can_edit && o.model.getOwnRole() === 'visitor') {
|
||||
o.can_edit = false
|
||||
}
|
||||
|
||||
let mutedAnonymousMessage
|
||||
if (
|
||||
!o.can_edit &&
|
||||
o.model.features?.get?.('x_peertubelivechat_mute_anonymous') &&
|
||||
_converse.api.settings.get('livechat_specific_is_anonymous') === true
|
||||
) {
|
||||
// If we are moderated because we are anonymous, we want to display a custom message.
|
||||
// FIXME: if x_peertubelivechat_mute_anonymous changes, user are first muted/voiced, and then only the
|
||||
// status 104 is sent. But we don't listen to 'change:x_peertubelivechat_mute_anonymous'.
|
||||
// So the custom message won't be correct. But this is not a big issue.
|
||||
// eslint-disable-next-line no-undef
|
||||
mutedAnonymousMessage = __(LOC_muted_anonymous_message)
|
||||
</div>
|
||||
${tplSlowMode(o)}
|
||||
${tplMucBottomPanel(o)}`
|
||||
}
|
||||
|
||||
return html`
|
||||
${tplViewerMode(o)}
|
||||
${tplSlowMode(o)}
|
||||
${
|
||||
mutedAnonymousMessage
|
||||
? html`<span class="muc-bottom-panel muc-bottom-panel--muted">${mutedAnonymousMessage}</span>`
|
||||
: tplMucBottomPanel(o)
|
||||
}`
|
||||
${tplMucBottomPanel(o)}`
|
||||
}
|
||||
|
@ -72,7 +72,7 @@ function getPeertubeButtons () {
|
||||
|
||||
export default (el) => {
|
||||
if (!api.settings.get('livechat_mini_muc_head')) {
|
||||
// original Template (this setting comes with livechatMiniMucHeadPlugin)
|
||||
// original Template (this settings comes with livechatMiniMucHeadPlugin)
|
||||
return html`${tplMucHead(el)}`
|
||||
}
|
||||
|
||||
|
@ -1,28 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2013-2024 JC Brand <https://github.com/conversejs/converse.js/>
|
||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
// Must import the original muc.js, because it imports some custom elements files.
|
||||
import '../../src/plugins/muc-views/templates/muc.js'
|
||||
import { getChatRoomBodyTemplate } from '../../src/plugins/muc-views/utils.js'
|
||||
import { html } from 'lit'
|
||||
|
||||
// Overloading the original muc.js, to add some custom elements.
|
||||
export default (o) => {
|
||||
return html`
|
||||
<div class="flyout box-flyout">
|
||||
<converse-dragresize></converse-dragresize>
|
||||
${
|
||||
o.model
|
||||
? html`
|
||||
<converse-muc-heading jid="${o.model.get('jid')}" class="chat-head chat-head-chatroom row no-gutters">
|
||||
</converse-muc-heading>
|
||||
<livechat-converse-muc-terms .model=${o.model} termstype="global"></livechat-converse-muc-terms>
|
||||
<livechat-converse-muc-terms .model=${o.model} termstype="muc"></livechat-converse-muc-terms>
|
||||
<livechat-converse-muc-poll .model=${o.model}></livechat-converse-muc-poll>
|
||||
<div class="chat-body chatroom-body row no-gutters">${getChatRoomBodyTemplate(o)}</div>`
|
||||
: ''}
|
||||
</div>`
|
||||
}
|
@ -40,7 +40,6 @@ module.exports = merge(prod, {
|
||||
alias: {
|
||||
'./templates/muc-bottom-panel.js': path.resolve('custom/templates/muc-bottom-panel.js'),
|
||||
'./templates/muc-head.js': path.resolve(__dirname, 'custom/templates/muc-head.js'),
|
||||
'./templates/muc.js': path.resolve(__dirname, 'custom/templates/muc.js'),
|
||||
'../../templates/background_logo.js$': path.resolve(__dirname, 'custom/templates/background_logo.js'),
|
||||
'./templates/muc-chatarea.js': path.resolve('custom/templates/muc-chatarea.js'),
|
||||
|
||||
|
@ -86,8 +86,7 @@ function defaultConverseParams (
|
||||
'livechatViewerModePlugin',
|
||||
'livechatDisconnectOnUnloadPlugin',
|
||||
'converse-slow-mode',
|
||||
'livechatEmojis',
|
||||
'converse-moderation-delay'
|
||||
'livechatEmojis'
|
||||
],
|
||||
show_retraction_warning: false, // No need to use this warning (except if we open to external clients?)
|
||||
muc_show_info_messages: mucShowInfoMessages,
|
||||
@ -96,7 +95,6 @@ function defaultConverseParams (
|
||||
prune_messages_above: 100, // only keep 100 message in history.
|
||||
pruning_behavior: 'unscrolled',
|
||||
colorize_username: true,
|
||||
send_chat_markers: [],
|
||||
|
||||
// This is a specific settings, that is used in ConverseJS customization, to force avatars loading in readonly mode.
|
||||
livechat_load_all_vcards: !!forceReadonly,
|
||||
|
@ -9,9 +9,7 @@ export const livechatSpecificsPlugin = {
|
||||
|
||||
_converse.api.settings.extend({
|
||||
// if user is authenticated with an external account (to add a logout button)
|
||||
livechat_specific_external_authent: false,
|
||||
// if user is anonymous
|
||||
livechat_specific_is_anonymous: false
|
||||
livechat_specific_external_authent: false
|
||||
})
|
||||
|
||||
_converse.api.listen.on('getHeadingButtons', (view: any, buttons: any[]) => {
|
||||
|
@ -1,70 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
const MODERATION_DELAY_TAG = 'moderation-delay'
|
||||
|
||||
/**
|
||||
* Moderation delay plugin definition.
|
||||
* This module adds a time counter for moderators, so they now how many time remains before message is broadcasted.
|
||||
*/
|
||||
export const moderationDelayPlugin = {
|
||||
dependencies: ['converse-muc', 'converse-muc-views'],
|
||||
async initialize (this: any) {
|
||||
const _converse = this._converse
|
||||
|
||||
_converse.api.listen.on('parseMUCMessage', (stanza: any, attrs: any) => {
|
||||
// Checking if there is any moderation delay in the message.
|
||||
const waiting = window.converse.env.sizzle(MODERATION_DELAY_TAG, stanza)?.[0]?.getAttribute('waiting')
|
||||
if (!waiting) { return attrs }
|
||||
return Object.assign(
|
||||
attrs,
|
||||
{
|
||||
moderation_delay_waiting: waiting
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
const Message = _converse.api.elements.registry['converse-chat-message']
|
||||
if (Message) {
|
||||
class MessageOverloaded extends Message {
|
||||
getDerivedMessageProps (): ReturnType<typeof Message.getDerivedMessageProps> {
|
||||
const r = super.getDerivedMessageProps()
|
||||
const waiting = this.model.get('moderation_delay_waiting')
|
||||
if (!waiting) {
|
||||
return r
|
||||
}
|
||||
const remains = waiting - (Date.now() / 1000)
|
||||
if (remains < 0) {
|
||||
// Message already broadcasted
|
||||
return r
|
||||
}
|
||||
|
||||
// Ok... We will add some info about how many remains...
|
||||
r.pretty_time = window.converse.env.html`
|
||||
${r.pretty_time}<span aria-hidden="true"> - ${Math.round(remains)}⏱</span>
|
||||
`
|
||||
// and we must update in 1 second...
|
||||
setTimeout(() => this.requestUpdate(), 1000)
|
||||
return r
|
||||
}
|
||||
}
|
||||
_converse.api.elements.define('converse-chat-message', MessageOverloaded)
|
||||
} else {
|
||||
console.error('Cannot find converse-chat-message custom elements, moderation delay will not be properly shown.')
|
||||
}
|
||||
},
|
||||
overrides: {
|
||||
ChatRoom: {
|
||||
getUpdatedMessageAttributes: function getUpdatedMessageAttributes (this: any, message: any, attrs: any) {
|
||||
const newAttrs = this.__super__.getUpdatedMessageAttributes(message, attrs)
|
||||
if (attrs.moderation_delay_waiting) {
|
||||
Object.assign(newAttrs, {
|
||||
moderation_delay_waiting: attrs.moderation_delay_waiting
|
||||
})
|
||||
}
|
||||
return newAttrs
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -34,22 +34,7 @@ const locKeys = [
|
||||
'task_delete_confirm',
|
||||
'task_list_pick_title',
|
||||
'task_list_pick_empty',
|
||||
'task_list_pick_message',
|
||||
'muted_anonymous_message',
|
||||
'new_poll',
|
||||
'poll_question',
|
||||
'poll_duration',
|
||||
'poll_anonymous_results',
|
||||
'poll_choice_n',
|
||||
'poll_title',
|
||||
'poll_instructions',
|
||||
'poll_end',
|
||||
'poll',
|
||||
'poll_vote_instructions',
|
||||
'poll_vote_instructions_xmpp',
|
||||
'poll_is_over',
|
||||
'poll_choice_invalid',
|
||||
'poll_anonymous_vote_ok'
|
||||
'task_list_pick_message'
|
||||
]
|
||||
|
||||
module.exports = locKeys
|
||||
|
@ -468,11 +468,8 @@ invalid_value_file_too_big: 'Die Datei ist zu groß (maximale Größe: %s).'
|
||||
invalid_value_duplicate: Doppelter Wert
|
||||
livechat_configuration_channel_emojis_title: Kanal-Emojis
|
||||
livechat_emojis_shortname: Kurzname
|
||||
livechat_emojis_shortname_desc: "Sie können Emojis im Chat mit \":Kurzname:\" verwenden.\n
|
||||
Der Kurzname kann mit einem Doppelpunkt (:) beginnen und/oder enden und darf nur
|
||||
alphanumerische Zeichen, Unterstriche und Bindestriche enthalten.\nEs wird dringend
|
||||
empfohlen, sie mit einem Doppelpunkt zu beginnen, damit die Benutzer die automatische
|
||||
Vervollständigung nutzen können (indem sie \":\" eingeben und dann TAB drücken).\n"
|
||||
livechat_emojis_shortname_desc: "Sie können Emojis mit \":Kurzname:\" verwenden.\n
|
||||
Der Kurzname darf nur alphanumerische Zeichen, Unterstriche und Bindestriche enthalten.\n"
|
||||
livechat_configuration_channel_emojis_desc: "Sie können benutzerdefinierte Emojis
|
||||
für Ihren Kanal konfigurieren.\nDiese Emojis werden in der Emoji-Auswahl verfügbar
|
||||
sein.\nBenutzer können sie auch mit ihrem Kurznamen verwenden (z. B. indem sie \"\
|
||||
@ -515,45 +512,3 @@ auth_description: "<h3>Authentifizierung</h3>\n"
|
||||
livechat_token_disabled_label: Livechat-Token deaktivieren
|
||||
share_chat_dock: Dock
|
||||
token_date: Datum
|
||||
too_many_entries: Zu viele Einträge
|
||||
livechat_configuration_channel_mute_anonymous_label: Anonyme Benutzer stummschalten
|
||||
muted_anonymous_message: Nur registrierte Benutzer können Nachrichten versenden.
|
||||
livechat_configuration_channel_mute_anonymous_desc: "Standardwert für neue Chaträume.\n
|
||||
Für bestehende Chaträume können Sie die Funktion im Raumkonfigurationsformular ändern.\n
|
||||
Wenn diese Funktion aktiviert ist, können anonyme Benutzer den Chat nur lesen, aber
|
||||
keine Nachrichten senden.\n"
|
||||
chat_terms_label: Nutzungsbedingungen
|
||||
chat_terms_description: "Diese Nutzungsbedingungen werden allen Nutzern angezeigt,
|
||||
wenn sie Chaträumen beitreten.\nStreamer können auch Bedingungen für ihre Kanäle
|
||||
konfigurieren, die direkt nach diesen globalen Bedingungen angezeigt werden.\n"
|
||||
invalid_value_too_long: Wert zu lang
|
||||
livechat_configuration_channel_terms_label: Chat Nutzungsbedingungen und Konditionen
|
||||
des Kanals
|
||||
livechat_configuration_channel_terms_desc: "Sie können eine \"Nutzungsbedingungen\"\
|
||||
-Nachricht konfigurieren, die Benutzern angezeigt wird, die Ihren Chaträumen beitreten.\n"
|
||||
poll_vote_instructions_xmpp: 'Senden Sie eine Nachricht mit einem Ausrufezeichen,
|
||||
gefolgt von Ihrer Wahlnummer, um abzustimmen. Beispiel: !1'
|
||||
new_poll: Eine neue Umfrage erstellen
|
||||
poll_instructions: Füllen Sie dieses Formular aus und senden Sie es ab, um eine neue
|
||||
Umfrage zu erstellen. Damit wird eine bestehende Umfrage beendet und ersetzt.
|
||||
poll_vote_instructions: 'Um abzustimmen, klicken Sie auf Ihre Wahl oder senden Sie
|
||||
eine Nachricht mit einem Ausrufezeichen gefolgt von der Nummer Ihrer Wahl (Beispiel:
|
||||
!1).'
|
||||
poll_anonymous_vote_ok: Ihre Stimme wird berücksichtigt. Die Stimmen sind anonym,
|
||||
sie werden den anderen Teilnehmern nicht angezeigt.
|
||||
poll: Umfrage
|
||||
poll_title: Neue Umfrage
|
||||
poll_is_over: Diese Umfrage ist nun beendet.
|
||||
poll_question: Frage
|
||||
poll_duration: Umfragezeit (in Minuten)
|
||||
poll_anonymous_results: Anonyme Ergebnisse
|
||||
poll_choice_n: 'Wahl {{N}}:'
|
||||
poll_end: 'Die Umfrage endet am:'
|
||||
poll_choice_invalid: Diese Wahl ist nicht gültig.
|
||||
poll_vote_ok: Ihre Stimme wurde berücksichtigt, die Zähler werden in Kürze aktualisiert.
|
||||
moderation_delay: Moderationsverzögerung
|
||||
livechat_configuration_channel_moderation_delay_desc: "Standardwert der Moderationsverzögerung:\n\
|
||||
<ul>\n <li>0: Moderationsverzögerung deaktiviert</li>\n <li>Beliebige positive
|
||||
ganze Zahl: Nachrichten werden für Nicht-Moderator-Teilnehmer um X Sekunden verzögert,
|
||||
so dass Moderatoren die Nachricht löschen können, bevor ein anderer Benutzer sie
|
||||
lesen kann.</li>\n</ul>\n"
|
||||
|
@ -45,11 +45,6 @@ diagnostic: |
|
||||
|
||||
chat_title: "<h3>Chat</h3>"
|
||||
|
||||
chat_terms_label: "Terms & Conditions"
|
||||
chat_terms_description: |
|
||||
These terms & conditions will be shown to all users when then join chatrooms.
|
||||
Streamers can also configure terms & conditions for their channels, that will be shown right after these global terms & conditions.
|
||||
|
||||
list_rooms_label: "List existing rooms"
|
||||
list_rooms_description: |
|
||||
<a class="peertube-plugin-livechat-prosody-list-rooms-btn">List rooms</a>
|
||||
@ -243,7 +238,7 @@ prosody_peertube_uri_description: |
|
||||
prosody_muc_log_by_default_label: "Log rooms content by default"
|
||||
prosody_muc_log_by_default_description: |
|
||||
If checked, room contents will be saved by default.
|
||||
Any user who joins a room will see what was written before they joins.<br>
|
||||
Any user who joins a room will see what was written before he joins.<br>
|
||||
Please note that it is always possible to enable/disable the content
|
||||
archiving for a specific room, by editing its properties.
|
||||
|
||||
@ -460,7 +455,6 @@ invalid_value_wrong_format: "Value is in the wrong format."
|
||||
invalid_value_not_in_range: "Value is not in authorized range."
|
||||
invalid_value_file_too_big: "File size is too big (max size: %s)."
|
||||
invalid_value_duplicate: "Duplicate value"
|
||||
invalid_value_too_long: "Value too long"
|
||||
too_many_entries: "Too many entries"
|
||||
|
||||
slow_mode_info: "Slow mode is enabled, users can send a message every %1$s seconds."
|
||||
@ -511,9 +505,8 @@ livechat_configuration_channel_emojis_desc: |
|
||||
Users can also use them by with their short name (for example by writing ":shortname:").
|
||||
livechat_emojis_shortname: 'Short name'
|
||||
livechat_emojis_shortname_desc: |
|
||||
You can use emojis in the chat using ":shortname:".
|
||||
The short name can start and/or end by a colon (:), and only contain alphanumerical characters, underscores and hyphens.
|
||||
It is strongly recommended to start them by a colon, so that users can use autocompletion (by typing ":" then press TAB).
|
||||
You can use emojis using ":shortname:".
|
||||
The short name can only contain alphanumerical characters, underscores and hyphens.
|
||||
livechat_emojis_file: 'File'
|
||||
livechat_emojis_file_desc: |
|
||||
The emoji file.
|
||||
@ -553,37 +546,3 @@ livechat_token_disabled_description: |
|
||||
These tokens can for example be used to include the chat in OBS web docks.
|
||||
Check <a href="https://livingston.frama.io/peertube-plugin-livechat/documentation/user/obs" target="_blank">the documentation</a> for more information.
|
||||
You can disable this feature by checking this setting.
|
||||
|
||||
muted_anonymous_message: Only registered users can send messages.
|
||||
livechat_configuration_channel_mute_anonymous_label: "Mute anonymous users"
|
||||
livechat_configuration_channel_mute_anonymous_desc: |
|
||||
Default value for new chatrooms.
|
||||
For existing chatrooms, you can change the feature in the room configuration form.
|
||||
When this feature is enabled, anonymous users can only read the chat, and not send messages.
|
||||
livechat_configuration_channel_terms_label: "Channel's chat terms & conditions"
|
||||
livechat_configuration_channel_terms_desc: |
|
||||
You can configure a "terms & conditions" message that will be shown to users joining your chatrooms.
|
||||
|
||||
new_poll: Create a new poll
|
||||
poll: Poll
|
||||
poll_title: New poll
|
||||
poll_instructions: Complete and submit this form to create a new poll. This will end and replace any existing poll.
|
||||
poll_question: Question
|
||||
poll_duration: Poll duration (in minutes)
|
||||
poll_anonymous_results: Anonymous results
|
||||
poll_choice_n: 'Choice {{N}}:'
|
||||
poll_end: 'Poll ends at:'
|
||||
poll_vote_instructions: "To vote, click on your choice or send a message with an exclamation mark followed by your choice number (Example: !1)."
|
||||
poll_vote_instructions_xmpp: "Send a message with an exclamation mark followed by your choice number to vote. Example: !1"
|
||||
poll_is_over: This poll is now over.
|
||||
poll_choice_invalid: This choice is not valid.
|
||||
poll_anonymous_vote_ok: Your vote is taken into account. Votes are anonymous, they will not be shown to other participants.
|
||||
poll_vote_ok: Your vote has been taking into account, the counters will be updated in a moment.
|
||||
|
||||
moderation_delay: Moderation delay
|
||||
livechat_configuration_channel_moderation_delay_desc: |
|
||||
Moderation delay default value:
|
||||
<ul>
|
||||
<li>0: moderation delay disabled</li>
|
||||
<li>Any positive integer: messages will be delayed for X seconds for non-moderator participants, allowing moderators to delete message before any user can read it.</li>
|
||||
</ul>
|
||||
|
@ -484,11 +484,9 @@ invalid_value_file_too_big: 'La taille du fichier est trop grande (taille maximu
|
||||
invalid_value_duplicate: Valeur en double
|
||||
livechat_configuration_channel_emojis_title: Émojis de la chaîne
|
||||
livechat_emojis_shortname: Nom court
|
||||
livechat_emojis_shortname_desc: "Vous pouvez utiliser l'émojis dans le tchat en utilisant
|
||||
\":nom_court:\".\nLe nom court peut commencer et/ou finir par des deux-points (:),
|
||||
et seulement contenir des caractères alphanumériques des underscores et des tirets.\n
|
||||
Il est fortement recommandé de les commencer par des deux-points, pour que les utilisateur⋅rices
|
||||
puissent utiliser l'autocomplétion (en tapant \":\" puis en pressant TABULATION).\n"
|
||||
livechat_emojis_shortname_desc: "Vous pouvez utiliser l'émojis en utilisant \":nom_court:\"\
|
||||
.\nLe nom court peut seulement contenir des caractères alphanumériques des underscores
|
||||
et des tirets.\n"
|
||||
livechat_emojis_file: Fichier
|
||||
livechat_emojis_file_desc: "Le fichier de l'émoji.\n"
|
||||
action_export: Exporter
|
||||
@ -525,51 +523,3 @@ livechat_token_disabled_description: "Les utilisateur⋅rices peuvent générer
|
||||
\ target=\"_blank\">la documentation</a> pour plus d'informations.\nVous pouvez
|
||||
désactiver cette fonctionnalité en cochant ce paramètre.\n"
|
||||
too_many_entries: "Trop d'entrées"
|
||||
livechat_configuration_channel_mute_anonymous_desc: "Valeur par défaut pour les nouveaux
|
||||
salons de discussion.\nPour les salons existants, vous pouvez changer la fonctionnalité
|
||||
via le formulaire de configuration du salon.\nQuand la fonctionnalité est active,
|
||||
les utilisateur⋅rices anonymes ne peuvent que lire le tchat, et ne peuvent pas envoyer
|
||||
de messages.\n"
|
||||
livechat_configuration_channel_mute_anonymous_label: Silencier les utilisateur⋅rices
|
||||
anonymes
|
||||
muted_anonymous_message: Seuls les utilisateur⋅rices enregistré⋅es peuvent envoyer
|
||||
des messages.
|
||||
token_date: Date
|
||||
chat_terms_label: Conditions d'utilisation
|
||||
invalid_value_too_long: Valeur trop longue
|
||||
chat_terms_description: "Ces conditions d'utilisation seront affichées à tous les
|
||||
utilisateur⋅rices lorsqu'iels rejoindront les salons de discussion.\nLes streameur⋅euses
|
||||
peuvent également configurer des conditions d'utilisation pour leurs canaux, qui
|
||||
seront affichées juste après les conditions de l'instance.\n"
|
||||
livechat_configuration_channel_terms_label: Conditions d'utilisation tchat de la chaîne
|
||||
livechat_configuration_channel_terms_desc: "Vous pouvez configurer un message de \"\
|
||||
conditions d'utilisation\" qui sera affiché aux utilisateur⋅rices qui rejoignent
|
||||
vos salons de discussion.\n"
|
||||
|
||||
|
||||
new_poll: Créer un nouveau sondage
|
||||
poll_title: Nouveau sondage
|
||||
poll_instructions: Complétez et soumettez ce formulaire pour créer un nouveau sondage.
|
||||
Ceci mettra fin au sondage précédent le cas échéant.
|
||||
poll_question: Question
|
||||
poll_duration: Durée du sondage (en minutes)
|
||||
poll_anonymous_results: Résultats anonymes
|
||||
poll_choice_n: 'Choix {{N}} :'
|
||||
poll_end: 'Fin du sondage :'
|
||||
poll_vote_instructions: "Pour voter, cliquez sur votre choix, ou envoyez un message
|
||||
avec un point d'exclamation suivi de votre choix (Exemple: !1)."
|
||||
poll_is_over: Ce sondage est à présent terminé.
|
||||
poll_choice_invalid: Ce choix n'est pas valide.
|
||||
poll_anonymous_vote_ok: Votre vote a été pris en compte. Les votes sont anonymes,
|
||||
ils ne seront pas montrés aux autres participant⋅es.
|
||||
poll_vote_ok: Votre vote a été pris en compte, les compteurs seront mis à jour dans
|
||||
un instant.
|
||||
poll_vote_instructions_xmpp: Envoyez un message avec un point d'exclamation suivi
|
||||
du numéro de votre choix pour voter. Exemple : !1
|
||||
poll: Sondage
|
||||
livechat_configuration_channel_moderation_delay_desc: "Valeur par défaut du délai
|
||||
de modération :\n<ul>\n <li>0 : délai de modération désactivé</li>\n <li>Tout
|
||||
nombre entier positif : les messages seront retardés de X secondes pour les participant⋅es
|
||||
non modérateur⋅rices, ce qui permet à ces derniers de supprimer les messages avant
|
||||
qu'un utilisateur⋅rice ne puisse le lire.</li>\n</ul>\n"
|
||||
moderation_delay: Délai de modération
|
||||
|
@ -274,4 +274,3 @@ action_export: Izvezi
|
||||
share_chat_embed: Ugradi
|
||||
token_label: Oznaka
|
||||
auth_description: "<h3>Autentifikacija</h3>\n"
|
||||
chat_terms_label: Uvjeti i odredbe
|
||||
|
@ -229,4 +229,3 @@ external_auth_custom_oidc_description: "チャットへのログインに、外
|
||||
次のドキュメントを参照してください:\n<a href=\"https://livingston.frama.io/peertube-plugin-livechat/ja/documentation/admin/settings/\"\
|
||||
\ target=\"_blank\">設定</a>ページ\n"
|
||||
external_auth_custom_oidc_button_label_description: このラベルは、OIDCプロバイダーを使用して認証するボタン名としてユーザーに表示されます。
|
||||
copied: コピーしました
|
||||
|
@ -33,42 +33,3 @@ help_builtin_prosody_label: Serwer Prosody
|
||||
disable_websocket_label: Wyłącz Websocket
|
||||
prosody_port_label: Port Prosody
|
||||
prosody_certificates_dir_label: Folder certyfikatów
|
||||
poll_title: Nowa ankieta
|
||||
theming_advanced_description: <h3>Zmiana motywu</h3>
|
||||
livechat_configuration_channel_command_cmd_label: Polecenie
|
||||
livechat_configuration_channel_command_message_label: Wiadomość
|
||||
livechat_configuration_channel_title: Opcje kanałów
|
||||
tasks: Zadania
|
||||
promote: Zostań moderatorem
|
||||
external_auth_facebook_oidc_label: Użyj Facebooka
|
||||
login_external_auth_alert_message: Uwierzytelnianie nie powiodło się
|
||||
cancel: Anuluj
|
||||
menu_configuration_label: Czaty
|
||||
successfully_saved: Pomyślnie zapisano
|
||||
livechat_configuration_channel_bot_nickname: Pseudonim bota
|
||||
livechat_configuration_channel_forbidden_words_comments_label: Komentarze
|
||||
invalid_value: Nieprawidłowa wartość.
|
||||
livechat_configuration_channel_slow_mode_label: Tryb spowolniony
|
||||
action_export: Eksportuj
|
||||
livechat_configuration_channel_command_label: Polecenie bota
|
||||
token_label: Etykieta
|
||||
livechat_emojis_file: Plik
|
||||
livechat_emojis_shortname: Skrócona nazwa
|
||||
save: Zapisz
|
||||
avatar_set_option_bird: Ptaki
|
||||
external_auth_google_oidc_label: Użyj Google
|
||||
task_name: Nazwa zadania
|
||||
avatar_set_option_cat: Koty
|
||||
task_description: Opis
|
||||
task_delete: Usuń zadanie
|
||||
livechat_configuration_channel_forbidden_words_reason_label: Powód
|
||||
livechat_configuration_channel_forbidden_words_label_label: Etykieta
|
||||
copied: Skopiowano
|
||||
autocolors_label: Automatyczne wykrywanie kolorów
|
||||
livechat_configuration_channel_quote_label2: Wiadomości
|
||||
action_import: Importuj
|
||||
auth_description: "<h3>Uwierzytelnianie</h3>\n"
|
||||
poll: Ankieta
|
||||
poll_question: Pytanie
|
||||
poll_anonymous_results: Anonimowe wyniki
|
||||
poll_choice_n: 'Wybór {{N}}:'
|
||||
|
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "peertube-plugin-livechat",
|
||||
"version": "10.3.2",
|
||||
"version": "10.1.2",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "peertube-plugin-livechat",
|
||||
"version": "10.3.2",
|
||||
"version": "10.1.2",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@xmpp/jid": "^0.13.1",
|
||||
|
14
package.json
14
package.json
@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "peertube-plugin-livechat",
|
||||
"description": "PeerTube plugin livechat: create chat rooms for your Peertube lives! Comes with many features: federation, moderation tools, chat bot, chat persistence, OBS integration, ...",
|
||||
"version": "10.3.2",
|
||||
"description": "NCTV fork of the peertube-plugin-livechat plugin, containing styling and other shit. This will be maintained with upstream.",
|
||||
"version": "10.1.2",
|
||||
"license": "AGPL-3.0",
|
||||
"author": {
|
||||
"name": "John Livingston",
|
||||
"url": "https://github.com/JohnXLivingston"
|
||||
"name": "Matty Boombalatty",
|
||||
"url": "https://gitea.nicecrew.digital/matty"
|
||||
},
|
||||
"bugs": "https://github.com/JohnXLivingston/peertube-plugin-livechat/issues",
|
||||
"bugs": "https://gitea.nicecrew.digital/matty/peertube-plugin-livechat/issues",
|
||||
"clientScripts": [
|
||||
{
|
||||
"script": "dist/client/common-client-plugin.js",
|
||||
@ -82,8 +82,8 @@
|
||||
"engines": {
|
||||
"npm": ">=7"
|
||||
},
|
||||
"homepage": "https://livingston.frama.io/peertube-plugin-livechat/",
|
||||
"repository": "github:JohnXLivingston/peertube-plugin-livechat",
|
||||
"homepage": "https://nicecrew.tv",
|
||||
"repository": "https://gitea.nicecrew.digital/matty/peertube-plugin-livechat",
|
||||
"keywords": [
|
||||
"peertube",
|
||||
"plugin"
|
||||
|
@ -14,9 +14,6 @@ local get_room_from_jid = rawget(mod_muc, "get_room_from_jid");
|
||||
|
||||
module:depends"http";
|
||||
|
||||
local mod_muc_peertubelivechat_terms = module:depends"muc_peertubelivechat_terms";
|
||||
local set_muc_terms = rawget(mod_muc_peertubelivechat_terms, "set_muc_terms");
|
||||
|
||||
function check_auth(routes)
|
||||
local function check_request_auth(event)
|
||||
local apikey = module:get_option_string("peertubelivechat_manage_rooms_apikey", "")
|
||||
@ -97,17 +94,6 @@ local function update_room(event)
|
||||
must104 = true;
|
||||
end
|
||||
end
|
||||
if type(config.moderation_delay) == "number" then
|
||||
if room._data.moderation_delay ~= config.moderation_delay then
|
||||
room._data.moderation_delay = config.moderation_delay;
|
||||
end
|
||||
end
|
||||
if (type(config.livechat_muc_terms) == "string") then
|
||||
-- to easily detect if the value is given or not, we consider that the caller passes "" when terms must be deleted.
|
||||
if set_muc_terms then
|
||||
set_muc_terms(room, config.livechat_muc_terms or nil);
|
||||
end
|
||||
end
|
||||
if type(config.removeAffiliationsFor) == "table" then
|
||||
-- array of jids
|
||||
for _, jid in ipairs(config.removeAffiliationsFor) do
|
||||
|
@ -4,16 +4,9 @@
|
||||
-- SPDX-License-Identifier: MIT
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
--
|
||||
-- This version contains a modification to take into account new config options:
|
||||
-- * "slow_mode_duration"
|
||||
-- * "mute_anonymous"
|
||||
-- * "moderation_delay"
|
||||
-- These options are introduced in the Peertube livechat plugin.
|
||||
--
|
||||
-- The "slow_mode_duration" comes with mod_muc_slow_mode.
|
||||
-- There will be a XEP proposal for this one. When done, these modifications will be submitted to the mod_muc_http_defaults maintainer.
|
||||
--
|
||||
-- The "moderation_delay" comes with mod_muc_moderation_delay
|
||||
-- This version contains a modification to take into account new config option "slow_mode_duration".
|
||||
-- This option is introduced in the Peertube livechat plugin, by mod_muc_slow_mode.
|
||||
-- There will be a XEP proposal. When done, these modifications will be submitted to the mod_muc_http_defaults maintainer.
|
||||
--
|
||||
|
||||
local http = require "net.http";
|
||||
@ -119,18 +112,7 @@ local function apply_config(room, settings)
|
||||
|
||||
-- specific to peertube-plugin-livechat:
|
||||
if (type(config.slow_mode_duration) == "number") and config.slow_mode_duration >= 0 then
|
||||
room._data.slow_mode_duration = config.slow_mode_duration;
|
||||
end
|
||||
if (type(config.moderation_delay) == "number") and config.moderation_delay >= 0 then
|
||||
room._data.moderation_delay = config.moderation_delay;
|
||||
end
|
||||
if (type(config.mute_anonymous) == "boolean") then
|
||||
room._data.x_peertubelivechat_mute_anonymous = config.mute_anonymous;
|
||||
end
|
||||
if (type(config.livechat_muc_terms) == "string") then
|
||||
-- we don't need to use set_muc_terms here, as this is called for a newly created room
|
||||
-- (and thus we don't need to broadcast changes)
|
||||
room._data.livechat_muc_terms = config.livechat_muc_terms;
|
||||
room._data.slow_mode_duration = config.slow_mode_duration;
|
||||
end
|
||||
elseif config ~= nil then
|
||||
module:log("error", "Invalid config returned from API for %s: %q", room.jid, config);
|
||||
|
@ -1,44 +0,0 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
# mod_muc_moderation_delay
|
||||
|
||||
With this module, you can apply a delay to groupchat messages delivery, so that room moderators can moderate them before other participants receives them.
|
||||
|
||||
This module is part of peertube-plugin-livechat, and is under the same LICENSE.
|
||||
This module can work on any Prosody server (version >= 0.12.x).
|
||||
|
||||
## Configuration
|
||||
|
||||
Just enable the module on your MUC component.
|
||||
The feature will be accessible throught the room configuration form.
|
||||
|
||||
The position in the room config form can be changed be setting the option `moderation_delay_form_position`.
|
||||
This value will be passed as priority for the "muc-config-form" hook.
|
||||
By default, the field will be between muc#roomconfig_changesubject and muc#roomconfig_moderatedroom.
|
||||
|
||||
``` lua
|
||||
VirtualHost "muc.example.com"
|
||||
modules_enabled = { "muc_moderation_delay" }
|
||||
moderation_delay_form_position = 96
|
||||
```
|
||||
|
||||
## Additional notes
|
||||
|
||||
For moderators, messages that are delayed will contain an extra `moderation-delay` xml tag, with `delay` and `waiting` attribute:
|
||||
|
||||
```xml
|
||||
<message xmlns="jabber:client" type="groupchat" id="18821520-e49b-4e59-b6c6-b45cc133905d" to="root@example.com/QH1H89H1" xml:lang="en" from="8df24108-6e70-4fc8-b1cc-f2db7fcdd535@room.example.com/root">
|
||||
<body>Hello world</body>
|
||||
<origin-id id="18821520-e49b-4e59-b6c6-b45cc133905d" xmlns="urn:xmpp:sid:0" />
|
||||
<markable xmlns="urn:xmpp:chat-markers:0" />
|
||||
<occupant-id id="V5gJudj4Ii3+LnikqUbSSH3NmPKO82zD+m7jRYushVY=" xmlns="urn:xmpp:occupant-id:0" />
|
||||
<stanza-id xmlns="urn:xmpp:sid:0" id="xkf36aYefSmQ9evPo1m6Neei" by="8df24108-6e70-4fc8-b1cc-f2db7fcdd535@room.example.com" />
|
||||
<moderation-delay delay="4" waiting="1720177157" />
|
||||
</message>
|
||||
```
|
||||
|
||||
Note: the `waiting` attribute is the timestamp at which the message will be broadcasted.
|
||||
|
||||
So compatible xmpp clients can display some information.
|
@ -1,61 +0,0 @@
|
||||
-- SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
-- Getter/Setter
|
||||
local function get_moderation_delay(room)
|
||||
return room._data.moderation_delay or nil;
|
||||
end
|
||||
|
||||
local function set_moderation_delay(room, delay)
|
||||
if delay == 0 then
|
||||
delay = nil;
|
||||
end
|
||||
if delay ~= nil then
|
||||
delay = assert(tonumber(delay), "Moderation delay is not a valid number");
|
||||
if delay < 0 then
|
||||
delay = nil;
|
||||
end
|
||||
end
|
||||
|
||||
if get_moderation_delay(room) == delay then return false; end
|
||||
|
||||
room._data.moderation_delay = delay;
|
||||
return true;
|
||||
end
|
||||
|
||||
-- Discovering support
|
||||
local function add_disco_form(event)
|
||||
table.insert(event.form, {
|
||||
name = "muc#roominfo_moderation_delay";
|
||||
value = "";
|
||||
});
|
||||
event.formdata["muc#roominfo_moderation_delay"] = get_moderation_delay(event.room);
|
||||
end
|
||||
|
||||
|
||||
-- Config form declaration
|
||||
local function add_form_option(event)
|
||||
table.insert(event.form, {
|
||||
name = "muc#roomconfig_moderation_delay";
|
||||
type = "text-single";
|
||||
datatype = "xs:integer";
|
||||
range_min = 0;
|
||||
range_max = 60; -- do not allow too big values, it does not make sense.
|
||||
label = "Moderation delay (0=disabled, any positive integer= messages will be delayed for X seconds for non-moderator participants.)";
|
||||
-- desc = "";
|
||||
value = get_moderation_delay(event.room);
|
||||
});
|
||||
end
|
||||
|
||||
local function config_submitted(event)
|
||||
set_moderation_delay(event.room, event.value);
|
||||
-- no need to 104 status, this feature is invisible for regular participants.
|
||||
end
|
||||
|
||||
return {
|
||||
set_moderation_delay = set_moderation_delay;
|
||||
get_moderation_delay = get_moderation_delay;
|
||||
add_disco_form = add_disco_form;
|
||||
add_form_option = add_form_option;
|
||||
config_submitted = config_submitted;
|
||||
}
|
@ -1,151 +0,0 @@
|
||||
-- SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
local st = require "util.stanza";
|
||||
local timer = require "util.timer";
|
||||
local get_time = require "util.time".now;
|
||||
local get_moderation_delay = module:require("config").get_moderation_delay;
|
||||
|
||||
local muc_util = module:require "muc/util";
|
||||
local valid_roles = muc_util.valid_roles;
|
||||
|
||||
local moderation_delay_tag = "moderation-delay";
|
||||
local xmlns_fasten = "urn:xmpp:fasten:0";
|
||||
local xmlns_moderated_0 = "urn:xmpp:message-moderate:0";
|
||||
local xmlns_retract_0 = "urn:xmpp:message-retract:0";
|
||||
local xmlns_moderated_1 = "urn:xmpp:message-moderate:1";
|
||||
local xmlns_retract_1 = "urn:xmpp:message-retract:1";
|
||||
local xmlns_st_id = "urn:xmpp:sid:0";
|
||||
|
||||
local queued_stanza_id_timers = {};
|
||||
|
||||
-- tests if a stanza is a retractation message.
|
||||
local function is_retractation_for_stanza_id(stanza)
|
||||
-- XEP 0425 was revised in 2023. For now, mod_muc_moderation uses the previous version.
|
||||
-- But we will make the code compatible with both.
|
||||
local apply_to = stanza:get_child("apply-to", xmlns_fasten);
|
||||
if apply_to and apply_to.attr.id then
|
||||
local moderated = apply_to:get_child("moderated", xmlns_moderated_0);
|
||||
if moderated then
|
||||
local retract = moderated:get_child("retract", xmlns_retract_0);
|
||||
if retract then
|
||||
return apply_to.attr.id;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local moderated = stanza:get_child("moderated", xmlns_moderated_1);
|
||||
if moderated then
|
||||
if moderated:get_child("retract", xmlns_retract_1) then
|
||||
return moderated.attr.id;
|
||||
end
|
||||
end
|
||||
|
||||
return nil;
|
||||
end
|
||||
|
||||
-- handler for muc-broadcast-message
|
||||
local function handle_broadcast_message(event)
|
||||
local room, stanza = event.room, event.stanza;
|
||||
local delay = get_moderation_delay(room);
|
||||
if delay == nil then
|
||||
-- feature disabled on the room, go for it.
|
||||
return;
|
||||
end
|
||||
|
||||
-- only delay groupchat messages with body.
|
||||
if stanza.attr.type ~= "groupchat" then
|
||||
return;
|
||||
end
|
||||
|
||||
-- detect retractations:
|
||||
local retracted_stanza_id = is_retractation_for_stanza_id(stanza);
|
||||
if retracted_stanza_id then
|
||||
module:log("debug", "Got a retractation message for %s", retracted_stanza_id);
|
||||
if queued_stanza_id_timers[retracted_stanza_id] then
|
||||
module:log("info", "Got a retractation message, for message %s that is currently waiting for broadcast. Cancelling.", retracted_stanza_id);
|
||||
timer.stop(queued_stanza_id_timers[retracted_stanza_id]);
|
||||
queued_stanza_id_timers[retracted_stanza_id] = nil;
|
||||
-- and we continue...
|
||||
end
|
||||
end
|
||||
|
||||
if not stanza:get_child("body") then
|
||||
-- Dont want to delay message without body.
|
||||
-- This is usually messages like "xxx is typing", or any other service message.
|
||||
-- This also should concern retractation messages.
|
||||
-- Clients that will receive retractation messages for message they never got, should just drop them. And that's ok.
|
||||
return;
|
||||
end
|
||||
|
||||
local stanza_id = nil; -- message stanza id... can be nil!
|
||||
local stanza_id_child = stanza:get_child("stanza-id", xmlns_st_id);
|
||||
if not stanza_id_child then
|
||||
-- this can happen when muc is not archived!
|
||||
-- in such case, message retractation is not possible.
|
||||
-- so, this is a normal use case, and we should handle it properly.
|
||||
else
|
||||
stanza_id = stanza_id_child.attr.id;
|
||||
end
|
||||
local id = stanza.attr.id;
|
||||
if not id then
|
||||
-- message should alway have an id, but just in case...
|
||||
module:log("warn", "Message has no id, wont delay it.");
|
||||
return;
|
||||
end
|
||||
|
||||
-- Message must be delayed, except for:
|
||||
-- * room moderators
|
||||
-- * the user that sent the message (if they don't get the echo quickly, their clients could have weird behaviours)
|
||||
module:log("debug", "Message %s / %s must be delayed by %i seconds, sending first broadcast wave.", id, stanza_id, delay);
|
||||
local moderator_role_value = valid_roles["moderator"];
|
||||
|
||||
local cloned_stanza = st.clone(stanza); -- we must clone, to send a copy for the second wave.
|
||||
|
||||
-- first of all, if the initiator occupant is not moderator, me must send to them.
|
||||
-- (delaying the echo message could have some quircks in some xmpp clients)
|
||||
if stanza.attr.from then
|
||||
local from_occupant = room:get_occupant_by_nick(stanza.attr.from);
|
||||
if from_occupant and valid_roles[from_occupant.role or "none"] < moderator_role_value then
|
||||
module:log("debug", "Message %s / %s must be sent separatly to it initialior %s.", id, stanza_id, delay, stanza.attr.from);
|
||||
room:route_to_occupant(from_occupant, stanza);
|
||||
end
|
||||
end
|
||||
|
||||
-- adding a tag, so that moderators can know that this message is delayed.
|
||||
stanza:tag(moderation_delay_tag, {
|
||||
delay = "" .. delay;
|
||||
waiting = string.format("%i", get_time() + delay);
|
||||
}):up();
|
||||
|
||||
-- then, sending to moderators (and only moderators):
|
||||
room:broadcast(stanza, function (nick, occupant)
|
||||
if valid_roles[occupant.role or "none"] >= moderator_role_value then
|
||||
return true;
|
||||
end
|
||||
return false;
|
||||
end);
|
||||
|
||||
local task = timer.add_task(delay, function ()
|
||||
module:log("debug", "Message %s has been delayed, sending to remaining participants.", id);
|
||||
room:broadcast(cloned_stanza, function (nick, occupant)
|
||||
if valid_roles[occupant.role or "none"] >= moderator_role_value then
|
||||
return false;
|
||||
end
|
||||
if nick == stanza.attr.from then
|
||||
-- we already sent it to them (because they are moderator, or because we sent them separately)
|
||||
return false;
|
||||
end
|
||||
return true;
|
||||
end);
|
||||
end);
|
||||
if stanza_id then
|
||||
-- store it, so we can stop timer if there is a retractation.
|
||||
queued_stanza_id_timers[stanza_id] = task;
|
||||
end
|
||||
|
||||
return true; -- stop the default broadcast_message processing.
|
||||
end
|
||||
|
||||
return {
|
||||
handle_broadcast_message = handle_broadcast_message;
|
||||
};
|
@ -1,30 +0,0 @@
|
||||
-- mod_muc_moderation_delay
|
||||
--
|
||||
-- SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
--
|
||||
-- This file is AGPL-v3 licensed.
|
||||
-- Please see the Peertube livechat plugin copyright information.
|
||||
-- https://livingston.frama.io/peertube-plugin-livechat/credits/
|
||||
--
|
||||
|
||||
local add_disco_form = module:require("config").add_disco_form;
|
||||
local config_submitted = module:require("config").config_submitted;
|
||||
local add_form_option = module:require("config").add_form_option;
|
||||
local handle_broadcast_message = module:require("delay").handle_broadcast_message;
|
||||
|
||||
-- form_position: the position in the room config form (this value will be passed as priority for the "muc-config-form" hook).
|
||||
-- By default, field will be between muc#roomconfig_changesubject and muc#roomconfig_moderatedroom
|
||||
local form_position = module:get_option_number("moderation_delay_form_position") or 80-2;
|
||||
|
||||
-- Plugin dependencies
|
||||
local mod_muc = module:depends "muc";
|
||||
|
||||
-- muc-disco and muc-config to configure the feature:
|
||||
module:hook("muc-disco#info", add_disco_form);
|
||||
module:hook("muc-config-submitted/muc#roomconfig_moderation_delay", config_submitted);
|
||||
module:hook("muc-config-form", add_form_option, form_position);
|
||||
|
||||
-- intercept muc-broadcast-message, and broadcast with delay if required.
|
||||
-- Priority is negative, as we want it to be the last handler.
|
||||
module:hook("muc-broadcast-message", handle_broadcast_message, -1000);
|
@ -1,30 +0,0 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
# mod_muc_peertubelivechat_roles
|
||||
|
||||
This module is a custom module that handles default roles for users.
|
||||
|
||||
This module is part of peertube-plugin-livechat, and is under the same LICENSE.
|
||||
|
||||
## Features
|
||||
|
||||
### Only registered users can talk
|
||||
|
||||
This feature will set default user roles to 'visitor' for anonymous users.
|
||||
|
||||
The feature is associated to a room configuration field (muc#roomconfig_x_peertubelivechat_mute_anonymous).
|
||||
The default value for this field will be set by mod_muc_http_defaults (which is a custom version of the original module).
|
||||
|
||||
Note: currently, all anonymous users are joining the original Peertube instance.
|
||||
This means we only have to handle anonymous users on the local "anon" virtualhost.
|
||||
|
||||
If anonymous users are muted, the room disco features will include "x_peertubelivechat_mute_anonymous".
|
||||
This is used by the ConverseJs frontend to display a message explaining why the user is muted.
|
||||
|
||||
### Only Peertube channel followers can talk
|
||||
|
||||
This feature will come later.
|
@ -1,84 +0,0 @@
|
||||
-- mod_muc_peertubelivechat_roles
|
||||
--
|
||||
-- SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
--
|
||||
-- This file is AGPL-v3 licensed.
|
||||
-- Please see the Peertube livechat plugin copyright information.
|
||||
-- https://livingston.frama.io/peertube-plugin-livechat/credits/
|
||||
--
|
||||
|
||||
-- To compute the anonymous host, we will simply replace "room." by "anon." in the current module host.
|
||||
-- This part is very peertube-plugin-livechat specific, but that's okay :)
|
||||
local anonymous_host = "@anon." .. module.host:sub(#"^room.");
|
||||
|
||||
local function get_peertubelivechat_mute_anonymous(room)
|
||||
return room._data.x_peertubelivechat_mute_anonymous;
|
||||
end
|
||||
|
||||
local function set_peertubelivechat_mute_anonymous(room, mute_anonymous)
|
||||
mute_anonymous = mute_anonymous and true or nil;
|
||||
if get_peertubelivechat_mute_anonymous(room) == mute_anonymous then return false; end
|
||||
room._data.x_peertubelivechat_mute_anonymous = mute_anonymous;
|
||||
|
||||
local role_to_test;
|
||||
local role_to_set;
|
||||
if (mute_anonymous) then
|
||||
-- mute all anonymous users (with "participant" role)
|
||||
role_to_test = "participant";
|
||||
role_to_set = "visitor";
|
||||
else
|
||||
-- voice all anonymous users (with "visitor" role).
|
||||
role_to_test = "visitor";
|
||||
role_to_set = "participant";
|
||||
end
|
||||
|
||||
for occupant_jid, occupant in room:each_occupant() do
|
||||
if (occupant.bare_jid:sub(-#anonymous_host) == anonymous_host) and occupant.role == role_to_test then
|
||||
room:set_role(true, occupant_jid, role_to_set);
|
||||
end
|
||||
end
|
||||
|
||||
return true;
|
||||
end
|
||||
|
||||
module:hook("muc-disco#info", function(event)
|
||||
if get_peertubelivechat_mute_anonymous(event.room) then
|
||||
event.reply:tag("feature", {var = "x_peertubelivechat_mute_anonymous"}):up();
|
||||
end
|
||||
end);
|
||||
|
||||
module:hook("muc-config-form", function(event)
|
||||
table.insert(event.form, {
|
||||
name = "muc#roomconfig_x_peertubelivechat_mute_anonymous";
|
||||
type = "boolean";
|
||||
label = "Mute anonymous users";
|
||||
desc = "Anonymous users will be muted by default.";
|
||||
value = get_peertubelivechat_mute_anonymous(event.room);
|
||||
});
|
||||
end, 121);
|
||||
|
||||
module:hook("muc-config-submitted/muc#roomconfig_x_peertubelivechat_mute_anonymous", function(event)
|
||||
if set_peertubelivechat_mute_anonymous(event.room, event.value) then
|
||||
event.status_codes["104"] = true;
|
||||
end
|
||||
end);
|
||||
|
||||
-- Note: muc-get-default-role does not get any occupant info.
|
||||
-- So we want use this hook to set default roles.
|
||||
-- We will do something a little hacky...: change the role in a high priority muc-occupant-pre-join hook!
|
||||
module:hook("muc-occupant-pre-join", function(event)
|
||||
local occupant = event.occupant;
|
||||
if occupant.role == "participant" then
|
||||
if get_peertubelivechat_mute_anonymous(event.room) and occupant.bare_jid ~= nil then
|
||||
if (occupant.bare_jid:sub(-#anonymous_host) == anonymous_host) then
|
||||
occupant.role = "visitor";
|
||||
end
|
||||
end
|
||||
end
|
||||
end, 1000);
|
||||
|
||||
return {
|
||||
get = get_peertubelivechat_mute_anonymous;
|
||||
set = set_peertubelivechat_mute_anonymous;
|
||||
};
|
@ -1,34 +0,0 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
# mod_muc_peertubelivechat_terms
|
||||
|
||||
This module is a custom module to handle Terms&Conditions in the livechat Peertube plugin.
|
||||
|
||||
This module is part of peertube-plugin-livechat, and is under the same LICENSE.
|
||||
|
||||
## Features
|
||||
|
||||
When a new occupant session is created for a MUC, this module will send to the user the global terms,
|
||||
and the MUC-specific terms (if defined).
|
||||
|
||||
This is done by sending groupchat messages.
|
||||
These messages will contain a "x-livechat-terms" tag, so that livechat front-end can detect these messages, and display them differently.
|
||||
For standard XMPP clients, these messages will show as standard MUC message coming from a specific nickname.
|
||||
|
||||
## Configuration
|
||||
|
||||
This modules take following options.
|
||||
|
||||
### muc_terms_service_nickname
|
||||
|
||||
The nickname that will be used by service messages.
|
||||
This module reserves the nickname, so than nobody can use it in MUC rooms
|
||||
(we don't want any user to spoof this nickname).
|
||||
|
||||
### muc_terms_global
|
||||
|
||||
The global terms.
|
@ -1,128 +0,0 @@
|
||||
-- mod_muc_peertubelivechat_terms
|
||||
--
|
||||
-- SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
--
|
||||
-- This file is AGPL-v3 licensed.
|
||||
-- Please see the Peertube livechat plugin copyright information.
|
||||
-- https://livingston.frama.io/peertube-plugin-livechat/credits/
|
||||
--
|
||||
|
||||
-- Exposed functions:
|
||||
-- get_muc_terms
|
||||
-- set_muc_terms
|
||||
|
||||
local jid_escape = require "util.jid".escape;
|
||||
local jid_resource = require "util.jid".resource;
|
||||
local st = require "util.stanza";
|
||||
local id = require "util.id";
|
||||
local datetime = require "util.datetime";
|
||||
|
||||
local service_nickname = module:get_option_string("muc_terms_service_nickname", "Service");
|
||||
local global_terms = module:get_option_string("muc_terms_global", "");
|
||||
|
||||
local function create_terms_message(room, type, terms)
|
||||
-- Note: we can't send the message from "room.jid": XMPP clients such as Gajim would ignore them.
|
||||
-- So we use a service_nickname.
|
||||
local from = room.jid .. '/' .. jid_escape(service_nickname);
|
||||
module:log("debug", "Creating %s terms message from %s (room %s)", type, from, room);
|
||||
local msg = st.message({
|
||||
type = "groupchat",
|
||||
from = from,
|
||||
id = id.medium()
|
||||
}, terms)
|
||||
:tag('x-livechat-terms', { type = type }):up() -- adding a custom tag to specify that it is a "terms" message, so that frontend can display it with a special template.
|
||||
:tag("delay", { xmlns = "urn:xmpp:delay", from = from, stamp = datetime.datetime() }):up() -- adding a delay to trick the moderation bot (see below)
|
||||
:tag("no-copy", { xmlns = "urn:xmpp:hints" }):up()
|
||||
:tag("no-store", { xmlns = "urn:xmpp:hints" }):up()
|
||||
:tag("no-permanent-store", { xmlns = "urn:xmpp:hints" }):up();
|
||||
-- concerning the delay tag:
|
||||
-- We are sending message to rooms from non-existant occupants.
|
||||
-- If the message contains something that should be moderated by the livechat moderation bot,
|
||||
-- it could generate some error logs (the bot will not find the user in the occupant list).
|
||||
-- At time of writing, the xmppjs-chat-bot ignore delayed message... so we use this hack to trick the bot.
|
||||
return msg;
|
||||
end
|
||||
|
||||
-- MUC Getter/Setter
|
||||
function get_muc_terms(room)
|
||||
return room._data.livechat_muc_terms or nil;
|
||||
end
|
||||
|
||||
function set_muc_terms(room, terms)
|
||||
if terms == "" then
|
||||
terms = nil;
|
||||
end
|
||||
|
||||
if get_muc_terms(room) == terms then return false; end
|
||||
|
||||
room._data.livechat_muc_terms = terms;
|
||||
if terms ~= nil then
|
||||
-- we must send new terms to all occupants.
|
||||
local msg = create_terms_message(room, "muc", terms);
|
||||
module:log("debug", "Broadcasting terms message to room %s", room);
|
||||
room:broadcast_message(msg);
|
||||
end
|
||||
return true;
|
||||
end
|
||||
|
||||
-- send the terms when joining:
|
||||
local function send_terms(event)
|
||||
local origin = event.origin;
|
||||
local room = event.room;
|
||||
local occupant = event.occupant;
|
||||
if global_terms then
|
||||
module:log("debug", "Sending global terms to %s", occupant.jid);
|
||||
local msg = create_terms_message(room, "global", global_terms);
|
||||
msg.attr.to = occupant.jid;
|
||||
origin.send(msg);
|
||||
end
|
||||
|
||||
local muc_terms = get_muc_terms(room);
|
||||
if muc_terms then
|
||||
local from = room.jid .. '/' .. jid_escape(service_nickname);
|
||||
module:log("debug", "Sending muc terms to %s", occupant.jid);
|
||||
local msg = create_terms_message(room, "muc", muc_terms);
|
||||
msg.attr.to = occupant.jid;
|
||||
origin.send(msg);
|
||||
end
|
||||
end
|
||||
-- Note: we could do that on muc-occupant-joined or muc-occupant-session-new.
|
||||
-- The first will not send it to multiple clients, the second will.
|
||||
-- After some reflexion, i will try muc-occupant-session-new, and see if it works as expected.
|
||||
module:hook("muc-occupant-session-new", send_terms);
|
||||
|
||||
-- reserve the service_nickname:
|
||||
local function enforce_nick_policy(event)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
local requested_nick = jid_resource(stanza.attr.to);
|
||||
local room = event.room;
|
||||
if not room then return; end
|
||||
|
||||
if requested_nick == service_nickname then
|
||||
module:log("debug", "Occupant tried to use the %s reserved nickname, blocking it.", service_nickname);
|
||||
local reply = st.error_reply(stanza, "cancel", "conflict", nil, room.jid):up();
|
||||
origin.send(reply);
|
||||
return true;
|
||||
end
|
||||
end
|
||||
module:hook("muc-occupant-pre-join", enforce_nick_policy);
|
||||
module:hook("muc-occupant-pre-change", enforce_nick_policy);
|
||||
|
||||
-- security check: we must remove all "x-livechat-terms" tag, to be sure nobody tries to spoof terms!
|
||||
module:hook("muc-occupant-groupchat", function(event)
|
||||
event.stanza:maptags(function (child)
|
||||
if child.name == 'x-livechat-terms' then
|
||||
return nil;
|
||||
end
|
||||
return child;
|
||||
end);
|
||||
end, 100);
|
||||
|
||||
-- don't save terms messages in history
|
||||
module:hook("muc-message-is-historic", function(event)
|
||||
local stanza = event.stanza;
|
||||
if (stanza:get_child("x-livechat-terms")) then
|
||||
return false, "hint";
|
||||
end
|
||||
end, 1);
|
@ -1,33 +0,0 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
# mod_muc_slow_pool
|
||||
|
||||
This module provide a way to create polls in MUC rooms.
|
||||
|
||||
This module is part of peertube-plugin-livechat, and is under the same LICENSE.
|
||||
|
||||
There will probably be a XEP proposal for this module behaviour. When done, this module will be published in the prosody-modules repository.
|
||||
|
||||
## Configuration
|
||||
|
||||
Just enable the module on your MUC component.
|
||||
All above configurations are optional.
|
||||
|
||||
## poll_groupchat_votes_priority
|
||||
|
||||
The priority for the hook that will take into account votes.
|
||||
You can change this, if you have some specific hook that should be done after/before counting votes (slow mode, firewall, ...).
|
||||
|
||||
Default: 40 (Prosody checks visitor role with priority of 50, we want this to be after).
|
||||
|
||||
## Strings
|
||||
|
||||
You can change some defaults strings, if you want for example to localize the poll messages.
|
||||
Here are the existing strings and default values:
|
||||
|
||||
* poll_string_over: This poll is now over.
|
||||
* poll_string_vote_instructions: Send a message with an exclamation mark followed by your choice number to vote. Example: !1
|
||||
* poll_string_invalid_choice: This choice is not valid.
|
||||
* poll_string_anonymous_vote_ok: Your vote is taken into account. Votes are anonymous, they will not be shown to other participants.
|
@ -1,17 +0,0 @@
|
||||
-- SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
-- FIXME: create a XEP to standardize this, and remove the "x-".
|
||||
local xmlns_poll = "http://jabber.org/protocol/muc#x-poll";
|
||||
local xmlns_poll_message = "http://jabber.org/protocol/muc#x-poll-message";
|
||||
local poll_message_tag = "x-poll";
|
||||
local poll_question_tag = "x-poll-question";
|
||||
local poll_choice_tag = "x-poll-choice";
|
||||
|
||||
return {
|
||||
xmlns_poll = xmlns_poll;
|
||||
xmlns_poll_message = xmlns_poll_message;
|
||||
poll_message_tag = poll_message_tag;
|
||||
poll_question_tag = poll_question_tag;
|
||||
poll_choice_tag = poll_choice_tag;
|
||||
};
|
@ -1,153 +0,0 @@
|
||||
-- SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
local st = require "util.stanza";
|
||||
local dataform = require "util.dataforms";
|
||||
local get_form_type = require "util.dataforms".get_type;
|
||||
local xmlns_poll = module:require("constants").xmlns_poll;
|
||||
|
||||
local end_current_poll = module:require("poll").end_current_poll;
|
||||
local create_poll = module:require("poll").create_poll;
|
||||
|
||||
local function get_form_layout(room, stanza)
|
||||
local form = dataform.new({
|
||||
title = "New poll",
|
||||
instructions = "Complete and submit this form to create a new poll. This will end and replace any existing poll.",
|
||||
{
|
||||
name = "FORM_TYPE";
|
||||
type = "hidden";
|
||||
value = xmlns_poll;
|
||||
}
|
||||
});
|
||||
|
||||
table.insert(form, {
|
||||
name = "muc#roompoll_question";
|
||||
type = "text-single";
|
||||
label = "Question";
|
||||
desc = "The poll question.";
|
||||
value = "";
|
||||
required = true;
|
||||
});
|
||||
table.insert(form, {
|
||||
name = "muc#roompoll_duration";
|
||||
type = "text-single";
|
||||
datatype = "xs:integer";
|
||||
range_min = 1;
|
||||
label = "Poll duration (in minutes)";
|
||||
desc = "The number of minutes to run the poll.";
|
||||
value = "";
|
||||
required = true;
|
||||
});
|
||||
table.insert(form, {
|
||||
name = "muc#roompoll_anonymous";
|
||||
type = "boolean";
|
||||
label = "Anonymous results";
|
||||
desc = "By enabling this, user's votes won't be publicly shown in the room.";
|
||||
value = true;
|
||||
});
|
||||
table.insert(form, {
|
||||
name = "muc#roompoll_choice1";
|
||||
type = "text-single";
|
||||
label = "Choice 1";
|
||||
desc = "";
|
||||
value = "";
|
||||
required = true;
|
||||
});
|
||||
table.insert(form, {
|
||||
name = "muc#roompoll_choice2";
|
||||
type = "text-single";
|
||||
label = "Choice 2";
|
||||
desc = "";
|
||||
value = "";
|
||||
required = true;
|
||||
});
|
||||
table.insert(form, {
|
||||
name = "muc#roompoll_choice3";
|
||||
type = "text-single";
|
||||
label = "Choice 3";
|
||||
desc = "";
|
||||
value = "";
|
||||
});
|
||||
table.insert(form, {
|
||||
name = "muc#roompoll_choice4";
|
||||
type = "text-single";
|
||||
label = "Choice 4";
|
||||
desc = "";
|
||||
value = "";
|
||||
});
|
||||
table.insert(form, {
|
||||
name = "muc#roompoll_choice5";
|
||||
type = "text-single";
|
||||
label = "Choice 5";
|
||||
desc = "";
|
||||
value = "";
|
||||
});
|
||||
|
||||
return form;
|
||||
end
|
||||
|
||||
local function send_form(room, origin, stanza)
|
||||
module:log("debug", "Sending the poll form");
|
||||
origin.send(st.reply(stanza):query(xmlns_poll)
|
||||
:add_child(get_form_layout(room, stanza.attr.from):form())
|
||||
) ;
|
||||
end
|
||||
|
||||
local function dataform_error_message(err)
|
||||
local out = {};
|
||||
for field, errmsg in pairs(err) do
|
||||
table.insert(out, ("%s: %s"):format(field, errmsg))
|
||||
end
|
||||
return table.concat(out, "; ");
|
||||
end
|
||||
|
||||
|
||||
local function process_form(room, origin, stanza, occupant)
|
||||
if not stanza.tags[1] then
|
||||
origin.send(st.error_reply(stanza, "modify", "bad-request"));
|
||||
return true;
|
||||
end
|
||||
local form = stanza.tags[1]:get_child("x", "jabber:x:data");
|
||||
if not form then
|
||||
origin.send(st.error_reply(stanza, "modify", "bad-request"));
|
||||
return true;
|
||||
end
|
||||
|
||||
local form_type, err = get_form_type(form);
|
||||
if not form_type then
|
||||
origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid dataform: "..err));
|
||||
return true;
|
||||
elseif form_type ~= xmlns_poll then
|
||||
origin.send(st.error_reply(stanza, "modify", "bad-request", "Unexpected FORM_TYPE, expected '"..xmlns_poll.."'"));
|
||||
return true;
|
||||
end
|
||||
|
||||
if form.attr.type == "cancel" then
|
||||
origin.send(st.reply(stanza));
|
||||
return true;
|
||||
elseif form.attr.type ~= "submit" then
|
||||
origin.send(st.error_reply(stanza, "cancel", "bad-request", "Not a submitted form"));
|
||||
return true;
|
||||
end
|
||||
|
||||
-- form submitted
|
||||
local fields, errors, present = get_form_layout(room, stanza.attr.from):data(form);
|
||||
if errors then
|
||||
origin.send(st.error_reply(stanza, "modify", "bad-request", dataform_error_message(errors)));
|
||||
return true;
|
||||
end
|
||||
|
||||
-- stop any poll that is already here
|
||||
end_current_poll(room);
|
||||
|
||||
-- create the new poll
|
||||
create_poll(room, fields, occupant);
|
||||
|
||||
origin.send(st.reply(stanza));
|
||||
return true;
|
||||
end
|
||||
|
||||
return {
|
||||
send_form = send_form;
|
||||
process_form = process_form;
|
||||
};
|
@ -1,232 +0,0 @@
|
||||
-- SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
local id = require "util.id";
|
||||
local st = require "util.stanza";
|
||||
local timer = require "util.timer";
|
||||
local xmlns_occupant_id = "urn:xmpp:occupant-id:0";
|
||||
local xmlns_replace = "urn:xmpp:message-correct:0";
|
||||
local xmlns_poll_message = module:require("constants").xmlns_poll_message;
|
||||
local poll_message_tag = module:require("constants").poll_message_tag;
|
||||
local poll_question_tag = module:require("constants").poll_question_tag;
|
||||
local poll_choice_tag = module:require("constants").poll_choice_tag;
|
||||
|
||||
local mod_muc = module:depends"muc";
|
||||
local get_room_from_jid = mod_muc.get_room_from_jid;
|
||||
|
||||
local debounce_delay = 5; -- number of seconds during which we must group votes to avoid flood.
|
||||
local scheduled_updates = {};
|
||||
|
||||
local string_poll_over = module:get_option_string("poll_string_over") or "This poll is now over.";
|
||||
local string_poll_vote_instructions = module:get_option_string("poll_string_vote_instructions") or "Send a message with an exclamation mark followed by your choice number to vote. Example: !1";
|
||||
|
||||
-- Build the content for poll start and end messages (that will go to the message <body>)
|
||||
local function build_poll_message_content(room, is_end_message)
|
||||
local current_poll = room._data.current_poll;
|
||||
if not current_poll then
|
||||
return nil;
|
||||
end
|
||||
|
||||
local content = current_poll["muc#roompoll_question"] .. "\n";
|
||||
|
||||
if is_end_message then
|
||||
content = content .. string_poll_over .. "\n";
|
||||
end
|
||||
|
||||
local total = 0;
|
||||
for choice, nb in pairs(current_poll.votes_by_choices) do
|
||||
total = total + nb;
|
||||
end
|
||||
for _, choice_desc in ipairs(current_poll.choices_ordered) do
|
||||
local choice, label = choice_desc.number, choice_desc.label;
|
||||
content = content .. choice .. ': ' .. label;
|
||||
-- if vote over, and at least 1 vote, we add the results.
|
||||
if is_end_message and total > 0 then
|
||||
local nb = current_poll.votes_by_choices[choice] or 0;
|
||||
local percent = string.format("%.2f", nb * 100 / total);
|
||||
content = content .. " (" .. nb .. "/" .. total .. " = " .. percent .. "%)";
|
||||
end
|
||||
content = content .. "\n";
|
||||
end
|
||||
|
||||
if not is_end_message then
|
||||
content = content .. string_poll_vote_instructions .. "\n";
|
||||
end
|
||||
|
||||
return content;
|
||||
end
|
||||
|
||||
-- construct the poll message stanza.
|
||||
-- Note: content can be nil, for updates messages.
|
||||
local function build_poll_message(room, content)
|
||||
local current_poll = room._data.current_poll;
|
||||
if not current_poll then
|
||||
return nil;
|
||||
end
|
||||
|
||||
local from = current_poll.occupant_nick; -- this is in fact room.jid/nickname
|
||||
|
||||
local msg = st.message({
|
||||
type = "groupchat",
|
||||
from = from,
|
||||
id = id.long()
|
||||
}, content);
|
||||
|
||||
msg:tag("occupant-id", {
|
||||
xmlns = xmlns_occupant_id,
|
||||
id = current_poll.occupant_id
|
||||
}):up();
|
||||
|
||||
if content == nil then
|
||||
-- No content, this is an update message.
|
||||
-- Adding some hints (XEP-0334):
|
||||
msg:tag("no-copy", { xmlns = "urn:xmpp:hints" }):up();
|
||||
msg:tag("no-store", { xmlns = "urn:xmpp:hints" }):up();
|
||||
msg:tag("no-permanent-store", { xmlns = "urn:xmpp:hints" }):up();
|
||||
end
|
||||
|
||||
-- now we must add some custom XML data, so that compatible clients can display the poll as they want:
|
||||
-- <x-poll xmlns="http://jabber.org/protocol/muc#x-poll-message" id="I9UWyoxsz4BN" votes="1" end="1719842224" over="">
|
||||
-- <x-poll-question>Poll question</x-poll-question>
|
||||
-- <x-poll-choice choice="1" votes="0">Choice 1 label</x-poll-choice>
|
||||
-- <x-poll-choice choice="2" votes="1">Choice 2 label</x-poll-choice>
|
||||
-- <x-poll-choice choice="3" votes="0">Choice 3 label</x-poll-choice>
|
||||
-- <x-poll-choice choice="4" votes="0">Choice 4 label</x-poll-choice>
|
||||
-- </x-poll>
|
||||
local total = 0;
|
||||
for choice, nb in pairs(current_poll.votes_by_choices) do
|
||||
total = total + nb;
|
||||
end
|
||||
|
||||
local message_attrs = {
|
||||
xmlns = xmlns_poll_message,
|
||||
id = current_poll.poll_id,
|
||||
votes = "" .. total
|
||||
};
|
||||
message_attrs["end"] = string.format("%i", current_poll.end_timestamp);
|
||||
if current_poll.already_ended then
|
||||
message_attrs["over"] = "";
|
||||
end
|
||||
msg:tag(poll_message_tag, message_attrs):text_tag(poll_question_tag, current_poll["muc#roompoll_question"], {});
|
||||
for _, choice_desc in ipairs(current_poll.choices_ordered) do
|
||||
local choice, label = choice_desc.number, choice_desc.label;
|
||||
local nb = current_poll.votes_by_choices[choice] or 0;
|
||||
total = total + nb;
|
||||
msg:text_tag(poll_choice_tag, label, {
|
||||
votes = "" .. nb,
|
||||
choice = choice
|
||||
});
|
||||
end
|
||||
msg:up();
|
||||
|
||||
return msg;
|
||||
end
|
||||
|
||||
-- sends a message when the poll starts.
|
||||
local function poll_start_message(room)
|
||||
if not room._data.current_poll then
|
||||
return nil;
|
||||
end
|
||||
module:log("debug", "Sending the start message for room %s poll", room.jid);
|
||||
local content = build_poll_message_content(room, false);
|
||||
local msg = build_poll_message(room, content);
|
||||
room:broadcast_message(msg);
|
||||
end
|
||||
|
||||
-- Send the poll update message
|
||||
local function send_poll_update_message(room)
|
||||
if not room._data.current_poll then
|
||||
return nil;
|
||||
end
|
||||
if room._data.current_poll.already_ended then
|
||||
module:log("debug", "Cancelling the update message for room %s poll, because already_ended==true.", room.jid);
|
||||
return nil;
|
||||
end
|
||||
|
||||
module:log("debug", "Sending an update message for room %s poll", room.jid);
|
||||
local msg = build_poll_message(room, nil);
|
||||
|
||||
room:broadcast_message(msg);
|
||||
end
|
||||
|
||||
-- Schedule an update of the start message.
|
||||
-- We do not send this update each time someone vote,
|
||||
-- to avoid flooding.
|
||||
local function schedule_poll_update_message(room_jid)
|
||||
if scheduled_updates[room_jid] then
|
||||
-- already a running timer, we can ignore to debounce.
|
||||
return;
|
||||
end
|
||||
scheduled_updates[room_jid] = timer.add_task(debounce_delay, function()
|
||||
scheduled_updates[room_jid] = nil;
|
||||
-- We dont pass room, because here it could have been removed from memory.
|
||||
-- So we must relad the room from the JID in any case.
|
||||
local room = get_room_from_jid(room_jid);
|
||||
if not room then
|
||||
return;
|
||||
end
|
||||
send_poll_update_message(room);
|
||||
end);
|
||||
end
|
||||
|
||||
-- Send a new message when the poll is over, with the result.
|
||||
local function poll_end_message(room)
|
||||
if not room._data.current_poll then
|
||||
return nil;
|
||||
end
|
||||
module:log("debug", "Sending the end message for room %s poll", room.jid);
|
||||
if scheduled_updates[room.jid] then
|
||||
module:log("debug", "Cancelling an update message for the poll %s", room.jid);
|
||||
timer.stop(scheduled_updates[room.jid]);
|
||||
scheduled_updates[room.jid] = nil;
|
||||
end
|
||||
local content = build_poll_message_content(room, true);
|
||||
local msg = build_poll_message(room, content);
|
||||
room:broadcast_message(msg);
|
||||
end
|
||||
|
||||
-- security check: we must remove all specific tags, to be sure nobody tries to spoof polls!
|
||||
local function remove_specific_tags_from_groupchat(event)
|
||||
event.stanza:maptags(function (child)
|
||||
if child.name == poll_message_tag then
|
||||
return nil;
|
||||
end
|
||||
if child.name == poll_question_tag then
|
||||
return nil;
|
||||
end
|
||||
if child.name == poll_choice_tag then
|
||||
return nil;
|
||||
end
|
||||
return child;
|
||||
end);
|
||||
end
|
||||
|
||||
-- when a new session is opened, we must send the current poll to the client
|
||||
local function handle_new_occupant_session(event)
|
||||
local room = event.room;
|
||||
local occupant = event.occupant;
|
||||
local origin = event.origin;
|
||||
if not occupant then
|
||||
return;
|
||||
end
|
||||
if not room._data.current_poll then
|
||||
return;
|
||||
end
|
||||
if room._data.current_poll.already_ended then
|
||||
return;
|
||||
end
|
||||
|
||||
-- Sending an update message to the new occupant.
|
||||
module:log("debug", "Sending a poll update message to new occupant %s", occupant.jid);
|
||||
local msg = build_poll_message(room, nil);
|
||||
msg.attr.to = occupant.jid;
|
||||
origin.send(msg);
|
||||
end
|
||||
|
||||
return {
|
||||
poll_start_message = poll_start_message;
|
||||
poll_end_message = poll_end_message;
|
||||
schedule_poll_update_message = schedule_poll_update_message;
|
||||
remove_specific_tags_from_groupchat = remove_specific_tags_from_groupchat;
|
||||
handle_new_occupant_session = handle_new_occupant_session;
|
||||
};
|
@ -1,99 +0,0 @@
|
||||
-- mod_muc_poll
|
||||
--
|
||||
-- SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
--
|
||||
-- This file is AGPL-v3 licensed.
|
||||
-- Please see the Peertube livechat plugin copyright information.
|
||||
-- https://livingston.frama.io/peertube-plugin-livechat/credits/
|
||||
--
|
||||
-- Implements: XEP-????: MUC Poll (XEP to come).
|
||||
|
||||
local st = require "util.stanza";
|
||||
local jid_bare = require "util.jid".bare;
|
||||
|
||||
local mod_muc = module:depends"muc";
|
||||
local get_room_from_jid = mod_muc.get_room_from_jid;
|
||||
|
||||
local xmlns_poll = module:require("constants").xmlns_poll;
|
||||
local send_form = module:require("form").send_form;
|
||||
local process_form = module:require("form").process_form;
|
||||
local handle_groupchat = module:require("poll").handle_groupchat;
|
||||
local remove_specific_tags_from_groupchat = module:require("message").remove_specific_tags_from_groupchat;
|
||||
local handle_new_occupant_session = module:require("message").handle_new_occupant_session;
|
||||
local room_restored = module:require("poll").room_restored;
|
||||
|
||||
local poll_groupchat_votes_priority = module:get_option_number("poll_groupchat_votes_priority") or 40;
|
||||
|
||||
|
||||
-- new poll creation, get form
|
||||
module:hook("iq-get/bare/" .. xmlns_poll .. ":query", function (event)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
local room_jid = stanza.attr.to;
|
||||
module:log("debug", "Received a request for the poll form");
|
||||
local room = get_room_from_jid(room_jid);
|
||||
if not room then
|
||||
origin.send(st.error_reply(stanza, "cancel", "item-not-found"));
|
||||
return true;
|
||||
end
|
||||
local from = jid_bare(stanza.attr.from);
|
||||
|
||||
local from_affiliation = room:get_affiliation(from);
|
||||
if (from_affiliation ~= "owner" and from_affiliation ~= "admin") then
|
||||
origin.send(st.error_reply(stanza, "auth", "forbidden"))
|
||||
return true;
|
||||
end
|
||||
|
||||
send_form(room, origin, stanza);
|
||||
return true;
|
||||
end);
|
||||
|
||||
-- new poll creation, form submission
|
||||
module:hook("iq-set/bare/" .. xmlns_poll .. ":query", function (event)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
local room_jid = stanza.attr.to;
|
||||
local from = stanza.attr.from;
|
||||
module:log("debug", "Received a form submission for the poll form on %s from %s", room_jid, from);
|
||||
local room = get_room_from_jid(room_jid);
|
||||
if not room then
|
||||
origin.send(st.error_reply(stanza, "cancel", "item-not-found"));
|
||||
return true;
|
||||
end
|
||||
|
||||
local occupant = room:get_occupant_by_real_jid(from);
|
||||
if not occupant then
|
||||
module:log("debug", "No occupant, ignoring...");
|
||||
origin.send(st.error_reply(stanza, "auth", "forbidden"))
|
||||
return true;
|
||||
end
|
||||
|
||||
local from_bare = jid_bare(stanza.attr.from);
|
||||
local from_affiliation = room:get_affiliation(from_bare);
|
||||
if (from_affiliation ~= "owner" and from_affiliation ~= "admin") then
|
||||
origin.send(st.error_reply(stanza, "auth", "forbidden"))
|
||||
return true;
|
||||
end
|
||||
|
||||
return process_form(room, origin, stanza, occupant);
|
||||
end);
|
||||
|
||||
-- Discovering support
|
||||
module:hook("muc-disco#info", function (event)
|
||||
event.reply:tag("feature", { var = xmlns_poll }):up();
|
||||
end);
|
||||
|
||||
-- On groupchat messages, we check if this is a vote for the current poll.
|
||||
-- Note: we use a high priority, so it will be handled before the slow mode.
|
||||
module:hook("muc-occupant-groupchat", handle_groupchat, poll_groupchat_votes_priority);
|
||||
|
||||
-- security check: we must remove all specific tags, to be sure nobody tries to spoof polls!
|
||||
module:hook("muc-occupant-groupchat", remove_specific_tags_from_groupchat, 1000);
|
||||
|
||||
-- when a room is restored (after a server restart for example),
|
||||
-- we must resume any current poll
|
||||
module:hook("muc-room-restored", room_restored);
|
||||
|
||||
-- when a new session is opened, we must send the current poll to the client
|
||||
-- Note: it should be in the MAM. But it is easier for clients to ignore delayed messages
|
||||
-- when displaying polls (to ignore old polls).
|
||||
module:hook("muc-occupant-session-new", handle_new_occupant_session, 10); -- must be after subject (20, see Prosody code)
|
@ -1,247 +0,0 @@
|
||||
-- SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
local id = require "util.id";
|
||||
local st = require "util.stanza";
|
||||
local get_time = require "util.time".now;
|
||||
local timer = require "util.timer";
|
||||
local mod_muc = module:depends"muc";
|
||||
local get_room_from_jid = mod_muc.get_room_from_jid;
|
||||
local poll_start_message = module:require("message").poll_start_message;
|
||||
local poll_end_message = module:require("message").poll_end_message;
|
||||
local schedule_poll_update_message = module:require("message").schedule_poll_update_message;
|
||||
|
||||
local string_poll_invalid_choice = module:get_option_string("poll_string_invalid_choice") or "This choice is not valid.";
|
||||
local string_poll_anonymous_vote_ok = module:get_option_string("poll_string_anonymous_vote_ok") or "Your vote is taken into account. Votes are anonymous, they will not be shown to other participants.";
|
||||
local string_poll_over = module:get_option_string("poll_string_over") or "This poll is now over.";
|
||||
|
||||
local scheduled_end = {};
|
||||
|
||||
local function schedule_poll_purge(room_jid)
|
||||
module:log("debug", "Scheduling a purge for poll %s", room_jid);
|
||||
timer.add_task(30, function ()
|
||||
module:log("info", "Must purge poll for room %s", room_jid);
|
||||
-- We dont pass room, because here it could have been removed from memory.
|
||||
-- So we must relad the room from the JID in any case.
|
||||
local room = get_room_from_jid(room_jid);
|
||||
if not room then
|
||||
module:log("debug", "Room %s not found, was maybe destroyed", room_jid);
|
||||
return;
|
||||
end
|
||||
-- we must check if the poll is ended (could be a new poll!)
|
||||
if not room._data.current_poll then
|
||||
module:log("debug", "Room %s has no current poll to purge", room_jid);
|
||||
return;
|
||||
end
|
||||
if not room._data.current_poll.already_ended then
|
||||
module:log("debug", "Room %s has has a poll that is not ended, must be a new one", room_jid);
|
||||
return;
|
||||
end
|
||||
module:log("info", "Purging poll for room %s", room_jid);
|
||||
room._data.current_poll = nil;
|
||||
end);
|
||||
end
|
||||
|
||||
local function end_current_poll (room)
|
||||
if not room._data.current_poll then
|
||||
return;
|
||||
end
|
||||
|
||||
if room._data.current_poll.already_ended then
|
||||
-- this can happen if the server was restarted before the purge
|
||||
schedule_poll_purge(room.jid);
|
||||
return;
|
||||
end
|
||||
|
||||
module:log("debug", "Ending the current poll for room %s", room.jid);
|
||||
room._data.current_poll.already_ended = true;
|
||||
|
||||
if scheduled_end[room.jid] then
|
||||
module:log("debug", "Stopping the end timer for the poll");
|
||||
timer.stop(scheduled_end[room_jid]);
|
||||
scheduled_end[room_jid] = nil;
|
||||
end
|
||||
|
||||
poll_end_message(room);
|
||||
|
||||
-- TODO: store the result somewhere, to keep track?
|
||||
|
||||
-- We don't remove the poll immediatly. Indeed, if the vote is anonymous,
|
||||
-- we don't want to expose votes from late users.
|
||||
schedule_poll_purge(room.jid);
|
||||
end
|
||||
|
||||
local function schedule_poll_end (room_jid, timestamp)
|
||||
local delay = timestamp - get_time();
|
||||
if delay <= 0 then
|
||||
delay = 1;
|
||||
end
|
||||
module:log("debug", "Must schedule a poll end in %i for room %s", delay, room_jid);
|
||||
|
||||
if scheduled_end[room_jid] then
|
||||
module:log("debug", "There is already a timer for the %s poll end, rescheduling", room_jid);
|
||||
timer.reschedule(scheduled_end[room_jid], delay);
|
||||
return;
|
||||
end
|
||||
scheduled_end[room_jid] = timer.add_task(delay, function ()
|
||||
module:log("debug", "Its time to end the poll for room %s", room_jid);
|
||||
scheduled_end[room_jid] = nil;
|
||||
-- We dont pass room, because here it could have been removed from memory.
|
||||
-- So we must relad the room from the JID in any case.
|
||||
local room = get_room_from_jid(room_jid);
|
||||
if not room then
|
||||
module:log("debug", "Room %s not found, was probably destroyed", room_jid);
|
||||
return; -- room was probably destroyed
|
||||
end
|
||||
end_current_poll(room);
|
||||
end);
|
||||
end
|
||||
|
||||
local function create_poll(room, fields, occupant)
|
||||
module:log("debug", "Creating a new poll for room %s, by %s", room.jid, occupant.bare_jid);
|
||||
room._data.current_poll = fields;
|
||||
room._data.current_poll.poll_id = id.short();
|
||||
room._data.current_poll.end_timestamp = get_time() + (60 * fields["muc#roompoll_duration"]);
|
||||
room._data.current_poll.votes_by_occupant = {};
|
||||
room._data.current_poll.votes_by_choices = {};
|
||||
room._data.current_poll.choices_ordered = {}; -- choices labels with numerical index, so we can have correct order
|
||||
room._data.current_poll.already_ended = false;
|
||||
room._data.current_poll.occupant_bare_jid = occupant.bare_jid;
|
||||
room._data.current_poll.occupant_nick = occupant.nick;
|
||||
room._data.current_poll.occupant_id = room:get_occupant_id(occupant);
|
||||
for field, _ in pairs(fields) do
|
||||
local c = field:match("^muc#roompoll_choice(%d+)$");
|
||||
if c then
|
||||
if fields["muc#roompoll_choice" .. c]:find("%S") then
|
||||
room._data.current_poll.votes_by_choices[c] = 0;
|
||||
table.insert(room._data.current_poll.choices_ordered, {
|
||||
number = c;
|
||||
label = fields["muc#roompoll_choice" .. c];
|
||||
});
|
||||
end
|
||||
end
|
||||
end
|
||||
table.sort(room._data.current_poll.choices_ordered, function(a, b)
|
||||
if a.number == b.number then
|
||||
return 0;
|
||||
end
|
||||
return tonumber(a.number) < tonumber(b.number);
|
||||
end);
|
||||
|
||||
poll_start_message(room);
|
||||
schedule_poll_end(room.jid, room._data.current_poll.end_timestamp);
|
||||
end
|
||||
|
||||
local function handle_groupchat(event)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
local room = event.room;
|
||||
if not room._data.current_poll then
|
||||
return;
|
||||
end
|
||||
|
||||
-- There is a current poll. Is this a vote?
|
||||
local body = stanza:get_child_text("body")
|
||||
if not body or #body < 1 then
|
||||
return;
|
||||
end
|
||||
local choice = body:match("^%s*!(%d+)%s*$");
|
||||
if not choice then
|
||||
return;
|
||||
end
|
||||
|
||||
-- Ok, seems it is a vote.
|
||||
|
||||
if get_time() >= room._data.current_poll.end_timestamp then
|
||||
module:log("debug", "Got a vote for a finished poll, not counting it.");
|
||||
-- Note: we keep bouncing messages a few seconds/minutes after the poll end
|
||||
-- to be sure any user that send the vote too late won't expose his choice.
|
||||
origin.send(st.error_reply(
|
||||
stanza,
|
||||
-- error_type = 'cancel' (see descriptions in RFC 6120 https://xmpp.org/rfcs/rfc6120.html#stanzas-error-syntax)
|
||||
"cancel",
|
||||
-- error_condition = 'not-allowed' (see RFC 6120 Defined Error Conditions https://xmpp.org/rfcs/rfc6120.html#stanzas-error-conditions)
|
||||
"not-allowed",
|
||||
string_poll_over
|
||||
));
|
||||
return true; -- stop!
|
||||
end
|
||||
|
||||
-- We must check that the choice is valid:
|
||||
if room._data.current_poll.votes_by_choices[choice] == nil then
|
||||
module:log("debug", "Invalid vote, bouncing it.");
|
||||
origin.send(st.error_reply(
|
||||
stanza,
|
||||
-- error_type = 'cancel' (see descriptions in RFC 6120 https://xmpp.org/rfcs/rfc6120.html#stanzas-error-syntax)
|
||||
"cancel",
|
||||
-- error_condition = 'not-allowed' (see RFC 6120 Defined Error Conditions https://xmpp.org/rfcs/rfc6120.html#stanzas-error-conditions)
|
||||
"bad-request",
|
||||
string_poll_invalid_choice
|
||||
));
|
||||
return true; -- stop!
|
||||
end
|
||||
|
||||
-- Ok, we can count the vote.
|
||||
local occupant = event.occupant;
|
||||
if not occupant then
|
||||
module:log("warn", "No occupant in the event, dont know how to count the vote");
|
||||
return
|
||||
end
|
||||
|
||||
local occupant_bare_id = occupant.bare_jid;
|
||||
module:log("debug", "Counting a new vote for room %s: choice %i, voter %s", room.jid, choice, occupant_bare_id);
|
||||
-- counting the vote:
|
||||
if room._data.current_poll.votes_by_occupant[occupant_bare_id] ~= nil then
|
||||
module:log("debug", "Occupant %s has already voted for current room %s vote, reassigning his vote.", occupant_bare_id, room.jid);
|
||||
room._data.current_poll.votes_by_choices[room._data.current_poll.votes_by_occupant[occupant_bare_id]] = room._data.current_poll.votes_by_choices[room._data.current_poll.votes_by_occupant[occupant_bare_id]] - 1;
|
||||
end
|
||||
room._data.current_poll.votes_by_choices[choice] = room._data.current_poll.votes_by_choices[choice] + 1;
|
||||
room._data.current_poll.votes_by_occupant[occupant_bare_id] = choice;
|
||||
|
||||
schedule_poll_update_message(room.jid);
|
||||
|
||||
-- When the poll is anonymous, we bounce the messages (but count the votes).
|
||||
local must_bounce = room._data.current_poll["muc#roompoll_anonymous"] == true;
|
||||
if must_bounce then
|
||||
module:log("debug", "Anonymous votes, bouncing it.");
|
||||
origin.send(st.error_reply(
|
||||
stanza,
|
||||
-- error_type
|
||||
"continue",
|
||||
-- error_condition
|
||||
"undefined-condition",
|
||||
string_poll_anonymous_vote_ok
|
||||
));
|
||||
return true; -- stop!
|
||||
end
|
||||
end
|
||||
|
||||
local function room_restored(event)
|
||||
local room = event.room;
|
||||
if not room._data.current_poll then
|
||||
return;
|
||||
end
|
||||
|
||||
module:log("info", "Restoring room %s with current ongoing poll.", room.jid);
|
||||
local now = get_time();
|
||||
if now >= room._data.current_poll.end_timestamp then
|
||||
module:log("info", "Current poll is over for room %s, ending it", room.jid);
|
||||
end_current_poll(room);
|
||||
return;
|
||||
end
|
||||
|
||||
if scheduled_end[room.jid] then
|
||||
module:log("info", "Poll for room %s is not finished yet, the end is still scheduled", room.jid);
|
||||
else
|
||||
module:log("info", "Poll for room %s is not finished yet, rescheduling the end", room.jid);
|
||||
schedule_poll_end(room.jid, room._data.current_poll.end_timestamp);
|
||||
end
|
||||
-- just in case, we can also reschedule an update message
|
||||
schedule_poll_update_message(room.jid);
|
||||
end
|
||||
|
||||
return {
|
||||
end_current_poll = end_current_poll;
|
||||
create_poll = create_poll;
|
||||
handle_groupchat = handle_groupchat;
|
||||
room_restored = room_restored;
|
||||
};
|
@ -329,7 +329,7 @@ module:hook("muc-room-destroyed", function(event)
|
||||
end);
|
||||
|
||||
-- When a user lose its admin/owner affilation, and is still subscribed to the node,
|
||||
-- we must unsubscribe them.
|
||||
-- we must unsubscribe him.
|
||||
module:hook("muc-set-affiliation", function(event)
|
||||
local previous_affiliation = event.previous_affiliation;
|
||||
local new_affiliation = event.affiliation;
|
||||
@ -374,7 +374,7 @@ module:hook("muc-occupant-left", function (event)
|
||||
|
||||
module:log(
|
||||
"debug",
|
||||
"Occupant %q has left room %q, we must unsubscribe them for pubsub nodes.",
|
||||
"Occupant %q has left room %q, we must unsubscribe him/her for pubsub nodes.",
|
||||
occupant.bare_jid, room_jid
|
||||
);
|
||||
for node in pairs(service.nodes) do
|
||||
|
@ -119,7 +119,6 @@ async function initChannelConfiguration (options: RegisterServerOptions): Promis
|
||||
// FIXME: this piece of code should not be in this file (nothing to do with initChannelConfiguration,
|
||||
// but will be more efficient to add here, as we already tested hasChat).
|
||||
// Note: no need to await here, would only degrade performances.
|
||||
// FIXME: should also update livechat_muc_terms if channel has changed.
|
||||
updateProsodyRoom(options, video.uuid, {
|
||||
name: video.name
|
||||
}).then(
|
||||
|
@ -4,7 +4,6 @@
|
||||
|
||||
import type { RegisterServerOptions } from '@peertube/peertube-types'
|
||||
import type { ChannelConfigurationOptions } from '../../../../shared/lib/types'
|
||||
import { channelTermsMaxLength } from '../../../../shared/lib/constants'
|
||||
|
||||
/**
|
||||
* Sanitize data so that they can safely be used/stored for channel configuration configuration.
|
||||
@ -36,27 +35,6 @@ async function sanitizeChannelConfigurationOptions (
|
||||
throw new Error('Invalid data.slowMode data type')
|
||||
}
|
||||
|
||||
const moderationData = data.moderation ?? {} // comes with livechat 10.3.0
|
||||
moderationData.delay ??= 0
|
||||
|
||||
// mute not present in livechat <= 10.2.0
|
||||
const mute = data.mute ?? {}
|
||||
mute.anonymous ??= false
|
||||
|
||||
if (typeof mute !== 'object') {
|
||||
throw new Error('Invalid data.mute data type')
|
||||
}
|
||||
|
||||
// terms not present in livechat <= 10.2.0
|
||||
let terms = data.terms
|
||||
if (terms !== undefined && (typeof terms !== 'string')) {
|
||||
throw new Error('Invalid data.terms data type')
|
||||
}
|
||||
if (terms && terms.length > channelTermsMaxLength) {
|
||||
throw new Error('data.terms value too long')
|
||||
}
|
||||
if (terms === '') { terms = undefined }
|
||||
|
||||
const result: ChannelConfigurationOptions = {
|
||||
bot: {
|
||||
enabled: _readBoolean(botData, 'enabled'),
|
||||
@ -68,17 +46,8 @@ async function sanitizeChannelConfigurationOptions (
|
||||
},
|
||||
slowMode: {
|
||||
duration: _readInteger(slowModeData, 'duration', 0, 1000)
|
||||
},
|
||||
mute: {
|
||||
anonymous: _readBoolean(mute, 'anonymous')
|
||||
},
|
||||
moderation: {
|
||||
delay: _readInteger(moderationData, 'delay', 0, 60)
|
||||
}
|
||||
}
|
||||
if (terms !== undefined) {
|
||||
result.terms = terms
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
@ -49,14 +49,7 @@ function getDefaultChannelConfigurationOptions (_options: RegisterServerOptions)
|
||||
},
|
||||
slowMode: {
|
||||
duration: 0
|
||||
},
|
||||
mute: {
|
||||
anonymous: false
|
||||
},
|
||||
moderation: {
|
||||
delay: 0
|
||||
},
|
||||
terms: undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -141,7 +141,7 @@ export class Emojis {
|
||||
* @param sn short name
|
||||
*/
|
||||
public validShortName (sn: any): boolean {
|
||||
if ((typeof sn !== 'string') || !/^:?[\w-]+:?$/.test(sn)) {
|
||||
if ((typeof sn !== 'string') || !/^:[\w-]+:$/.test(sn)) {
|
||||
this.logger.debug('Short name invalid: ' + (typeof sn === 'string' ? sn : '???'))
|
||||
return false
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ const got = require('got')
|
||||
* - server B: server that follows ours (or used to watch V, without following A)
|
||||
* - user from B connect to the B XMPP server
|
||||
* - server B has server A connection informations (got it using ActivityPub)
|
||||
* - but, when using Websocket S2S, server A needs information from B, that it never receives
|
||||
* - but, when using Websocket S2S, server A needs information from B, that he never receives
|
||||
*
|
||||
* Indeed, the XMPP S2S dialback mecanism will try to connect back to
|
||||
* server A, and transmit a secret key, to ensure that all incomming connection
|
||||
|
@ -17,10 +17,6 @@ const locContent: Map<string, string> = new Map<string, string>()
|
||||
* - We are using keys to identify strings
|
||||
* - the `loc` function gets the english segment for the key
|
||||
* - the build-languages.js script builds all needed files.
|
||||
*
|
||||
* The loc function is also used to customize some labels on Prosody backend.
|
||||
* The front-end will then replace english strings by their translation.
|
||||
* (see mod_muc_poll for example).
|
||||
* @param key The key to translate
|
||||
*/
|
||||
function loc (key: string): string {
|
||||
|
@ -41,7 +41,7 @@ function getCheckConfigurationChannelMiddleware (options: RegisterServerOptions)
|
||||
} 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 they has no right.')
|
||||
logger.warn('Current user tries to access a channel for which he has no right.')
|
||||
res.sendStatus(403)
|
||||
return
|
||||
}
|
||||
|
@ -64,8 +64,6 @@ async function updateProsodyRoom (
|
||||
data: {
|
||||
name?: string
|
||||
slow_mode_duration?: number
|
||||
moderation_delay?: number
|
||||
livechat_muc_terms?: string
|
||||
addAffiliations?: Affiliations
|
||||
removeAffiliationsFor?: string[]
|
||||
}
|
||||
@ -94,12 +92,6 @@ async function updateProsodyRoom (
|
||||
if (('slow_mode_duration' in data) && data.slow_mode_duration !== undefined) {
|
||||
apiData.slow_mode_duration = data.slow_mode_duration
|
||||
}
|
||||
if (('moderation_delay' in data) && data.moderation_delay !== undefined) {
|
||||
apiData.moderation_delay = data.moderation_delay
|
||||
}
|
||||
if ('livechat_muc_terms' in data) {
|
||||
apiData.livechat_muc_terms = data.livechat_muc_terms ?? ''
|
||||
}
|
||||
if (('addAffiliations' in data) && data.addAffiliations !== undefined) {
|
||||
apiData.addAffiliations = data.addAffiliations
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ let singleton: LivechatProsodyAuth | undefined
|
||||
*
|
||||
* The livechat tokens password are encrypted in data files.
|
||||
* The associated secret key is in the database.
|
||||
* This is to ensure an additional security level: if an attacker has access to file system, they also must have access
|
||||
* This is to ensure an additional security level: if an attacker has access to file system, he also must have access
|
||||
* to DB to get the secret key and decrypt passwords.
|
||||
*/
|
||||
export class LivechatProsodyAuth {
|
||||
|
@ -85,7 +85,7 @@ async function getProsodyFilePaths (options: RegisterServerOptions): Promise<Pro
|
||||
if (!(await fs.promises.stat(settings['prosody-certificates-dir'] as string)).isDirectory()) {
|
||||
// We can throw an exception here...
|
||||
// Because if the user input a wrong directory, the plugin will not register,
|
||||
// and they will never be able to fix the conf.
|
||||
// and he will never be able to fix the conf
|
||||
logger.error('Certificate directory does not exist or is not a directory')
|
||||
certsDir = undefined
|
||||
} else {
|
||||
@ -175,8 +175,7 @@ async function getProsodyConfig (options: RegisterServerOptionsV5): Promise<Pros
|
||||
'chat-no-anonymous',
|
||||
'auto-ban-anonymous-ip',
|
||||
'federation-dont-publish-remotely',
|
||||
'disable-channel-configuration',
|
||||
'chat-terms'
|
||||
'disable-channel-configuration'
|
||||
])
|
||||
|
||||
const valuesToHideInDiagnostic = new Map<string, string>()
|
||||
@ -200,9 +199,6 @@ async function getProsodyConfig (options: RegisterServerOptionsV5): Promise<Pros
|
||||
let certificates: ProsodyConfigCertificates = false
|
||||
const useBots = !settings['disable-channel-configuration']
|
||||
const bots: ProsodyConfig['bots'] = {}
|
||||
const chatTerms = (typeof settings['chat-terms'] === 'string') && settings['chat-terms']
|
||||
? settings['chat-terms']
|
||||
: undefined
|
||||
|
||||
let useExternal: boolean = false
|
||||
try {
|
||||
@ -264,7 +260,7 @@ async function getProsodyConfig (options: RegisterServerOptionsV5): Promise<Pros
|
||||
const roomApiUrl = baseApiUrl + 'room?apikey=' + apikey + '&jid={room.jid|jid_node}'
|
||||
const testApiUrl = baseApiUrl + 'test?apikey=' + apikey
|
||||
|
||||
const config = new ProsodyConfigContent(paths, prosodyDomain, chatTerms)
|
||||
const config = new ProsodyConfigContent(paths, prosodyDomain)
|
||||
if (!disableAnon) {
|
||||
config.useAnonymous(autoBanIP)
|
||||
}
|
||||
@ -366,8 +362,6 @@ async function getProsodyConfig (options: RegisterServerOptionsV5): Promise<Pros
|
||||
}
|
||||
}
|
||||
|
||||
config.usePoll()
|
||||
|
||||
config.useTestModule(apikey, testApiUrl)
|
||||
|
||||
const debugMucAdminJids = debugMucAdmins(options)
|
||||
|
@ -5,33 +5,9 @@
|
||||
import type { ProsodyFilePaths } from './paths'
|
||||
import type { ExternalComponent } from './components'
|
||||
import { BotConfiguration } from '../../configuration/bot'
|
||||
import { loc } from '../../loc'
|
||||
import { userInfo } from 'os'
|
||||
|
||||
/**
|
||||
* Use this class to construct a string that will be writen as a multiline Lua string.
|
||||
*/
|
||||
class ConfigEntryValueMultiLineString extends String {
|
||||
public serialize (): string {
|
||||
const s = this.toString()
|
||||
let i = 0
|
||||
// Lua multiline strings can be escaped by [[ ]], or [==[ ]==] with any number of =
|
||||
// http://lua-users.org/wiki/StringsTutorial
|
||||
// So, to have a proper value, we will check if the string contains [[ or ]],
|
||||
// and try again by adding "=" until we do not found the pattern.
|
||||
while (true) {
|
||||
const opening = '[' + '='.repeat(i) + '['
|
||||
const closing = ']' + '='.repeat(i) + ']'
|
||||
if (!s.includes(opening) && !s.includes(closing)) {
|
||||
break
|
||||
}
|
||||
i++
|
||||
}
|
||||
return '[' + '='.repeat(i) + '[' + s + ']' + '='.repeat(i) + ']'
|
||||
}
|
||||
}
|
||||
|
||||
type ConfigEntryValue = boolean | number | string | ConfigEntryValueMultiLineString | ConfigEntryValue[]
|
||||
type ConfigEntryValue = boolean | number | string | ConfigEntryValue[]
|
||||
|
||||
type ConfigEntries = Map<string, ConfigEntryValue>
|
||||
|
||||
@ -57,9 +33,6 @@ type ConfigLogExpiration =
|
||||
ConfigLogExpirationNever | ConfigLogExpirationPeriod | ConfigLogExpirationSeconds | ConfigLogExpirationError
|
||||
|
||||
function writeValue (value: ConfigEntryValue): string {
|
||||
if (value instanceof ConfigEntryValueMultiLineString) {
|
||||
return value.serialize() + ';\n'
|
||||
}
|
||||
if (typeof value === 'boolean') {
|
||||
return value.toString() + ';\n'
|
||||
}
|
||||
@ -178,7 +151,7 @@ class ProsodyConfigContent {
|
||||
log: string
|
||||
prosodyDomain: string
|
||||
|
||||
constructor (paths: ProsodyFilePaths, prosodyDomain: string, chatTerms?: string) {
|
||||
constructor (paths: ProsodyFilePaths, prosodyDomain: string) {
|
||||
this.paths = paths
|
||||
this.global = new ProsodyConfigGlobal()
|
||||
this.log = ''
|
||||
@ -239,19 +212,8 @@ class ProsodyConfigContent {
|
||||
this.muc.set('muc_room_default_history_length', 20)
|
||||
|
||||
this.muc.add('modules_enabled', 'muc_slow_mode')
|
||||
this.muc.add('slow_mode_duration_form_position', 120)
|
||||
|
||||
this.muc.add('modules_enabled', 'pubsub_peertubelivechat')
|
||||
this.muc.add('modules_enabled', 'muc_peertubelivechat_roles')
|
||||
|
||||
this.muc.add('modules_enabled', 'muc_peertubelivechat_terms')
|
||||
this.muc.set('muc_terms_service_nickname', 'Peertube')
|
||||
if (chatTerms) {
|
||||
this.muc.set('muc_terms_global', new ConfigEntryValueMultiLineString(chatTerms))
|
||||
}
|
||||
|
||||
this.muc.add('modules_enabled', 'muc_moderation_delay')
|
||||
this.muc.add('moderation_delay_form_position', 118)
|
||||
this.muc.add('slow_mode_duration_form_position', 120)
|
||||
}
|
||||
|
||||
useAnonymous (autoBanIP: boolean): void {
|
||||
@ -415,7 +377,7 @@ class ProsodyConfigContent {
|
||||
// Using direct S2S for outgoing connection can be an issue, if the local instance dont allow incomming S2S.
|
||||
// Indeed, the remote instance will not necessarely be able to discover the Websocket Endpoint.
|
||||
// To be sure the remote instance knows the websocket endpoint, we must use Websocket for the firt outgoing connect.
|
||||
// So, we will add a parameter for mod_s2s_peertubelivechat, to tell them not to use outgoin s2s connection.
|
||||
// So, we will add a parameter for mod_s2s_peertubelivechat, to tell him not to use outgoint s2s connection.
|
||||
this.global.set('s2s_peertubelivechat_no_outgoing_directs2s_to_peertube', s2sPort === null)
|
||||
|
||||
this.muc.add('modules_enabled', 'dialback') // This allows s2s connections without certicicates!
|
||||
@ -534,17 +496,6 @@ class ProsodyConfigContent {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable the poll feature.
|
||||
*/
|
||||
usePoll (): void {
|
||||
this.muc.add('modules_enabled', 'muc_poll')
|
||||
this.muc.set('poll_string_over', loc('poll_is_over'))
|
||||
this.muc.set('poll_string_invalid_choice', loc('poll_choice_invalid'))
|
||||
this.muc.set('poll_string_anonymous_vote_ok', loc('poll_anonymous_vote_ok'))
|
||||
this.muc.set('poll_string_vote_instructions', loc('poll_vote_instructions_xmpp'))
|
||||
}
|
||||
|
||||
addMucAdmins (jids: string[]): void {
|
||||
for (const jid of jids) {
|
||||
this.muc.add('admins', jid)
|
||||
|
@ -5,7 +5,7 @@
|
||||
import type { RegisterServerOptions } from '@peertube/peertube-types'
|
||||
import type { RoomConf } from 'xmppjs-chat-bot'
|
||||
import { getProsodyDomain } from '../prosody/config/domain'
|
||||
import { listProsodyRooms, updateProsodyRoom } from '../prosody/api/manage-rooms'
|
||||
import { listProsodyRooms } from '../prosody/api/manage-rooms'
|
||||
import { getChannelInfosById } from '../database/channel'
|
||||
import { ChannelConfigurationOptions } from '../../../shared/lib/types'
|
||||
import {
|
||||
@ -108,7 +108,7 @@ class RoomChannel {
|
||||
* @return Returns true if the data where found and valid. If there is no data (or no valid data), returns false.
|
||||
*/
|
||||
public async readData (): Promise<boolean> {
|
||||
// Reading the data file (see https://livingston.frama.io/peertube-plugin-livechat/technical/data/)
|
||||
// Reading the data file (see https://livingston.frama.io/peertube-plugin-livechat/fr/technical/data/)
|
||||
|
||||
let content: string
|
||||
try {
|
||||
@ -292,7 +292,10 @@ class RoomChannel {
|
||||
this.logger.info('Syncing...')
|
||||
this.isWriting = true
|
||||
|
||||
const prosodyRoomUpdates = new Map<string, Parameters<typeof updateProsodyRoom>[2]>()
|
||||
// Note 2024-05-15: slow_mode_duration becomes a default value.
|
||||
// We dont need prosodyRoomUpdates anymore. but keeping the code commented,
|
||||
// if we want to enable again a similar sync.
|
||||
// const prosodyRoomUpdates = new Map<string, Parameters<typeof updateProsodyRoom>[2]>()
|
||||
|
||||
try {
|
||||
const data = this._serializeData() // must be atomic
|
||||
@ -352,12 +355,12 @@ class RoomChannel {
|
||||
|
||||
await BotConfiguration.singleton().updateRoom(roomJID, botConf)
|
||||
|
||||
// Now we also must update some room metadata on Prosody side (livechat_muc_terms, ...)
|
||||
// This can be done without waiting for the API call to finish, but we don't want to send thousands of
|
||||
// API calls at the same time. So storing data in a map, and we well launch it sequentially at the end
|
||||
prosodyRoomUpdates.set(roomJID, {
|
||||
livechat_muc_terms: channelConfigurationOptions.terms ?? '' // must pass a string, else wont update
|
||||
})
|
||||
// // Now we also must update some room metadata (slow mode duration, ...)
|
||||
// // This can be done without waiting for the API call to finish, but we don't want to send thousands of
|
||||
// // API calls at the same time. So storing data in a map, and we well launch it sequentially at the end
|
||||
// prosodyRoomUpdates.set(roomJID, {
|
||||
// slow_mode_duration: channelConfigurationOptions.slowMode.duration
|
||||
// })
|
||||
|
||||
this.roomConfToUpdate.delete(roomJID)
|
||||
}
|
||||
@ -371,22 +374,22 @@ class RoomChannel {
|
||||
this.isWriting = false
|
||||
}
|
||||
|
||||
if (prosodyRoomUpdates.size) {
|
||||
// Here we don't have to wait.
|
||||
// If it fails (for example because we are turning off prosody), it is not a big deal.
|
||||
// Does not worth the cost to wait.
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
setTimeout(async () => {
|
||||
this.logger.info('Syncing done, but still some data to send to Prosody')
|
||||
for (const [roomJID, data] of prosodyRoomUpdates.entries()) {
|
||||
try {
|
||||
await updateProsodyRoom(this.options, roomJID, data)
|
||||
} catch (err) {
|
||||
this.logger.error(`Failed updating prosody room info: "${err as string}".`)
|
||||
}
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
// if (prosodyRoomUpdates.size) {
|
||||
// // Here we don't have to wait.
|
||||
// // If it fails (for example because we are turning off prosody), it is not a big deal.
|
||||
// // Does not worth the cost to wait.
|
||||
// // eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
// setTimeout(async () => {
|
||||
// this.logger.info('Syncing done, but still some data to send to Prosody')
|
||||
// for (const [roomJID, data] of prosodyRoomUpdates.entries()) {
|
||||
// try {
|
||||
// await updateProsodyRoom(this.options, roomJID, data)
|
||||
// } catch (err) {
|
||||
// this.logger.error(`Failed updating prosody room info: "${err as string}".`)
|
||||
// }
|
||||
// }
|
||||
// }, 0)
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -20,7 +20,7 @@ async function initPromoteApiRouter (options: RegisterServerOptions, router: Rou
|
||||
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 they has no right.')
|
||||
logger.warn('Current user tries to access the promote API for which he has no right.')
|
||||
res.sendStatus(403)
|
||||
return
|
||||
}
|
||||
|
@ -36,26 +36,14 @@ interface RoomDefaults {
|
||||
|
||||
// Following fields are specific to livechat (for now), and requires a customized version for mod_muc_http_defaults.
|
||||
slow_mode_duration?: number
|
||||
mute_anonymous?: boolean
|
||||
livechat_muc_terms?: string
|
||||
moderation_delay?: number
|
||||
}
|
||||
affiliations?: Affiliations
|
||||
}
|
||||
|
||||
async function _getChannelSpecificOptions (
|
||||
options: RegisterServerOptions,
|
||||
channelId: number
|
||||
): Promise<Partial<RoomDefaults['config']>> {
|
||||
async function slowModeDuration (options: RegisterServerOptions, channelId: number): Promise<number> {
|
||||
const channelOptions = await getChannelConfigurationOptions(options, channelId) ??
|
||||
getDefaultChannelConfigurationOptions(options)
|
||||
|
||||
return {
|
||||
slow_mode_duration: channelOptions.slowMode.duration,
|
||||
mute_anonymous: channelOptions.mute.anonymous,
|
||||
livechat_muc_terms: channelOptions.terms,
|
||||
moderation_delay: channelOptions.moderation.delay
|
||||
}
|
||||
return channelOptions.slowMode.duration
|
||||
}
|
||||
|
||||
/**
|
||||
@ -101,14 +89,12 @@ async function initRoomApiRouter (options: RegisterServerOptions, router: Router
|
||||
}
|
||||
|
||||
const roomDefaults: RoomDefaults = {
|
||||
config: Object.assign(
|
||||
{
|
||||
name: channelInfos.displayName,
|
||||
description: ''
|
||||
// subject: channelInfos.displayName
|
||||
},
|
||||
await _getChannelSpecificOptions(options, channelId)
|
||||
),
|
||||
config: {
|
||||
name: channelInfos.displayName,
|
||||
description: '',
|
||||
// subject: channelInfos.displayName
|
||||
slow_mode_duration: await slowModeDuration(options, channelId)
|
||||
},
|
||||
affiliations: affiliations
|
||||
}
|
||||
|
||||
@ -155,15 +141,13 @@ async function initRoomApiRouter (options: RegisterServerOptions, router: Router
|
||||
}
|
||||
|
||||
const roomDefaults: RoomDefaults = {
|
||||
config: Object.assign(
|
||||
{
|
||||
name: video.name,
|
||||
description: '',
|
||||
language: video.language
|
||||
// subject: video.name
|
||||
},
|
||||
await _getChannelSpecificOptions(options, video.channelId)
|
||||
),
|
||||
config: {
|
||||
name: video.name,
|
||||
description: '',
|
||||
language: video.language,
|
||||
// subject: video.name
|
||||
slow_mode_duration: await slowModeDuration(options, video.channelId)
|
||||
},
|
||||
affiliations: affiliations
|
||||
}
|
||||
|
||||
|
@ -148,14 +148,6 @@ function initChatSettings ({ registerSetting }: RegisterServerOptions): void {
|
||||
private: true,
|
||||
descriptionHTML: loc('chat_title')
|
||||
})
|
||||
registerSetting({
|
||||
name: 'chat-terms',
|
||||
private: true,
|
||||
label: loc('chat_terms_label'),
|
||||
type: 'input-textarea',
|
||||
default: '',
|
||||
descriptionHTML: loc('chat_terms_description')
|
||||
})
|
||||
registerSetting({
|
||||
name: 'prosody-list-rooms',
|
||||
label: loc('list_rooms_label'),
|
||||
|
@ -1,5 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
export const channelTermsMaxLength = 400
|
@ -101,15 +101,6 @@ interface ChannelConfigurationOptions {
|
||||
slowMode: {
|
||||
duration: number
|
||||
}
|
||||
mute: { // comes with Livechat 10.2.0
|
||||
anonymous: boolean
|
||||
// TODO: https://github.com/JohnXLivingston/peertube-plugin-livechat/issues/127
|
||||
// nonFollowers: boolean (or a number of seconds?)
|
||||
}
|
||||
terms?: string // comes with Livechat 10.2.0
|
||||
moderation: { // comes with Livechat 10.3.0
|
||||
delay: number
|
||||
}
|
||||
}
|
||||
|
||||
interface ChannelForbiddenWords {
|
||||
|
@ -30,7 +30,7 @@ To enable this feature, you will need to set up your server and DNS records, so
|
||||
### Plugin settings
|
||||
|
||||
Start by going to the livechat plugin settings of your instance, then enable the setting "Enable connection to room using external XMPP accounts".
|
||||
By checking this setting, new settings appear below.
|
||||
By checking this settings, new settings appear below.
|
||||
|
||||
First of all, the "Prosody server to server port" field.
|
||||
This one defaults to 5269, which is the standard port for this service.
|
||||
|
@ -7,16 +7,6 @@ chapter: false
|
||||
|
||||
This section describes the plugin settings page.
|
||||
|
||||
## {{% livechat_label livechat_configuration_channel_terms_label %}}
|
||||
|
||||
{{% livechat_label livechat_configuration_channel_terms_desc %}}
|
||||
|
||||
For more information on this feature, check the documentation for [channel's terms & conditions](/peertube-plugin-livechat/documentation/user/streamers/terms).
|
||||
|
||||
{{% notice info %}}
|
||||
Changing this setting will restart the chat server, and all users will be disconnected for a short time.
|
||||
{{% /notice %}}
|
||||
|
||||
## {{% livechat_label "list_rooms_label" %}}
|
||||
|
||||
When pressing the «List rooms» button, all existing chatrooms will be listed.
|
||||
@ -76,7 +66,7 @@ The chat can be customized (readonly mode, use the current theme, ...).
|
||||
|
||||
You can for example generate a readonly URL and use it in OBS to integrate the chat in your live stream!
|
||||
|
||||
This setting allows you to choose who can access this modal.
|
||||
This settings allows you to choose who can access this modal.
|
||||
|
||||
### {{% livechat_label per_live_video_label %}}
|
||||
|
||||
@ -167,7 +157,7 @@ You can choose which theme to use for ConverseJS:
|
||||
The plugin comes with an AppImage that is used to run the [Prosody XMPP server](https://prosody.im).
|
||||
If this AppImage is not working, you can fallback to the Prosody that is packaged for your server. Just install the `prosody` package.
|
||||
|
||||
This setting should only be used if the plugin is broken, and waiting for a patch.
|
||||
This settings should only be used if the plugin is broken, and waiting for a patch.
|
||||
|
||||
### {{% livechat_label disable_websocket_label %}}
|
||||
|
||||
@ -181,7 +171,7 @@ This setting should only be used if the plugin is broken, and waiting for a patc
|
||||
|
||||
{{% livechat_label prosody_peertube_uri_description %}}
|
||||
|
||||
If this setting is left empty, and you are using Peertube >= 5.1 or later, the plugin will use values from your Peertube configuration file to guess on which interface and port request have to be done.
|
||||
If this settings is left empty, and you are using Peertube >= 5.1 or later, the plugin will use values from your Peertube configuration file to guess on which interface and port request have to be done.
|
||||
|
||||
In last resort, it will use your Peertube public URI.
|
||||
So, any API Call will go throught your Nginx server.
|
||||
@ -231,7 +221,7 @@ As example, this option can allow an instance of Matterbridge (once it could use
|
||||
|
||||
### {{% livechat_label prosody_components_label %}}
|
||||
|
||||
This setting enable XMPP external components to connect to the server.
|
||||
This settings enable XMPP external components to connect to the server.
|
||||
By default, this option **only allows connections from localhost components**.
|
||||
You have to change the "{{% livechat_label prosody_components_interfaces_label %}}" value to listen on other network interfaces.
|
||||
|
||||
|
@ -29,7 +29,7 @@ In some case (like for some Docker Peertube installation), the diagnostic tools
|
||||
|
||||
In such case, try changing the "{{% livechat_label prosody_peertube_uri_label %}}" settings, by setting `http://127.0.0.1:9000` (assuming 9000 is the port on which Peertube listen, ask your instance administrators if you don't know).
|
||||
|
||||
Check the help for [this setting](/peertube-plugin-livechat/documentation/admin/settings/) for more information.
|
||||
Check the help for [this settings](/peertube-plugin-livechat/documentation/admin/settings/) for more information.
|
||||
|
||||
### Websocket
|
||||
|
||||
|
@ -53,7 +53,7 @@ This feature can be disabled by the instance's adminitrators.
|
||||
You can use OBS "Custom browser docks" to integrate the chat in your OBS while you are streaming.
|
||||
The livechat plugin offers a way to create long term token that can identify you automatically to join the chat, so you don't have to enter your password in OBS.
|
||||
|
||||
To do so, just use the "{{% livechat_label share_chat_link %}}" feature, and open the "{{% livechat_label share_chat_dock %}}" tab.
|
||||
To do so, just use the "{{% livechat_label share_chat_link %}}", and open the "{{% livechat_label share_chat_dock %}}" tab.
|
||||
From there, you can create a new token using the "+" button.
|
||||
|
||||

|
||||
|
@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "For streamers"
|
||||
description: "How to setup the chat for your live stream"
|
||||
weight: 200
|
||||
weight: 20
|
||||
chapter: false
|
||||
---
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "Some basics"
|
||||
description: "Some basics about how to setup and use the chat for your live stream"
|
||||
weight: 100
|
||||
weight: 10
|
||||
chapter: false
|
||||
---
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "Chat bot"
|
||||
description: "Chat bot setup"
|
||||
weight: 400
|
||||
weight: 40
|
||||
chapter: false
|
||||
---
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "Channel configuration"
|
||||
description: "Peertube channel chatrooms configuration"
|
||||
weight: 200
|
||||
weight: 30
|
||||
chapter: false
|
||||
---
|
||||
|
||||
@ -20,8 +20,6 @@ By clicking on a channel, you will then be able to setup some options for your c
|
||||
|
||||
Here you can configure:
|
||||
|
||||
* [{{% livechat_label livechat_configuration_channel_terms_label %}}](/peertube-plugin-livechat/documentation/user/streamers/terms)
|
||||
* [{{% livechat_label livechat_configuration_channel_mute_anonymous_label %}}](/peertube-plugin-livechat/documentation/user/streamers/moderation) default value
|
||||
* [The slow mode](/peertube-plugin-livechat/documentation/user/streamers/slow_mode)
|
||||
* [The chat bot](/peertube-plugin-livechat/documentation/user/streamers/bot)
|
||||
* [Custom emojis](/peertube-plugin-livechat/documentation/user/streamers/emojis)
|
||||
|
@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "Custom emojis"
|
||||
description: "Plugin peertube-plugin-livechat custom emojis"
|
||||
weight: 330
|
||||
weight: 32
|
||||
chapter: false
|
||||
---
|
||||
|
||||
@ -21,8 +21,6 @@ On the [channel configuration page](/peertube-plugin-livechat/documentation/user
|
||||
|
||||

|
||||
|
||||
{{% livechat_label livechat_emojis_shortname_desc %}}
|
||||
|
||||
### Import / Export
|
||||
|
||||
On the channel configuration page, there are an "{{% livechat_label action_import %}}" and an "{{% livechat_label action_export %}}" button.
|
||||
@ -34,7 +32,7 @@ The file must be a valid JSON file, using the following format:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"sn": ":short_name:",
|
||||
"sn": ":short_name",
|
||||
"url": "https://example.com/image.png"
|
||||
}
|
||||
]
|
||||
|
@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "Moderation"
|
||||
description: "Plugin peertube-plugin-livechat advanced moderation features"
|
||||
weight: 300
|
||||
weight: 60
|
||||
chapter: false
|
||||
---
|
||||
|
||||
@ -27,7 +27,7 @@ You can access room settings and moderation tools using the [chat dropdown menu]
|
||||
|
||||
{{% notice tip %}}
|
||||
The video owner will be owner of the chat room.
|
||||
This means they can configure the room, delete it, promote other users as admins, ...
|
||||
This means he can configure the room, delete it, promote other users as admins, ...
|
||||
{{% /notice %}}
|
||||
|
||||
{{% notice tip %}}
|
||||
@ -39,30 +39,6 @@ Clicking this button will give them owner access on the room.
|
||||
You can use [ConverseJS moderation commands](https://conversejs.org/docs/html/features.html#moderating-chatrooms) to moderate the room.
|
||||
When you open the chat room in full screen, there will also be a menu with dedicated commands on the top right.
|
||||
|
||||
## {{% livechat_label livechat_configuration_channel_mute_anonymous_label %}}
|
||||
|
||||
{{% notice info %}}
|
||||
This feature comes with the livechat plugin version 10.2.0.
|
||||
{{% /notice %}}
|
||||
|
||||
You can prevent anonymous users to send messages. In such case, only registered users will be able to talk in the chat.
|
||||
|
||||
To enable or disable this feature, use the [chat dropdown menu](/peertube-plugin-livechat/documentation/user/viewers), open the "configure" menu.
|
||||
In the form, you will find a "{{% livechat_label livechat_configuration_channel_mute_anonymous_label %}}" checkbox.
|
||||
|
||||

|
||||
|
||||
Anonymous users won't have the message field, and will see following prompt: "{{% livechat_label muted_anonymous_message %}}"
|
||||
|
||||

|
||||
|
||||
When this feature is enabled, anonymous users will be assigned the "visitor" role.
|
||||
You can change their role to "participant" if you want to allow some of them to talk.
|
||||
|
||||
If you change the room configuration, all anonymous users will be muted or unmuted.
|
||||
|
||||
You can choose to enable or disable this feature for new chatrooms on the [channel configuration page](/peertube-plugin-livechat/documentation/user/streamers/channel).
|
||||
|
||||
## Roles and affiliations
|
||||
|
||||
There are several roles that can be assignated to users in chat rooms: owner, moderators, member, ...
|
||||
|
@ -1,46 +0,0 @@
|
||||
---
|
||||
title: "Moderation delay"
|
||||
description: "Plugin peertube-plugin-livechat moderation delay"
|
||||
weight: 325
|
||||
chapter: false
|
||||
---
|
||||
|
||||
{{% notice info %}}
|
||||
This feature comes with the livechat plugin version 10.3.0.
|
||||
{{% /notice %}}
|
||||
|
||||
## Introduction
|
||||
|
||||
As a streamer, you can choose to delay messages in the chat, to let some time to moderators to delete messages before they can even be read by other participants.
|
||||
|
||||
When this feature is enabled, moderators will see all messages without any delay.
|
||||
Chat participants won't see that their own messages are delayed.
|
||||
|
||||
Please note that messages sent by moderators will also be delayed, to avoid them to respond to messages that are not even visible by other participants.
|
||||
|
||||
## Moderation delay option
|
||||
|
||||
On the [channel configuration page](/peertube-plugin-livechat/documentation/user/streamers/channel), you can set the "{{% livechat_label moderation_delay %}}" option:
|
||||
|
||||

|
||||
|
||||
This value will apply as a default value for all your channel's chatrooms.
|
||||
|
||||
Setting the value to `0` will disable the feature.
|
||||
|
||||
Setting the value to a positive integer will set the delay, in seconds, to apply to messages.
|
||||
Please avoid setting the value too high.
|
||||
Ideally it should not exceed a few seconds (4 or 5 seconds for example).
|
||||
|
||||
To modify the value for an already existing room, just open the room "configuration" menu (on top of the chat window), and change the moderation delay value in the configuration form.
|
||||
|
||||
{{% notice warning %}}
|
||||
Currently, this feature has one known bug: users that join the chat will get all messages, even messages that are still pending for other participants.
|
||||
However, messages sent after they joined will be delayed correctly.
|
||||
{{% /notice %}}
|
||||
|
||||
## In the chat
|
||||
|
||||
As a moderator, you will see the remaining time (in seconds) before the message is broadcasted, just besides the message datetime.
|
||||
|
||||

|
@ -1,88 +0,0 @@
|
||||
---
|
||||
title: "Polls"
|
||||
description: "You can create polls to ask viewers their opinion."
|
||||
weight: 330
|
||||
chapter: false
|
||||
---
|
||||
|
||||
{{% notice info %}}
|
||||
This feature comes with the livechat plugin version 10.2.0.
|
||||
{{% /notice %}}
|
||||
|
||||
## Create a poll
|
||||
|
||||
You can create a new poll by using the "{{% livechat_label new_poll %}}" action in the chat top menu:
|
||||
|
||||

|
||||
|
||||
{{% notice warning %}}
|
||||
This poll feature should not be considered as a reliable voting system.
|
||||
It is easy to cheat.
|
||||
There is no mechanism to prevent anonymous users to vote multiple times by just reloading the chat.
|
||||
Votes are never fully anonymous, someone having access to the server could see who voted for what choice.
|
||||
{{% /notice %}}
|
||||
|
||||
### Poll form
|
||||
|
||||
Fill the form fields:
|
||||
|
||||
* "{{% livechat_label poll_question %}}": the question to ask to you viewers
|
||||
* "{{% livechat_label poll_duration %}}": the duration for which viewers can vote
|
||||
* "{{% livechat_label poll_anonymous_results %}}": if checked, votes won't be publicly visible in the chat
|
||||
* "Choice N": choices that will be presented to viewers
|
||||
|
||||
You must at least fill the two first choices fields.
|
||||
|
||||
Once you submit the form, the poll will instantly start.
|
||||
|
||||
If there was a previous unfinished poll, it will end and its result will be shown.
|
||||
|
||||
### Access rights
|
||||
|
||||
Every room's admins can create a new poll.
|
||||
|
||||
When you promote someone as room admin or owner, they gets instant access to the "{{% livechat_label new_poll %}}" action.
|
||||
|
||||
When you remove admin or owner rights to someone, they can't create new poll. But any existing poll will continue until it ends.
|
||||
|
||||
Every user that is not muted can vote.
|
||||
This means that you can prevent anonymous users to vote by using the ["{{% livechat_label livechat_configuration_channel_mute_anonymous_label %}}" feature](/peertube-plugin-livechat/documentation/user/streamers/moderation).
|
||||
|
||||
## Poll workflow
|
||||
|
||||
When the polls starts, a first message will be sent in the chat, from the account of the user creating the poll.
|
||||
|
||||
A banner will also appear to show the poll, and will be updated regularly with the current votes.
|
||||
|
||||

|
||||
|
||||
Viewers can then vote by clicking on their choice, or by sending message like "!1" in the chat.
|
||||
|
||||
Votes counts will be updated regularly in the banner.
|
||||
|
||||
Viewers can change their vote at any time, just by making a new choice.
|
||||
Their precedent choice will be replaced by the new one.
|
||||
|
||||

|
||||
|
||||
{{% notice tip %}}
|
||||
Anonymous viewers can only vote once they have choosen their nickname.
|
||||
{{% /notice %}}
|
||||
|
||||
If "{{% livechat_label poll_anonymous_results %}}" is checked, votes won't be shown to other users.
|
||||
If unchecked, votes will be publicly visible as you will see message like "!1" in the chat.
|
||||
|
||||
{{% notice info %}}
|
||||
For viewers using XMPP clients or outdated livechat plugin versions, the banner will not be visible.
|
||||
But they will see the message in the chat and will be able to vote by sending messages with their choices.
|
||||
{{% /notice %}}
|
||||
|
||||
When the poll ends, a new message will be sent in the chat, with the results.
|
||||
|
||||

|
||||
|
||||
{{% notice info %}}
|
||||
The only way to get old polls results is to search for the poll end message in the chat.
|
||||
For now, polls results are not saved by any other means.
|
||||
So don't forget to note polls results if you want to keep them.
|
||||
{{% /notice %}}
|
@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "Slow mode"
|
||||
description: "Plugin peertube-plugin-livechat slow mode"
|
||||
weight: 320
|
||||
weight: 31
|
||||
chapter: false
|
||||
---
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "Tasks / To-do lists"
|
||||
description: "You can handle tasks and task lists with your moderation team."
|
||||
weight: 350
|
||||
weight: 35
|
||||
chapter: false
|
||||
---
|
||||
|
||||
|
@ -1,47 +0,0 @@
|
||||
---
|
||||
title: "Terms & conditions"
|
||||
description: "Configure channel's chat terms & conditions"
|
||||
weight: 310
|
||||
chapter: false
|
||||
---
|
||||
|
||||
{{% notice info %}}
|
||||
This feature comes with the livechat plugin version 10.2.0.
|
||||
{{% /notice %}}
|
||||
|
||||
## Configuration
|
||||
|
||||
You can add terms & conditions to your channel.
|
||||
These terms will be shown to all users joining the chat.
|
||||
|
||||
To configure the terms & conditions, go to the [channel configuration page](/peertube-plugin-livechat/documentation/user/streamers/channel):
|
||||
|
||||

|
||||
|
||||
URL in the message will be clickable.
|
||||
You can also do some styling: [Message Styling](https://xmpp.org/extensions/xep-0393.html).
|
||||
|
||||
## Viewers
|
||||
|
||||
When joining the chat, viewers will see the terms:
|
||||
|
||||

|
||||
|
||||
{{% notice info %}}
|
||||
Peertube instance's admin can also set global terms & conditions.
|
||||
If so, these terms will be shown above your channel's terms.
|
||||
{{% /notice %}}
|
||||
|
||||
{{% notice info %}}
|
||||
Anonymous users will only see the terms & conditions once they have chosen their nickname (in other words: once they are able to talk).
|
||||
{{% /notice %}}
|
||||
|
||||
You can change the terms content at any time, it will be instantly updated for all viewers.
|
||||
|
||||
Users can hide the terms & conditions.
|
||||
When doing so, terms won't be shown again, unless you change the content.
|
||||
|
||||
{{% notice info %}}
|
||||
If your Peertube instance allows joining chat with [XMPP clients](https://livingston.frama.io/peertube-plugin-livechat/documentation/admin/advanced/xmpp_clients/), users using such clients will see the terms as chat messages, coming from a "Peertube" account.
|
||||
When you update terms, they will receive a new message with the update terms content.
|
||||
{{% /notice %}}
|
Binary file not shown.
Before Width: | Height: | Size: 19 KiB |
Binary file not shown.
Before Width: | Height: | Size: 114 KiB |
Binary file not shown.
Before Width: | Height: | Size: 30 KiB |
Binary file not shown.
Before Width: | Height: | Size: 69 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user