14 Commits

Author SHA1 Message Date
cb52a55895 bump version 2024-06-20 17:45:44 -04:00
4148444e91 Merge branch 'main' of https://github.com/JohnXLivingston/peertube-plugin-livechat 2024-06-20 17:45:31 -04:00
de14b95f9a better send button 2024-06-19 21:52:15 -04:00
4f80119c83 slightly edit send button style 2024-06-19 21:36:15 -04:00
80b2093202 livechat message form formatting 2024-06-19 21:00:18 -04:00
3d4afc4341 edit chat text area size 2024-06-19 20:39:46 -04:00
49a87237ec better sizing for mobile 2024-06-19 20:15:45 -04:00
0737e14472 better sizing for mobile 2024-06-19 19:52:55 -04:00
226ea38e4d better sizing for mobile 2024-06-19 19:34:40 -04:00
559fe731e0 update package json 2024-06-19 19:24:27 -04:00
e8eb56d0b7 fix dumb nigger shit 2024-06-19 19:02:36 -04:00
1b97366cd8 fix dumb nigger shit 2024-06-19 18:19:19 -04:00
1f3eee9889 better mobile device sizing 2024-06-19 17:56:09 -04:00
772c1c1d14 better sizing for mobile and desktop devices 2024-06-19 17:43:09 -04:00
144 changed files with 2334 additions and 20127 deletions

View File

@ -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

View File

@ -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;
}

View File

@ -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 })

View File

@ -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

View File

@ -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 {

View File

@ -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,

View File

@ -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=${''}

View File

@ -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')
}
}

View File

@ -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)

View File

@ -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 {

View File

@ -7,8 +7,7 @@ export enum ValidationErrorType {
WrongType,
WrongFormat,
NotInRange,
Duplicate,
TooLong
Duplicate
}
export class ValidationError extends Error {

View File

@ -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)
}

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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'

View File

@ -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)
}
}
}
})

View File

@ -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)

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -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>`
: ''
}`
}

View File

@ -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>`
}

View File

@ -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
}

View File

@ -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
})
}

View File

@ -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)

View File

@ -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.
}
}
}
})

View File

@ -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;
}
}

View File

@ -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>
`
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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)}`
}

View File

@ -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)}`
}

View File

@ -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>`
}

View File

@ -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'),

View File

@ -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,

View File

@ -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[]) => {

View File

@ -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">&nbsp;-&nbsp;${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
}
}
}
}

View File

@ -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

View File

@ -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"

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -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: コピーしました

View File

@ -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
View File

@ -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",

View File

@ -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"

View File

@ -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

View File

@ -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);

View File

@ -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.

View File

@ -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;
}

View File

@ -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;
};

View File

@ -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);

View File

@ -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.

View File

@ -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;
};

View File

@ -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.

View File

@ -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);

View File

@ -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.

View File

@ -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;
};

View File

@ -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;
};

View File

@ -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;
};

View File

@ -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)

View File

@ -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;
};

View File

@ -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

View File

@ -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(

View File

@ -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
}

View File

@ -49,14 +49,7 @@ function getDefaultChannelConfigurationOptions (_options: RegisterServerOptions)
},
slowMode: {
duration: 0
},
mute: {
anonymous: false
},
moderation: {
delay: 0
},
terms: undefined
}
}
}

View File

@ -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
}

View File

@ -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

View File

@ -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 {

View File

@ -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
}

View File

@ -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
}

View File

@ -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 {

View File

@ -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)

View File

@ -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)

View File

@ -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)
// }
}
/**

View File

@ -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
}

View File

@ -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
}

View File

@ -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'),

View File

@ -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

View File

@ -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 {

View File

@ -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.

View File

@ -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.

View File

@ -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

View File

@ -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.
![Share link popup - dock tab](/peertube-plugin-livechat/images/share_dock.png?classes=shadow,border&height=200px)

View File

@ -1,7 +1,7 @@
---
title: "For streamers"
description: "How to setup the chat for your live stream"
weight: 200
weight: 20
chapter: false
---

View File

@ -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
---

View File

@ -1,7 +1,7 @@
---
title: "Chat bot"
description: "Chat bot setup"
weight: 400
weight: 40
chapter: false
---

View File

@ -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)

View File

@ -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
![Channel configuration / Channel emojis](/peertube-plugin-livechat/images/channel_custom_emojis.png?classes=shadow,border&height=400px)
{{% 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"
}
]

View File

@ -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.
![Room configuration / Mute anonymous users](/peertube-plugin-livechat/images/configure_mute_anonymous.png?classes=shadow,border&height=400px)
Anonymous users won't have the message field, and will see following prompt: "{{% livechat_label muted_anonymous_message %}}"
![Room configuration / Muted anonymous users](/peertube-plugin-livechat/images/anonymous_muted.png?classes=shadow,border&height=400px)
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, ...

View File

@ -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:
![Channel configuration / Moderation delay](/peertube-plugin-livechat/images/moderation_delay_channel_option.png?classes=shadow,border&height=400px)
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.
![Moderation delay timer](/peertube-plugin-livechat/images/moderation_delay_timer.png?classes=shadow,border)

View File

@ -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:
![Poll form](/peertube-plugin-livechat/images/polls_form.png?classes=shadow,border&height=200px)
{{% 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.
![Poll start](/peertube-plugin-livechat/images/polls_start.png?classes=shadow,border&height=200px)
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.
![Poll votes](/peertube-plugin-livechat/images/polls_votes.png?classes=shadow,border&height=200px)
{{% 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.
![Poll end](/peertube-plugin-livechat/images/polls_end.png?classes=shadow,border&height=200px)
{{% 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 %}}

View File

@ -1,7 +1,7 @@
---
title: "Slow mode"
description: "Plugin peertube-plugin-livechat slow mode"
weight: 320
weight: 31
chapter: false
---

View File

@ -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
---

View File

@ -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):
![Channel configuration / Terms](/peertube-plugin-livechat/images/channel_terms_config.png?classes=shadow,border&height=400px)
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:
![Terms](/peertube-plugin-livechat/images/terms.png?classes=shadow,border&height=400px)
{{% 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