Merge branch 'release/3.2.0' into main

This commit is contained in:
John Livingston 2021-07-20 02:57:14 +02:00
commit 537897e1ed
No known key found for this signature in database
GPG Key ID: B17B5640CE66CDBC
17 changed files with 454 additions and 34 deletions

View File

@ -1,5 +1,16 @@
# Changelog
## v3.2.0
### Features
* Builtin Prosody: list existing rooms in the settings page
* Builtin Prosody: new settings to enable local C2S. For example, can be used with Matterbridge (thanks https://github.com/tytan652)
### Fixes
* Fix broken API diagnostic.
## v3.1.0
### Features

View File

@ -24,6 +24,7 @@ If you have any question, or if you want to talk about this plugin, you can join
## Settings
For the chat mode, and related settings, please refer to the corresponding documentation:
* [Prosody server controlled by Peertube (recommended)](documentation/prosody.md). **This is the recommanded setup**.
* [Connect to an existing XMPP server with ConverseJS](documentation/conversejs.md)
* [Use an external web chat tool](documentation/external.md)

View File

@ -29,7 +29,7 @@ This roadmap is given as an indication. It will be updated as we go along accord
[x] | Documentation | Rewrite documentation for more clarity. | v3.0.0
[ ] | Documentation | Add screenshots.
[ ] | Documentation | User documentation.
[ ] | Builtin Prosody | Room administration: add a button in the plugin settings to open a modal with existing rooms list.
[.] | Builtin Prosody | Room administration: add a button in the plugin settings to open a modal with existing rooms list. TODO: use a modal. | v3.2.0
[ ] | Builtin Prosody | Check with yunohost how to integrate.
[ ] | Settings | Translate settings page.
[ ] | ConverseJS | UI: make custom templates, for a better UI/UX. Autoshow muc participants depending on the chat window width.

View File

@ -53,4 +53,34 @@
.peertube-plugin-livechat-warning {
color: orange;
}
}
.peertube-plugin-livechat-error {
color: red;
}
table.peertube-plugin-livechat-prosody-list-rooms {
border: 1px solid black;
margin: 5px 0px;
}
table.peertube-plugin-livechat-prosody-list-rooms tr:nth-child(odd) {
background-color: #eee;
}
table.peertube-plugin-livechat-prosody-list-rooms tr:nth-child(even) {
background-color: #fff;
}
table.peertube-plugin-livechat-prosody-list-rooms th {
background-color: var(--mainHoverColor);
border: 1px solid black;
color: var(--mainBackgroundColor);
padding: 4px 5px;
}
table.peertube-plugin-livechat-prosody-list-rooms td {
border: 1px solid #555;
color: black;
padding: 4px 5px;
}

View File

@ -11,6 +11,7 @@ interface RegisterClientHelpers {
// NB: getBaseRouterRoute will come with Peertube > 3.2.1 (3.3.0?)
getBaseRouterRoute?: () => string
isLoggedIn: () => boolean
getAuthHeader: () => { 'Authorization': string } | undefined
getSettings: () => Promise<{ [ name: string ]: string }>
notifier: {
info: (text: string, title?: string, timeout?: number) => void
@ -63,6 +64,7 @@ interface RegisterOptions {
interface Video {
isLive: boolean
isLocal: boolean
name: string
originInstanceUrl: string
uuid: string
}

View File

@ -1,4 +1,4 @@
import type { ChatType } from 'shared/lib/types'
import type { ChatType, ProsodyListRoomsResult } from 'shared/lib/types'
interface ActionPluginSettingsParams {
npmName: string
@ -31,6 +31,134 @@ function register ({ registerHook, registerSettingsScript, peertubeHelpers }: Re
diagButton.setAttribute('href', getBaseRoute() + '/settings/diagnostic')
diagButton.setAttribute('target', '_blank')
})
console.log('[peertube-plugin-livechat] Initializing prosody-list-rooms button')
const listRoomsButtons: NodeListOf<HTMLAnchorElement> =
document.querySelectorAll('.peertube-plugin-livechat-prosody-list-rooms')
listRoomsButtons.forEach(listRoomsButton => {
if (listRoomsButton.classList.contains('btn')) { return }
listRoomsButton.classList.add('btn')
listRoomsButton.classList.add('action-button')
listRoomsButton.classList.add('orange-button')
listRoomsButton.onclick = async (): Promise<void> => {
console.log('[peertube-plugin-livechat] Opening the room list...')
// TODO: use showModal (seems buggy with Peertube 3.2.1)
try {
document.querySelectorAll('.peertube-plugin-livechat-prosody-list-rooms-content')
.forEach(dom => dom.remove())
const container = document.createElement('div')
container.classList.add('peertube-plugin-livechat-prosody-list-rooms-content')
container.textContent = '...'
listRoomsButton.after(container)
const response = await fetch(getBaseRoute() + '/webchat/prosody-list-rooms', {
method: 'GET',
headers: peertubeHelpers.getAuthHeader()
})
if (!response.ok) {
throw new Error('Response is not ok')
}
const json: ProsodyListRoomsResult = await response.json()
if (!json.ok) {
container.textContent = json.error
container.classList.add('peertube-plugin-livechat-error')
} else {
const rooms = json.rooms.sort((a, b) => {
const timestampA = a.lasttimestamp ?? 0
const timestampB = b.lasttimestamp ?? 0
if (timestampA === timestampB) {
return a.name.localeCompare(b.name)
} else if (timestampA < timestampB) {
return 1
} else {
return -1
}
})
container.textContent = ''
const table = document.createElement('table')
table.classList.add('peertube-plugin-livechat-prosody-list-rooms')
container.append(table)
// TODO: translate labels.
const labels: any = {
RoomName: 'Room name',
RoomDescription: 'Room description',
NotFound: 'Not found',
Video: 'Video',
LastActivity: 'Last activity'
}
const titleLineEl = document.createElement('tr')
const titleNameEl = document.createElement('th')
titleNameEl.textContent = labels.RoomName
const titleDescriptionEl = document.createElement('th')
titleDescriptionEl.textContent = labels.RoomDescription
const titleVideoEl = document.createElement('th')
titleVideoEl.textContent = labels.Video
const titleLastActivityEl = document.createElement('th')
titleLastActivityEl.textContent = labels.LastActivity
titleLineEl.append(titleNameEl)
titleLineEl.append(titleDescriptionEl)
titleLineEl.append(titleVideoEl)
titleLineEl.append(titleLastActivityEl)
table.append(titleLineEl)
rooms.forEach(room => {
const uuid = room.localpart
const href = getBaseRoute() + '/webchat/room/' + encodeURIComponent(uuid)
const lineEl = document.createElement('tr')
const nameEl = document.createElement('td')
const aEl = document.createElement('a')
aEl.textContent = room.name
aEl.target = '_blank'
const descriptionEl = document.createElement('td')
descriptionEl.textContent = room.description
const videoEl = document.createElement('td')
const lastActivityEl = document.createElement('td')
if (room.lasttimestamp && (typeof room.lasttimestamp === 'number')) {
const date = new Date(room.lasttimestamp * 1000)
lastActivityEl.textContent = date.toLocaleDateString() + ' ' + date.toLocaleTimeString()
}
nameEl.append(aEl)
lineEl.append(nameEl)
lineEl.append(descriptionEl)
lineEl.append(videoEl)
lineEl.append(lastActivityEl)
table.append(lineEl)
if (/^[a-zA-A0-9-]+$/.test(uuid)) {
const p = fetch('/api/v1/videos/' + uuid, {
method: 'GET',
headers: peertubeHelpers.getAuthHeader()
})
p.then(async res => {
if (!res.ok) {
videoEl.textContent = labels.NotFound
return
}
const video: Video | undefined = await res.json()
if (!video) {
videoEl.textContent = labels.NotFound
return
}
aEl.href = href
const aVideoEl = document.createElement('a')
aVideoEl.textContent = video.name
aVideoEl.target = '_blank'
aVideoEl.href = '/videos/watch/' + uuid
videoEl.append(aVideoEl)
}, () => {
console.error('[peertube-plugin-livechat] Failed to retrieve video ' + uuid)
})
}
})
}
} catch (error) {
console.error(error)
peertubeHelpers.notifier.error('Room list failed')
}
}
})
}
})
registerSettingsScript({
@ -42,7 +170,15 @@ function register ({ registerHook, registerSettingsScript, peertubeHelpers }: Re
case 'prosody-port':
case 'prosody-peertube-uri':
case 'chat-type-help-builtin-prosody':
case 'prosody-list-rooms':
case 'prosody-advanced':
case 'prosody-c2s':
return options.formValues['chat-type'] !== ('builtin-prosody' as ChatType)
case 'prosody-c2s-port':
return !(
options.formValues['chat-type'] === ('builtin-prosody' as ChatType) &&
options.formValues['prosody-c2s'] === true
)
case 'chat-server':
case 'chat-room':
case 'chat-bosh-uri':

View File

@ -61,6 +61,27 @@ This is the port that the Prosody server will use. By default it is set to 52800
These settings are common with other chat modes.
Here is the documentation: [common settings](./common.md).
### Prosody advanced settings
#### Enable client to server connections
This setting enable XMPP clients to connect to the builtin Prosody server.
For now, this option **only allows connections from localhost clients**.
As example, this option can allow an instance of Matterbridge (once it could use anonymous login) *on the same machine* to bridge your chat with another services like a Matrix room.
##### Prosody client to server port
The port that will be used by the c2s module of the builtin Prosody server.
XMPP clients shall use this port to connect.
Change it if this port is already in use on your server.
## Moderation
You can list all existing chatrooms: in the plugin settings screen, there is a button «List rooms».
You can delete old rooms: join the room, and use the menu on the top to destroy the room.
## Notes
All instance moderators and admins will be owner for created chat rooms.

View File

@ -1,7 +1,7 @@
{
"name": "peertube-plugin-livechat",
"description": "PeerTube plugin livechat",
"version": "3.1.0",
"version": "3.2.0",
"author": "John Livingston",
"bugs": "https://github.com/JohnXLivingston/peertube-plugin-livechat/issues",
"clientScripts": [

View File

@ -0,0 +1,5 @@
# mod_http_peertubelivechat_list_rooms
This module is a custom module that allows Peertube server to list chat rooms.
This module is part of peertube-plugin-livechat, and is under the same LICENSE.

View File

@ -0,0 +1,62 @@
local json = require "util.json";
local jid_split = require"util.jid".split;
local array = require "util.array";
local mod_muc = module:depends"muc";
local all_rooms = rawget(mod_muc, "all_rooms")
module:depends"http";
function check_auth(routes)
local function check_request_auth(event)
local apikey = module:get_option_string("peertubelivechat_list_rooms_apikey", "")
if apikey == "" then
return false, 500;
end
if event.request.headers.authorization ~= "Bearer " .. apikey then
return false, 401;
end
return true;
end
for route, handler in pairs(routes) do
routes[route] = function (event, ...)
local permit, code = check_request_auth(event);
if not permit then
return code;
end
return handler(event, ...);
end;
end
return routes;
end
local function list_rooms(event)
local request, response = event.request, event.response;
local rooms_json = array();
for room in all_rooms() do
local localpart = jid_split(room.jid);
local history = room._history;
local lasttimestamp;
if history ~= nil and #history > 0 then
lasttimestamp = history[#history].timestamp;
end
rooms_json:push({
jid = room.jid;
localpart = localpart;
name = room:get_name() or localpart;
lang = room.get_language and room:get_language();
description = room:get_description();
lasttimestamp = lasttimestamp;
})
end
event.response.headers["Content-Type"] = "application/json";
return json.encode_array(rooms_json);
end
module:provides("http", {
route = check_auth {
["GET /list-rooms"] = list_rooms;
};
});

View File

@ -21,12 +21,14 @@ export async function diagProsody (test: string, options: RegisterServerOptions)
// FIXME: these tests are very similar to tests in testProsodyCorrectlyRunning. Remove from here?
// Testing the prosody config file.
let prosodyPort: string
let prosodyHost: string
try {
const wantedConfig = await getProsodyConfig(options)
const filePath = wantedConfig.paths.config
result.messages.push(`Prosody will run on port '${wantedConfig.port}'`)
prosodyPort = wantedConfig.port
prosodyHost = wantedConfig.host
result.messages.push(`Prosody will use ${wantedConfig.baseApiUrl} as base uri from api calls`)
@ -99,7 +101,8 @@ export async function diagProsody (test: string, options: RegisterServerOptions)
const testResult = await got(apiUrl, {
method: 'GET',
headers: {
authorization: 'Bearer ' + await getAPIKey(options)
authorization: 'Bearer ' + await getAPIKey(options),
host: prosodyHost
},
responseType: 'json',
resolveBodyOnly: true
@ -120,7 +123,8 @@ export async function diagProsody (test: string, options: RegisterServerOptions)
const testResult = await got(apiUrl, {
method: 'GET',
headers: {
authorization: 'Bearer ' + await getAPIKey(options)
authorization: 'Bearer ' + await getAPIKey(options),
host: prosodyHost
},
responseType: 'json',
resolveBodyOnly: true

View File

@ -66,6 +66,7 @@ async function getProsodyFilePaths (options: RegisterServerOptions): Promise<Pro
interface ProsodyConfig {
content: string
paths: ProsodyFilePaths
host: string
port: string
baseApiUrl: string
}
@ -77,6 +78,7 @@ async function getProsodyConfig (options: RegisterServerOptions): Promise<Prosod
if (!/^\d+$/.test(port)) {
throw new Error('Invalid port')
}
const enableC2s = (await options.settingsManager.getSetting('prosody-c2s') as boolean) || false
const prosodyDomain = await getProsodyDomain(options)
const paths = await getProsodyFilePaths(options)
@ -99,11 +101,21 @@ async function getProsodyConfig (options: RegisterServerOptions): Promise<Prosod
config.usePeertubeBosh(prosodyDomain, port)
config.useMucHttpDefault(roomApiUrl)
if (enableC2s) {
const c2sPort = (await options.settingsManager.getSetting('prosody-c2s-port') as string) || '52822'
if (!/^\d+$/.test(c2sPort)) {
throw new Error('Invalid c2s port')
}
config.useC2S(c2sPort)
}
// TODO: add a settings so that admin can choose? (on/off and duration)
config.useMam('1w') // Remove archived messages after 1 week
// TODO: add a settings to choose?
config.useDefaultPersistent()
config.useListRoomsApi(apikey)
config.useTestModule(apikey, testApiUrl)
let logLevel: ProsodyLogLevel | undefined
@ -126,7 +138,8 @@ async function getProsodyConfig (options: RegisterServerOptions): Promise<Prosod
content,
paths,
port,
baseApiUrl
baseApiUrl,
host: prosodyDomain
}
}

View File

@ -190,6 +190,8 @@ class ProsodyConfigContent {
this.anon.set('http_external_url', 'http://' + prosodyDomain)
this.muc.set('restrict_room_creation', 'local')
this.muc.set('http_host', prosodyDomain)
this.muc.set('http_external_url', 'http://' + prosodyDomain)
if (this.authenticated) {
this.authenticated.set('trusted_proxies', ['127.0.0.1', '::1'])
@ -201,6 +203,10 @@ class ProsodyConfigContent {
}
}
useC2S (c2sPort: string): void {
this.global.set('c2s_ports', [c2sPort])
}
useMucHttpDefault (url: string): void {
this.muc.add('modules_enabled', 'muc_http_defaults')
this.muc.add('muc_create_api_url', url)
@ -236,10 +242,15 @@ class ProsodyConfigContent {
this.muc.set('muc_room_default_persistent', true)
}
useListRoomsApi (apikey: string): void {
this.muc.add('modules_enabled', 'http_peertubelivechat_list_rooms')
this.muc.set('peertubelivechat_list_rooms_apikey', apikey)
}
useTestModule (prosodyApikey: string, apiurl: string): void {
this.global.add('modules_enabled', 'http_peertubelivechat_test')
this.global.set('peertubelivechat_test_apikey', prosodyApikey)
this.global.set('peertubelivechat_test_peertube_api_url', apiurl)
this.muc.add('modules_enabled', 'http_peertubelivechat_test')
this.muc.set('peertubelivechat_test_apikey', prosodyApikey)
this.muc.set('peertubelivechat_test_peertube_api_url', apiurl)
}
setLog (level: ProsodyLogLevel, syslog?: ProsodyLogLevel[]): void {

View File

@ -195,7 +195,10 @@ async function ensureProsodyRunning (options: RegisterServerOptions): Promise<vo
})
// Set the http-bind route.
changeHttpBindRoute(options, config.port)
changeHttpBindRoute(options, {
host: config.host,
port: config.port
})
async function sleep (ms: number): Promise<any> {
return new Promise((resolve) => {

View File

@ -1,16 +1,23 @@
import type { Router, RequestHandler, Request, Response, NextFunction } from 'express'
import type { ProxyOptions } from 'express-http-proxy'
import type { ChatType } from '../../../shared/lib/types'
import { getBaseRouterRoute, getBaseStaticRoute } from '../helpers'
import type { ChatType, ProsodyListRoomsResult } from '../../../shared/lib/types'
import { getBaseRouterRoute, getBaseStaticRoute, isUserAdmin } from '../helpers'
import { asyncMiddleware } from '../middlewares/async'
import { getProsodyDomain } from '../prosody/config/domain'
import { getAPIKey } from '../apikey'
import * as path from 'path'
const bodyParser = require('body-parser')
const got = require('got')
const fs = require('fs').promises
const proxy = require('express-http-proxy')
let httpBindRoute: RequestHandler
interface ProsodyHttpBindInfo {
host: string
port: string
}
let currentProsodyHttpBindInfo: ProsodyHttpBindInfo | null = null
async function initWebchatRouter (options: RegisterServerOptions): Promise<Router> {
const {
@ -97,22 +104,76 @@ async function initWebchatRouter (options: RegisterServerOptions): Promise<Route
httpBindRoute(req, res, next)
}
)
router.get('/prosody-list-rooms', asyncMiddleware(
async (req: Request, res: Response, _next: NextFunction) => {
if (!res.locals.authenticated) {
res.sendStatus(403)
return
}
if (!await isUserAdmin(options, res)) {
res.sendStatus(403)
return
}
const chatType: ChatType = await options.settingsManager.getSetting('chat-type') as ChatType
if (chatType !== 'builtin-prosody') {
const message = 'Please save the settings first.' // TODO: translate?
res.status(200)
const r: ProsodyListRoomsResult = {
ok: false,
error: message
}
res.json(r)
return
}
if (!currentProsodyHttpBindInfo) {
throw new Error('It seems that prosody is not binded... Cant list rooms.')
}
const apiUrl = `http://localhost:${currentProsodyHttpBindInfo.port}/peertubelivechat_list_rooms/list-rooms`
peertubeHelpers.logger.debug('Calling list rooms API on url: ' + apiUrl)
const rooms = await got(apiUrl, {
method: 'GET',
headers: {
authorization: 'Bearer ' + await getAPIKey(options),
host: currentProsodyHttpBindInfo.host
},
responseType: 'json',
resolveBodyOnly: true
})
res.status(200)
const r: ProsodyListRoomsResult = {
ok: true,
rooms: rooms
}
res.json(r)
}
))
return router
}
function changeHttpBindRoute ({ peertubeHelpers }: RegisterServerOptions, port: string | null): void {
function changeHttpBindRoute (
{ peertubeHelpers }: RegisterServerOptions,
prosodyHttpBindInfo: ProsodyHttpBindInfo | null
): void {
const logger = peertubeHelpers.logger
logger.info('Changing http-bind port for ' + (port ?? 'null'))
if (port !== null && !/^\d+$/.test(port)) {
logger.error('Port is not valid. Replacing by null')
port = null
if (prosodyHttpBindInfo && !/^\d+$/.test(prosodyHttpBindInfo.port)) {
logger.error(`Port '${prosodyHttpBindInfo.port}' is not valid. Replacing by null`)
prosodyHttpBindInfo = null
}
if (port === null) {
if (!prosodyHttpBindInfo) {
logger.info('Changing http-bind port for null')
currentProsodyHttpBindInfo = null
httpBindRoute = (_req: Request, res: Response, _next: NextFunction) => {
res.status(404)
res.send('Not found')
}
} else {
logger.info('Changing http-bind port for ' + prosodyHttpBindInfo.port + ', on host ' + prosodyHttpBindInfo.host)
const options: ProxyOptions = {
https: false,
proxyReqPathResolver: async (_req: Request): Promise<string> => {
@ -122,7 +183,8 @@ function changeHttpBindRoute ({ peertubeHelpers }: RegisterServerOptions, port:
parseReqBody: true // Note that setting this to false overrides reqAsBuffer and reqBodyEncoding below.
// FIXME: should we remove cookies?
}
httpBindRoute = proxy('http://localhost:' + port, options)
currentProsodyHttpBindInfo = prosodyHttpBindInfo
httpBindRoute = proxy('http://localhost:' + prosodyHttpBindInfo.port, options)
}
}

View File

@ -91,6 +91,13 @@ Please read the
private: true
})
registerSetting({
name: 'prosody-list-rooms',
label: 'List existing rooms',
type: 'html',
descriptionHTML: '<a class="peertube-plugin-livechat-prosody-list-rooms">List rooms</a>',
private: true
})
registerSetting({
name: 'prosody-port',
label: 'Prosody port',
@ -103,18 +110,6 @@ Change it if this port is already in use on your server.<br>
You can close this port on your firewall, it will not be accessed from the outer world.`
})
registerSetting({
name: 'prosody-peertube-uri',
label: 'Peertube url for API calls',
type: 'input',
default: '',
private: true,
descriptionHTML:
`Please let this settings empty if you don't know what you are doing.<br>
In some rare case, Prosody can't call Peertube's API from its public URI.
You can use this field to customise Peertube's URI for Prosody modules (for example with «http://localhost:9000»).`
})
registerSetting({
name: 'chat-server',
label: 'XMPP service server',
@ -269,6 +264,50 @@ Example: height:400px;`,
private: false
})
// ********** Built-in Prosody advanced settings
registerSetting({
name: 'prosody-advanced',
type: 'html',
private: true,
descriptionHTML: '<h3>Prosody advanced settings</h3>'
})
registerSetting({
name: 'prosody-peertube-uri',
label: 'Peertube url for API calls',
type: 'input',
default: '',
private: true,
descriptionHTML:
`Please let this settings empty if you don't know what you are doing.<br>
In some rare case, Prosody can't call Peertube's API from its public URI.
You can use this field to customise Peertube's URI for Prosody modules (for example with «http://localhost:9000»).`
})
registerSetting({
name: 'prosody-c2s',
label: 'Enable client to server connections',
type: 'input-checkbox',
default: false,
private: true,
descriptionHTML:
`Enable XMPP clients to connect to the builtin Prosody server.<br>
This option alone only allows connections from localhost clients.`
})
registerSetting({
name: 'prosody-c2s-port',
label: 'Prosody client to server port',
type: 'input',
default: '52822',
private: true,
descriptionHTML:
`The port that will be used by the c2s module of the builtin Prosody server.<br>
XMPP clients shall use this port to connect.<br>
Change it if this port is already in use on your server.<br>
Keep it close this port on your firewall for now, it will not be accessed from the outer world.`
})
// ********** settings changes management
settingsManager.onSettingsChange(async (settings: any) => {
if ('chat-type' in settings) {

View File

@ -1,5 +1,25 @@
type ChatType = 'disabled' | 'builtin-prosody' | 'builtin-converse' | 'external-uri'
export {
ChatType
interface ProsodyListRoomsResultError {
ok: false
error: string
}
interface ProsodyListRoomsResultSuccess {
ok: true
rooms: Array<{
jid: string
localpart: string
name: string
lang: string
description: string
lasttimestamp?: number
}>
}
type ProsodyListRoomsResult = ProsodyListRoomsResultError | ProsodyListRoomsResultSuccess
export {
ChatType,
ProsodyListRoomsResult
}