Log in with external Peertube account (#348):

* For anonymous users: new "log in using an external account" dialog, with the "remote Peertube account" options
* ConverseJS: using global vars for custom localized string (injected
  using Webpack)
This commit is contained in:
John Livingston 2024-04-08 19:02:56 +02:00
parent c55fabc972
commit 8fc8e3032b
No known key found for this signature in database
GPG Key ID: B17B5640CE66CDBC
15 changed files with 328 additions and 29 deletions

View File

@ -14,6 +14,8 @@ TODO: https://github.com/JohnXLivingston/peertube-plugin-livechat/issues/48
* #143: User colors: implementing [XEP-0392](https://xmpp.org/extensions/xep-0392.html) to have random colors on users nicknames
* #330: Chat does no more use an iframe to display the chat besides the videos.
* #330: Fullscreen chat: now uses a custom page (in other words: when opening the chat in a new tab, you will have the Peertube menu).
* For anonymous users: new "log in using an external account" dialog, with following options:
* remote Peertube account
### Minor changes and fixes

View File

@ -2,6 +2,7 @@
const fs = require('node:fs')
const path = require('node:path')
const YAML = require('yaml')
const locKeys = require('./loc.keys.js')
/**
* This script will patch ConverseJS .po files, to add custom strings.
@ -11,9 +12,7 @@ const livechatDir = path.resolve(__dirname, '..', 'languages')
const converseDir = path.resolve(__dirname, '..', 'build', 'conversejs', 'src', 'i18n')
// Labels to import:
const labels = loadLabels([
'slow_mode_info'
])
const labels = loadLabels(locKeys)
function loadLabels (keys) {
const labels = {}

View File

@ -79,6 +79,7 @@ rm -rf "$converse_build_dir/custom/"
echo "Adding the custom files..."
cp -r "$src_dir/custom/" "$converse_build_dir/custom/"
mv "$converse_build_dir/custom/webpack.livechat.js" "$converse_build_dir/"
cp "$src_dir/loc.keys.js" "$converse_build_dir/"
echo "Patching i18n files to add custom labels..."
/bin/env node conversejs/build-conversejs-patch-i18n.js

View File

@ -0,0 +1,117 @@
import { api } from '@converse/headless/core.js'
import { CustomElement } from 'shared/components/element.js'
import { tplExternalLoginModal } from 'templates/livechat-external-login-modal.js'
import { __ } from 'i18n'
export default class LivechatExternalLoginContentElement extends CustomElement {
static get properties () {
return {
remote_peertube_state: { type: String, attribute: false },
remote_peertube_alert_message: { type: String, attribute: false },
remote_peertube_try_anyway_url: { type: String, attribute: false }
}
}
constructor () {
super()
this.remote_peertube_state = 'init'
}
render () {
return tplExternalLoginModal(this, {
remote_peertube_state: this.remote_peertube_state,
remote_peertube_alert_message: this.remote_peertube_alert_message,
remote_peertube_try_anyway_url: this.remote_peertube_try_anyway_url
})
}
onKeyUp (_ev) {
if (this.remote_peertube_state !== 'init') {
this.remote_peertube_state = 'init'
this.remote_peertube_alert_message = ''
this.clearAlert()
}
}
async openRemotePeertube (ev) {
ev.preventDefault()
this.clearAlert()
const remotePeertubeUrl = ev.target.peertube_url.value.trim()
if (!remotePeertubeUrl) { return }
this.remote_peertube_state = 'loading'
try {
// Calling Peertube API to check if livechat plugin is available.
// In the meantime, this will also check that the URL exists, and is a Peertube instance
// (or something with similar API result... as the user typed the url, we assume there is no security risk here).
const configApiUrl = new URL('/api/v1/config', remotePeertubeUrl)
const config = await (await fetch(configApiUrl.toString())).json()
if (!config || typeof config !== 'object') {
throw new Error('Invalid config API result')
}
if (!('plugin' in config) || !('registered' in config.plugin) || !Array.isArray(config.plugin.registered)) {
throw new Error('No registered plugin in config API result')
}
if (!config.plugin.registered.find(p => p.npmName === 'peertube-plugin-livechat')) {
console.error('Plugin livechat not available on remote instance')
this.remote_peertube_state = 'error'
// eslint-disable-next-line no-undef
this.remote_peertube_alert_message = __(LOC_login_remote_peertube_no_livechat)
return
}
// Note: we do not check if the livechat plugin disables federation (neither on current or remote instance).
// We assume this is not a standard use case, and we don't want to add to much use cases.
// Now we must search the current video on the remote instance, to be sure it federates, and to get the url.
// Note: url search can be disabled on remote instance for non logged in users...
// As we are not authenticated on remote here, there are chances that the search wont return anything.
// As a fallback, we will launch another search with the video UUID.
// And if no result neither, we will just propose to open using the lazy-load page.
const videoUrl = api.settings.get('livechat_peertube_video_original_url')
const videoUUID = api.settings.get('livechat_peertube_video_uuid')
for (const search of [videoUrl, videoUUID]) {
if (!search) { continue }
// searching first on federation network, then on vidiverse (this could be disabled)
for (const searchTarget of ['local', 'search-index']) {
const searchAPIUrl = new URL('/api/v1/search/videos', remotePeertubeUrl)
searchAPIUrl.searchParams.append('start', '0')
searchAPIUrl.searchParams.append('count', 1)
searchAPIUrl.searchParams.append('search', search)
searchAPIUrl.searchParams.append('searchTarget', searchTarget)
const videos = await (await fetch(searchAPIUrl.toString())).json()
if (videos && Array.isArray(videos.data) && videos.data.length > 0 && videos.data[0].uuid) {
console.log('Video found, opening on remote instance')
this.remote_peertube_state = 'ok'
window.location.href = new URL(
'/videos/watch/' + encodeURIComponent(videos.data[0].uuid), remotePeertubeUrl
).toString()
return
}
}
}
console.error('Video not found on remote instance')
this.remote_peertube_state = 'error'
// eslint-disable-next-line no-undef
this.remote_peertube_alert_message = __(LOC_login_remote_peertube_video_not_found)
this.remote_peertube_try_anyway_url = new URL(
'/search/lazy-load-video;url=' + encodeURIComponent(videoUrl),
remotePeertubeUrl
).toString()
} catch (err) {
console.error(err)
this.remote_peertube_state = 'error'
// eslint-disable-next-line no-undef
this.remote_peertube_alert_message = __(LOC_login_remote_peertube_url_invalid)
}
}
clearAlert () {
this.remote_peertube_alert_message = ''
this.remote_peertube_try_anyway_url = ''
}
}
api.elements.define('converse-livechat-external-login-content', LivechatExternalLoginContentElement)

View File

@ -0,0 +1,20 @@
import { __ } from 'i18n'
import BaseModal from 'plugins/modal/modal.js'
import { api } from '@converse/headless/core'
import { html } from 'lit'
import 'livechat-external-login-content.js'
class ExternalLoginModal extends BaseModal {
remotePeertubeError = ''
renderModal () {
return html`<converse-livechat-external-login-content></converse-livechat-external-login-content>`
}
getModalTitle () {
// eslint-disable-next-line no-undef
return __(LOC_login_using_external_account)
}
}
api.elements.define('converse-livechat-external-login', ExternalLoginModal)

View File

@ -43,30 +43,39 @@ body.livechat-readonly.livechat-noscroll {
}
// Viewer mode
.livechat-viewer-mode-nick {
.livechat-viewer-mode-content {
display: none;
}
body[livechat-viewer-mode="on"] {
.livechat-viewer-mode-nick {
display: initial;
form {
display: flex !important;
flex-flow: row wrap !important;
padding-bottom: 0.5em !important;
border-top: 1px solid var(--chatroom-head-bg-color) !important;
gap: 10px;
align-items: baseline;
form {
display: flex !important;
flex-flow: row wrap !important;
padding-bottom: 0.5em !important;
border-top: var(--chatroom-separator-border-bottom) !important;
gap: 10px;
align-items: baseline;
label {
color: var(--text-color); // fix converseJs css that breaks this label color.
}
label {
color: var(--text-color); // fix converseJs css that breaks this label color.
}
}
hr {
margin: 0;
background-color: var(--chatroom-head-bg-color);
}
.livechat-viewer-mode-external-login {
padding: 2em;
}
}
body[livechat-viewer-mode="on"] {
.livechat-viewer-mode-content {
display: initial;
}
converse-muc-bottom-panel {
>:not(.livechat-viewer-mode-nick) {
>:not(.livechat-viewer-mode-content) {
display: none;
}
}

View File

@ -0,0 +1,56 @@
import { __ } from 'i18n'
import { html } from 'lit'
export const tplExternalLoginModal = (el, o) => {
// eslint-disable-next-line no-undef
const i18nRemotePeertube = __(LOC_login_remote_peertube)
// eslint-disable-next-line no-undef
const i18nRemotePeertubeUrl = __(LOC_login_remote_peertube_url)
const i18nRemotePeertubeOpen = __('OK')
return html`<div class="modal-body livechat-external-login-modal">
<form class="converse-form chatroom-form" @submit=${(ev) => el.openRemotePeertube(ev)}>
<label>
${i18nRemotePeertube}
<input
type="url"
placeholder="${i18nRemotePeertubeUrl}"
class="form-control ${o.remote_peertube_alert_message ? 'is-invalid' : ''}"
name="peertube_url"
?disabled=${o.remote_peertube_state === 'loading'}
/>
</label>
<input
type="submit"
class="btn btn-primary"
value="${i18nRemotePeertubeOpen}"
@keyup=${el.onKeyUp}
?disabled=${o.remote_peertube_state === 'loading'}
/>
${
o.remote_peertube_state !== 'loading'
? ''
: html`<small class="form-text text-muted">${
// eslint-disable-next-line no-undef
__(LOC_login_remote_peertube_searching)
}</small>`
}
${!o.remote_peertube_alert_message
? ''
: html`<div class="invalid-feedback d-block">${o.remote_peertube_alert_message}</div>`
}
${!o.remote_peertube_try_anyway_url
? ''
: html`<div class="form-text">
${
// eslint-disable-next-line no-undef
__(LOC_login_remote_peertube_video_not_found_try_anyway)
}
<button class="btn btn-primary" onclick="window.location.href='${o.remote_peertube_try_anyway_url}'">${
// eslint-disable-next-line no-undef
__(LOC_login_remote_peertube_video_not_found_try_anyway_button)
}</button>
</div>`
}
</fieldset>
</form></div>`
}

View File

@ -3,6 +3,7 @@ import { _converse, api } from '@converse/headless/core'
import { html } from 'lit'
import tplMucBottomPanel from '../../src/plugins/muc-views/templates/muc-bottom-panel.js'
import { CustomElement } from 'shared/components/element.js'
import 'shared/modals/livechat-external-login.js'
async function setNickname (ev, model) {
ev.preventDefault()
@ -54,7 +55,8 @@ class SlowMode extends CustomElement {
return html`<div class="livechat-slow-mode-info-box">
<converse-icon class="fa fa-info-circle" size="1.2em"></converse-icon>
${__(
'Slow mode is enabled, users can send a message every %1$s seconds.',
// eslint-disable-next-line no-undef
LOC_slow_mode_info,
this.model.config.get('slow_mode_duration')
)}
<i class="livechat-hide-slow-mode-info-box" @click=${this.closeSlowModeInfoBox}>
@ -82,14 +84,15 @@ export default (o) => {
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-nick chatroom-form-container"
@submit=${ev => setNickname(ev, model)}>
<form class="converse-form chatroom-form">
<div class="livechat-viewer-mode-content chatroom-form-container">
<form class="converse-form chatroom-form" @submit=${ev => setNickname(ev, model)}>
<label>${i18nHeading}</label>
<fieldset class="form-group">
<input type="text"
required="required"
required
name="nick"
value=""
class="form-control"
@ -99,6 +102,21 @@ export default (o) => {
<input type="submit" class="btn btn-primary" name="join" value="${i18nJoin}"/>
</fieldset>
</form>
${
// If we open a room with forcetype, there is no current video... So just disabling external login
// (in such case, we should be logged in as admin/moderator...)
!api.settings.get('livechat_peertube_video_original_url')
? ''
: html`
<hr>
<div class="livechat-viewer-mode-external-login">
<button class="btn btn-primary" @click=${ev => {
ev.preventDefault()
api.modal.show('converse-livechat-external-login')
}}>${i18nExternalLogin}</button>
</div>
`
}
</div>
${tplSlowMode(o)}
${tplMucBottomPanel(o)}`

View File

@ -1,18 +1,54 @@
const prod = require('./webpack/webpack.build.js')
const { merge } = require('webpack-merge')
const webpack = require('webpack')
const path = require('path')
const fs = require('fs')
const locKeys = require('./loc.keys.js')
function loadLocs () {
// Loading english strings, so we can inject them as constants.
const refFile = path.resolve(__dirname, '..', '..', 'dist', 'languages', 'en.reference.json')
if (!fs.existsSync(refFile)) {
throw new Error('Missing english reference file, please run "npm run build:languages" before building ConverseJS')
}
const english = require(refFile)
const r = {}
for (const key of locKeys) {
if (!(key in english) || (typeof english[key] !== 'string')) {
throw new Error('Missing english string key=' + key)
}
r['LOC_' + key] = JSON.stringify(english[key])
}
return r
}
module.exports = merge(prod, {
entry: path.resolve(__dirname, 'custom/entry.js'),
output: {
filename: 'converse.min.js'
},
plugins: [
new webpack.DefinePlugin(loadLocs())
],
resolve: {
extensions: ['.js'],
alias: {
'./templates/muc-bottom-panel.js': path.resolve('custom/templates/muc-bottom-panel.js'),
'../../templates/background_logo.js$': path.resolve(__dirname, 'custom/templates/background_logo.js'),
'shared/styles/index.scss$': path.resolve(__dirname, 'custom/shared/styles/livechat.scss')
'shared/styles/index.scss$': path.resolve(__dirname, 'custom/shared/styles/livechat.scss'),
'shared/modals/livechat-external-login.js': path.resolve(
__dirname,
'custom/shared/modals/livechat-external-login.js'
),
'templates/livechat-external-login-modal.js': path.resolve(
__dirname,
'custom/templates/livechat-external-login-modal.js'
),
'livechat-external-login-content.js': path.resolve(
__dirname,
'custom/livechat-external-login-content.js'
)
}
}
})

View File

@ -8,7 +8,10 @@ import type { AuthentInfos } from './auth'
* @returns default parameters to provide to ConverseJS.
*/
function defaultConverseParams (
{ forceReadonly, theme, assetsPath, room, forceDefaultHideMucParticipants, autofocus }: InitConverseJSParams
{
forceReadonly, theme, assetsPath, room, forceDefaultHideMucParticipants, autofocus,
peertubeVideoOriginalUrl, peertubeVideoUUID
}: InitConverseJSParams
): any {
const mucShowInfoMessages = forceReadonly
? [
@ -87,7 +90,10 @@ function defaultConverseParams (
colorize_username: true,
// This is a specific settings, that is used in ConverseJS customization, to force avatars loading in readonly mode.
livechat_load_all_vcards: !!forceReadonly
livechat_load_all_vcards: !!forceReadonly,
livechat_peertube_video_original_url: peertubeVideoOriginalUrl,
livechat_peertube_video_uuid: peertubeVideoUUID
}
// TODO: params.clear_messages_on_reconnection = true when muc_mam will be available.

View File

@ -6,7 +6,9 @@ export const livechatViewerModePlugin = {
const _converse = this._converse
_converse.api.settings.extend({
livechat_enable_viewer_mode: false
livechat_enable_viewer_mode: false,
livechat_peertube_video_original_url: undefined,
livechat_peertube_video_uuid: undefined
})
const originalGetDefaultMUCNickname = _converse.getDefaultMUCNickname

19
conversejs/loc.keys.js Normal file
View File

@ -0,0 +1,19 @@
/** Localization keys to inject in ConverseJS:
* these keys are used to:
* - inject needed localization strings in ConverseJS language files
* - defined global variable using Webpack, to retrieve the english key to pass to the ConverseJS localization function
*/
const locKeys = [
'slow_mode_info',
'login_using_external_account',
'login_remote_peertube',
'login_remote_peertube_searching',
'login_remote_peertube_url',
'login_remote_peertube_url_invalid',
'login_remote_peertube_no_livechat',
'login_remote_peertube_video_not_found',
'login_remote_peertube_video_not_found_try_anyway',
'login_remote_peertube_video_not_found_try_anyway_button'
]
module.exports = locKeys

View File

@ -392,3 +392,13 @@ invalid_value: "Invalid value."
slow_mode_info: "Slow mode is enabled, users can send a message every %1$s seconds."
chatroom_not_accessible: "This chatroom does not exist, or is not accessible to you."
login_using_external_account: "Log in using an external account"
login_remote_peertube: "Log in using an account on another Peertube instance:"
login_remote_peertube_url: "Your Peertube instance URL"
login_remote_peertube_searching: "Searching the video on the Peertube instance..."
login_remote_peertube_url_invalid: "Invalid Peertube URL."
login_remote_peertube_no_livechat: "The livechat plugin is not installed on this Peertube instance."
login_remote_peertube_video_not_found: "This video is not available on this Peertube instance."
login_remote_peertube_video_not_found_try_anyway: "In some cases, the video can still be retrieved if you connect to the remote instance."
login_remote_peertube_video_not_found_try_anyway_button: "Try anyway to open the video on the Peertube instance"

View File

@ -77,6 +77,8 @@ async function getConverseJSParams (
} = connectionInfos
return {
peertubeVideoOriginalUrl: roomInfos.video?.url,
peertubeVideoUUID: roomInfos.video?.uuid,
staticBaseUrl,
assetsPath: staticBaseUrl + 'conversejs/',
isRemoteChat: !!(roomInfos.video?.remote),

View File

@ -1,6 +1,8 @@
type ConverseJSTheme = 'peertube' | 'default' | 'concord'
interface InitConverseJSParams {
peertubeVideoOriginalUrl?: string
peertubeVideoUUID?: string
isRemoteChat: boolean
localAnonymousJID: string | null
remoteAnonymousJID: string | null