Merge branch 'develop'

This commit is contained in:
SamTV12345 2024-04-29 21:32:32 +02:00
commit 8fa708a374
54 changed files with 5925 additions and 4418 deletions

17
.editorconfig Normal file
View file

@ -0,0 +1,17 @@
root = true
[*]
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
end_of_line = lf
# editorconfig-tools is unable to ignore longs strings or urls
max_line_length = off
[CHANGELOG.md]
indent_size = 4
[*.bat]
end_of_line = crlf

View file

@ -4,15 +4,15 @@
# Always ensure to load the env variables in every terminal session. # Always ensure to load the env variables in every terminal session.
# Otherwise the env variables will not be available # Otherwise the env variables will not be available
DOCKER_COMPOSE_APP_DEV_PORT_PUBLISHED=9001 DOCKER_COMPOSE_APP_PORT_PUBLISHED=9001
DOCKER_COMPOSE_APP_DEV_PORT_TARGET=9001 DOCKER_COMPOSE_APP_PORT_TARGET=9001
# IMPORTANT: When the env var DEFAULT_PAD_TEXT is unset or empty, then the pad is not established (not the landing page). # IMPORTANT: When the env var DEFAULT_PAD_TEXT is unset or empty, then the pad is not established (not the landing page).
# The env var DEFAULT_PAD_TEXT seems to be mandatory in the latest version of etherpad. # The env var DEFAULT_PAD_TEXT seems to be mandatory in the latest version of etherpad.
DOCKER_COMPOSE_APP_DEV_ENV_DEFAULT_PAD_TEXT="Welcome to etherpad" DOCKER_COMPOSE_APP_DEV_ENV_DEFAULT_PAD_TEXT="Welcome to etherpad"
DOCKER_COMPOSE_APP_DEV_ADMIN_PASSWORD= DOCKER_COMPOSE_APP_ADMIN_PASSWORD=
DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_DATABASE=db DOCKER_COMPOSE_POSTGRES_DATABASE=db
DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_PASSWORD=etherpad-lite-password DOCKER_COMPOSE_POSTGRES_PASSWORD=etherpad-lite-password
DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_USER=etherpad-lite-user DOCKER_COMPOSE_POSTGRES_USER=etherpad-lite-user

18
.env.dev.default Normal file
View file

@ -0,0 +1,18 @@
# Please copy and rename this file.
#
# !Attention!
# Always ensure to load the env variables in every terminal session.
# Otherwise the env variables will not be available
DOCKER_COMPOSE_APP_DEV_PORT_PUBLISHED=9001
DOCKER_COMPOSE_APP_DEV_PORT_TARGET=9001
# IMPORTANT: When the env var DEFAULT_PAD_TEXT is unset or empty, then the pad is not established (not the landing page).
# The env var DEFAULT_PAD_TEXT seems to be mandatory in the latest version of etherpad.
DOCKER_COMPOSE_APP_DEV_ENV_DEFAULT_PAD_TEXT="Welcome to etherpad"
DOCKER_COMPOSE_APP_DEV_ADMIN_PASSWORD=
DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_DATABASE=db
DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_PASSWORD=etherpad-lite-password
DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_USER=etherpad-lite-user

View file

@ -36,7 +36,7 @@ jobs:
- uses: pnpm/action-setup@v3 - uses: pnpm/action-setup@v3
name: Install pnpm name: Install pnpm
with: with:
version: 8 version: 9.0.4
run_install: false run_install: false
- name: Get pnpm store directory - name: Get pnpm store directory
shell: bash shell: bash
@ -93,7 +93,7 @@ jobs:
- uses: pnpm/action-setup@v3 - uses: pnpm/action-setup@v3
name: Install pnpm name: Install pnpm
with: with:
version: 8 version: 9.0.4
run_install: false run_install: false
- name: Get pnpm store directory - name: Get pnpm store directory
shell: bash shell: bash
@ -163,7 +163,7 @@ jobs:
- uses: pnpm/action-setup@v3 - uses: pnpm/action-setup@v3
name: Install pnpm name: Install pnpm
with: with:
version: 8 version: 9.0.4
run_install: false run_install: false
- name: Get pnpm store directory - name: Get pnpm store directory
shell: bash shell: bash
@ -216,7 +216,7 @@ jobs:
- uses: pnpm/action-setup@v3 - uses: pnpm/action-setup@v3
name: Install pnpm name: Install pnpm
with: with:
version: 8 version: 9.0.4
run_install: false run_install: false
- name: Get pnpm store directory - name: Get pnpm store directory
shell: bash shell: bash

View file

@ -38,7 +38,7 @@ jobs:
- uses: pnpm/action-setup@v3 - uses: pnpm/action-setup@v3
name: Install pnpm name: Install pnpm
with: with:
version: 8 version: 9.0.4
run_install: false run_install: false
- name: Get pnpm store directory - name: Get pnpm store directory
shell: bash shell: bash

View file

@ -47,7 +47,7 @@ jobs:
- uses: pnpm/action-setup@v3 - uses: pnpm/action-setup@v3
name: Install pnpm name: Install pnpm
with: with:
version: 8 version: 9.0.4
run_install: false run_install: false
- name: Get pnpm store directory - name: Get pnpm store directory
shell: bash shell: bash

View file

@ -37,7 +37,7 @@ jobs:
- uses: pnpm/action-setup@v3 - uses: pnpm/action-setup@v3
name: Install pnpm name: Install pnpm
with: with:
version: 8 version: 9.0.4
run_install: false run_install: false
- name: Get pnpm store directory - name: Get pnpm store directory
shell: bash shell: bash

View file

@ -30,7 +30,7 @@ jobs:
- uses: pnpm/action-setup@v3 - uses: pnpm/action-setup@v3
name: Install pnpm name: Install pnpm
with: with:
version: 8 version: 9.0.4
run_install: false run_install: false
- name: Get pnpm store directory - name: Get pnpm store directory
shell: bash shell: bash
@ -96,7 +96,7 @@ jobs:
- uses: pnpm/action-setup@v3 - uses: pnpm/action-setup@v3
name: Install pnpm name: Install pnpm
with: with:
version: 8 version: 9.0.4
run_install: false run_install: false
- name: Get pnpm store directory - name: Get pnpm store directory
shell: bash shell: bash
@ -163,7 +163,7 @@ jobs:
- uses: pnpm/action-setup@v3 - uses: pnpm/action-setup@v3
name: Install pnpm name: Install pnpm
with: with:
version: 8 version: 9.0.4
run_install: false run_install: false
- name: Get pnpm store directory - name: Get pnpm store directory
shell: bash shell: bash

View file

@ -32,7 +32,7 @@ jobs:
- uses: pnpm/action-setup@v3 - uses: pnpm/action-setup@v3
name: Install pnpm name: Install pnpm
with: with:
version: 8 version: 9.0.4
run_install: false run_install: false
- name: Get pnpm store directory - name: Get pnpm store directory
shell: bash shell: bash
@ -76,7 +76,7 @@ jobs:
- uses: pnpm/action-setup@v3 - uses: pnpm/action-setup@v3
name: Install pnpm name: Install pnpm
with: with:
version: 8 version: 9.0.4
run_install: false run_install: false
- name: Get pnpm store directory - name: Get pnpm store directory
shell: bash shell: bash
@ -147,7 +147,7 @@ jobs:
- uses: pnpm/action-setup@v3 - uses: pnpm/action-setup@v3
name: Install pnpm name: Install pnpm
with: with:
version: 8 version: 9.0.4
run_install: false run_install: false
- name: Get pnpm store directory - name: Get pnpm store directory
shell: bash shell: bash

View file

@ -29,7 +29,7 @@ jobs:
- uses: pnpm/action-setup@v3 - uses: pnpm/action-setup@v3
name: Install pnpm name: Install pnpm
with: with:
version: 8 version: 9.0.4
run_install: false run_install: false
- name: Get pnpm store directory - name: Get pnpm store directory
shell: bash shell: bash

View file

@ -32,7 +32,7 @@ jobs:
- uses: pnpm/action-setup@v3 - uses: pnpm/action-setup@v3
name: Install pnpm name: Install pnpm
with: with:
version: 8 version: 9.0.4
run_install: false run_install: false
- name: Get pnpm store directory - name: Get pnpm store directory
shell: bash shell: bash

View file

@ -38,7 +38,7 @@ jobs:
- uses: pnpm/action-setup@v3 - uses: pnpm/action-setup@v3
name: Install pnpm name: Install pnpm
with: with:
version: 8 version: 9.0.4
run_install: false run_install: false
- name: Only install direct dependencies - name: Only install direct dependencies
run: pnpm config set auto-install-peers false run: pnpm config set auto-install-peers false

View file

@ -38,7 +38,7 @@ jobs:
- uses: pnpm/action-setup@v3 - uses: pnpm/action-setup@v3
name: Install pnpm name: Install pnpm
with: with:
version: 8 version: 9.0.4
run_install: false run_install: false
- name: Get pnpm store directory - name: Get pnpm store directory
shell: bash shell: bash
@ -132,7 +132,7 @@ jobs:
- uses: pnpm/action-setup@v3 - uses: pnpm/action-setup@v3
name: Install pnpm name: Install pnpm
with: with:
version: 8 version: 9.0.4
run_install: false run_install: false
- name: Get pnpm store directory - name: Get pnpm store directory
shell: bash shell: bash

View file

@ -1,3 +1,13 @@
# 2.0.3
### Notable enhancements and fixes
- Added documentation for replacing apikeys with oauth2
- Bumped live plugin manager to 0.20.0. Thanks to @fgreinacher
- Added better documentation for using docker-compose with Etherpad
# 2.0.2 # 2.0.2
### Notable enhancements and fixes ### Notable enhancements and fixes

View file

@ -8,7 +8,7 @@ FROM node:alpine as adminBuild
WORKDIR /opt/etherpad-lite WORKDIR /opt/etherpad-lite
COPY ./ ./ COPY ./ ./
RUN cd ./admin && npm install -g pnpm && pnpm install && pnpm run build --outDir ./dist RUN cd ./admin && npm install -g pnpm@9.0.4 && pnpm install && pnpm run build --outDir ./dist
RUN cd ./ui && pnpm install && pnpm run build --outDir ./dist RUN cd ./ui && pnpm install && pnpm run build --outDir ./dist
@ -91,7 +91,7 @@ RUN mkdir -p "${EP_DIR}" && chown etherpad:etherpad "${EP_DIR}"
# https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=863199 # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=863199
RUN \ RUN \
mkdir -p /usr/share/man/man1 && \ mkdir -p /usr/share/man/man1 && \
npm install pnpm -g && \ npm install pnpm@9.0.4 -g && \
apk update && apk upgrade && \ apk update && apk upgrade && \
apk add --no-cache \ apk add --no-cache \
ca-certificates \ ca-certificates \

View file

@ -43,6 +43,63 @@ We're looking for maintainers and have some funding available. Please contact J
## Installation ## Installation
### Docker-Compose
```yaml
services:
app:
user: "0:0"
image: etherpad/etherpad:latest
tty: true
stdin_open: true
volumes:
- plugins:/opt/etherpad-lite/src/plugin_packages
- etherpad-var:/opt/etherpad-lite/var
depends_on:
- postgres
environment:
NODE_ENV: production
ADMIN_PASSWORD: ${DOCKER_COMPOSE_APP_ADMIN_PASSWORD:-admin}
DB_CHARSET: ${DOCKER_COMPOSE_APP_DB_CHARSET:-utf8mb4}
DB_HOST: postgres
DB_NAME: ${DOCKER_COMPOSE_POSTGRES_DATABASE:-etherpad}
DB_PASS: ${DOCKER_COMPOSE_POSTGRES_PASSWORD:-admin}
DB_PORT: ${DOCKER_COMPOSE_POSTGRES_PORT:-5432}
DB_TYPE: "postgres"
DB_USER: ${DOCKER_COMPOSE_POSTGRES_USER:-admin}
# For now, the env var DEFAULT_PAD_TEXT cannot be unset or empty; it seems to be mandatory in the latest version of etherpad
DEFAULT_PAD_TEXT: ${DOCKER_COMPOSE_APP_DEFAULT_PAD_TEXT:- }
DISABLE_IP_LOGGING: ${DOCKER_COMPOSE_APP_DISABLE_IP_LOGGING:-false}
SOFFICE: ${DOCKER_COMPOSE_APP_SOFFICE:-null}
TRUST_PROXY: ${DOCKER_COMPOSE_APP_TRUST_PROXY:-true}
restart: always
ports:
- "${DOCKER_COMPOSE_APP_PORT_PUBLISHED:-9001}:${DOCKER_COMPOSE_APP_PORT_TARGET:-9001}"
postgres:
image: postgres:15-alpine
environment:
POSTGRES_DB: ${DOCKER_COMPOSE_POSTGRES_DATABASE:-etherpad}
POSTGRES_PASSWORD: ${DOCKER_COMPOSE_POSTGRES_PASSWORD:-admin}
POSTGRES_PORT: ${DOCKER_COMPOSE_POSTGRES_PORT:-5432}
POSTGRES_USER: ${DOCKER_COMPOSE_POSTGRES_USER:-admin}
PGDATA: /var/lib/postgresql/data/pgdata
restart: always
# Exposing the port is not needed unless you want to access this database instance from the host.
# Be careful when other postgres docker container are running on the same port
# ports:
# - "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data/pgdata
volumes:
postgres_data:
plugins:
etherpad-var:
```
### Requirements ### Requirements
[Node.js](https://nodejs.org/) >= **18.18.2**. [Node.js](https://nodejs.org/) >= **18.18.2**.

View file

@ -1,7 +1,7 @@
{ {
"name": "admin", "name": "admin",
"private": true, "private": true,
"version": "2.0.2", "version": "2.0.3",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@ -9,31 +9,33 @@
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": {}, "dependencies": {
"@radix-ui/react-switch": "^1.0.3"
},
"devDependencies": { "devDependencies": {
"@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-toast": "^1.1.5", "@radix-ui/react-toast": "^1.1.5",
"i18next": "^23.10.1", "@types/react": "^18.2.79",
"i18next-browser-languagedetector": "^7.2.1", "@types/react-dom": "^18.2.25",
"lucide-react": "^0.365.0", "@typescript-eslint/eslint-plugin": "^7.7.0",
"react": "^18.2.0", "@typescript-eslint/parser": "^7.7.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.51.2",
"react-i18next": "^14.1.0",
"react-router-dom": "^6.22.3",
"zustand": "^4.5.2",
"@types/react": "^18.2.74",
"@types/react-dom": "^18.2.24",
"@typescript-eslint/eslint-plugin": "^7.5.0",
"@typescript-eslint/parser": "^7.5.0",
"@vitejs/plugin-react-swc": "^3.5.0", "@vitejs/plugin-react-swc": "^3.5.0",
"eslint": "^9.0.0", "eslint": "^9.0.0",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5", "eslint-plugin-react-refresh": "^0.4.5",
"i18next": "^23.11.2",
"i18next-browser-languagedetector": "^7.2.1",
"lucide-react": "^0.372.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.51.3",
"react-i18next": "^14.1.0",
"react-router-dom": "^6.22.3",
"socket.io-client": "^4.7.5", "socket.io-client": "^4.7.5",
"typescript": "^5.4.4", "typescript": "^5.4.5",
"vite": "^5.2.8", "vite": "^5.2.9",
"vite-plugin-static-copy": "^1.0.2", "vite-plugin-static-copy": "^1.0.3",
"vite-plugin-svgr": "^4.2.0" "vite-plugin-svgr": "^4.2.0",
"zustand": "^4.5.2"
} }
} }

View file

@ -6,7 +6,7 @@ import {NavLink, Outlet, useNavigate} from "react-router-dom";
import {useStore} from "./store/store.ts"; import {useStore} from "./store/store.ts";
import {LoadingScreen} from "./utils/LoadingScreen.tsx"; import {LoadingScreen} from "./utils/LoadingScreen.tsx";
import {Trans, useTranslation} from "react-i18next"; import {Trans, useTranslation} from "react-i18next";
import {Cable, Construction, Crown, NotepadText, Wrench} from "lucide-react"; import {Cable, Construction, Crown, NotepadText, Wrench, PhoneCall} from "lucide-react";
const WS_URL = import.meta.env.DEV? 'http://localhost:9001' : '' const WS_URL = import.meta.env.DEV? 'http://localhost:9001' : ''
export const App = ()=> { export const App = ()=> {
@ -97,7 +97,9 @@ export const App = ()=> {
<li><NavLink to="/plugins"><Cable/><Trans i18nKey="admin_plugins"/></NavLink></li> <li><NavLink to="/plugins"><Cable/><Trans i18nKey="admin_plugins"/></NavLink></li>
<li><NavLink to={"/settings"}><Wrench/><Trans i18nKey="admin_settings"/></NavLink></li> <li><NavLink to={"/settings"}><Wrench/><Trans i18nKey="admin_settings"/></NavLink></li>
<li><NavLink to={"/help"}> <Construction/> <Trans i18nKey="admin_plugins_info"/></NavLink></li> <li><NavLink to={"/help"}> <Construction/> <Trans i18nKey="admin_plugins_info"/></NavLink></li>
<li><NavLink to={"/pads"}><NotepadText/><Trans i18nKey="ep_admin_pads:ep_adminpads2_manage-pads"/></NavLink></li> <li><NavLink to={"/pads"}><NotepadText/><Trans
i18nKey="ep_admin_pads:ep_adminpads2_manage-pads"/></NavLink></li>
<li><NavLink to={"/shout"}><PhoneCall/>Communication</NavLink></li>
</ul> </ul>
</div> </div>
</div> </div>

View file

@ -0,0 +1,13 @@
export type ShoutType = {
type: string,
data:{
type: string,
payload: {
message: {
message: string,
sticky: boolean
},
timestamp: number
}
}
}

View file

@ -250,10 +250,18 @@ td, th {
color: #666; color: #666;
} }
.settings-page {
display: flex;
flex-direction: column;
gap: 20px;
height: 100%;
}
.settings { .settings {
flex-grow: max(1, 1);
outline: none; outline: none;
width: 100%; width: 100%;
min-height: 80vh;
resize: none; resize: none;
} }
@ -596,6 +604,25 @@ pre {
outline: none; outline: none;
} }
.send-message {
position: relative;
}
.send-message input {
width: auto;
}
.send-message {
}
.send-message svg {
position: absolute;
right: 3px;
bottom: -3px;
left: auto !important;
}
.search-field svg { .search-field svg {
position: absolute; position: absolute;
left: 3px; left: 3px;
@ -725,3 +752,52 @@ input, button, select, optgroup, textarea {
right: 10px; right: 10px;
color: #666; color: #666;
} }
.SwitchRoot {
align-self: center;
width: 60px;
height: 30px;
background-color: black;
border-radius: 9999px;
position: relative;
box-shadow: 0 2px 10px var(--black-a7);
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
.SwitchRoot:focus {
box-shadow: 0 0 0 2px black;
}
.SwitchRoot[data-state='checked'] {
background-color: var(--etherpad-color);
}
.SwitchThumb {
display: block;
width: 20px;
height: 20px;
background-color: white;
border-radius: 9999px;
box-shadow: 0 2px 2px var(--black-a7);
transition: transform 100ms;
transform: translateX(2px);
will-change: transform;
}
.SwitchThumb[data-state='checked'] {
transform: translateX(25px);
}
.Label {
color: white;
font-size: 15px;
line-height: 1;
}
.message {
position: relative;
padding: 10px;
border: 1px solid #e0e0e0;
margin: 10px 20px 10px 10px;
border-radius: 10px 0 10px 10px;
background-color: var(--etherpad-color);
color: white
}

View file

@ -12,6 +12,7 @@ import {I18nextProvider} from "react-i18next";
import i18n from "./localization/i18n.ts"; import i18n from "./localization/i18n.ts";
import {PadPage} from "./pages/PadPage.tsx"; import {PadPage} from "./pages/PadPage.tsx";
import {ToastDialog} from "./utils/Toast.tsx"; import {ToastDialog} from "./utils/Toast.tsx";
import {ShoutPage} from "./pages/ShoutPage.tsx";
const router = createBrowserRouter(createRoutesFromElements( const router = createBrowserRouter(createRoutesFromElements(
<><Route element={<App/>}> <><Route element={<App/>}>
@ -20,6 +21,7 @@ const router = createBrowserRouter(createRoutesFromElements(
<Route path="/settings" element={<SettingsPage/>}/> <Route path="/settings" element={<SettingsPage/>}/>
<Route path="/help" element={<HelpPage/>}/> <Route path="/help" element={<HelpPage/>}/>
<Route path="/pads" element={<PadPage/>}/> <Route path="/pads" element={<PadPage/>}/>
<Route path="/shout" element={<ShoutPage/>}/>
</Route><Route path="/login"> </Route><Route path="/login">
<Route index element={<LoginScreen/>}/> <Route index element={<LoginScreen/>}/>
</Route></> </Route></>

View file

@ -21,7 +21,7 @@ export const HelpPage = () => {
return <div key={hookName+i}> return <div key={hookName+i}>
<h3>{hookName}</h3> <h3>{hookName}</h3>
<ul> <ul>
{Object.keys(hooks[hookName]).map((hook, i) => <li>{hook} {Object.keys(hooks[hookName]).map((hook, i) => <li key={hook+i}>{hook}
<ul key={hookName+hook+i}> <ul key={hookName+hook+i}>
{Object.keys(hooks[hookName][hook]).map((subHook, i) => <li key={i}>{subHook}</li>)} {Object.keys(hooks[hookName][hook]).map((subHook, i) => <li key={i}>{subHook}</li>)}
</ul> </ul>
@ -46,12 +46,12 @@ export const HelpPage = () => {
</div> </div>
<h2><Trans i18nKey="admin_plugins.installed"/></h2> <h2><Trans i18nKey="admin_plugins.installed"/></h2>
<ul> <ul>
{helpData.installedPlugins.map((plugin, i) => <li key={i}>{plugin}</li>)} {helpData.installedPlugins.map((plugin, i) => <li key={plugin+i}>{plugin}</li>)}
</ul> </ul>
<h2><Trans i18nKey="admin_plugins_info.parts"/></h2> <h2><Trans i18nKey="admin_plugins_info.parts"/></h2>
<ul> <ul>
{helpData.installedParts.map((part, i) => <li key={i}>{part}</li>)} {helpData.installedParts.map((part, i) => <li key={part+i}>{part}</li>)}
</ul> </ul>
<h2><Trans i18nKey="admin_plugins_info.hooks"/></h2> <h2><Trans i18nKey="admin_plugins_info.hooks"/></h2>

View file

@ -100,7 +100,15 @@ export const HomePage = () => {
pluginsSocket!.on('results:search', (data: { pluginsSocket!.on('results:search', (data: {
results: PluginDef[] results: PluginDef[]
}) => { }) => {
if (Array.isArray(data.results) && data.results.length > 0) {
setPlugins(data.results) setPlugins(data.results)
} else {
useStore.getState().setToastState({
open: true,
title: "Error retrieving plugins",
success: false
})
}
}) })
@ -142,7 +150,7 @@ export const HomePage = () => {
<tbody style={{overflow: 'auto'}}> <tbody style={{overflow: 'auto'}}>
{sortedInstalledPlugins.map((plugin, index) => { {sortedInstalledPlugins.map((plugin, index) => {
return <tr key={index}> return <tr key={index}>
<td>{plugin.name}</td> <td><a rel="noopener noreferrer" href={`https://npmjs.com/${plugin.name}`} target="_blank">{plugin.name}</a></td>
<td>{plugin.version}</td> <td>{plugin.version}</td>
<td> <td>
{ {

View file

@ -8,7 +8,7 @@ export const SettingsPage = ()=>{
const settingsSocket = useStore(state=>state.settingsSocket) const settingsSocket = useStore(state=>state.settingsSocket)
const settings = useStore(state=>state.settings) const settings = useStore(state=>state.settings)
return <div> return <div className="settings-page">
<h1><Trans i18nKey="admin_settings.current"/></h1> <h1><Trans i18nKey="admin_settings.current"/></h1>
<textarea value={settings} className="settings" onChange={v => { <textarea value={settings} className="settings" onChange={v => {
useStore.getState().setSettings(v.target.value) useStore.getState().setSettings(v.target.value)

View file

@ -0,0 +1,76 @@
import {useEffect, useState} from "react";
import {SendHorizonal} from 'lucide-react'
import {useStore} from "../store/store.ts";
import * as Switch from '@radix-ui/react-switch';
import {ShoutType} from "../components/ShoutType.ts";
export const ShoutPage = ()=>{
const [totalUsers, setTotalUsers] = useState(0);
const [message, setMessage] = useState<string>("");
const [sticky, setSticky] = useState<boolean>(false);
const socket = useStore(state => state.settingsSocket);
const [shouts, setShouts] = useState<ShoutType[]>([]);
useEffect(() => {
fetch('/stats')
.then(response => response.json())
.then(data => setTotalUsers(data.totalUsers));
}, []);
useEffect(() => {
if(socket) {
socket.on('shout', (shout) => {
setShouts([...shouts, shout])
})
}
}, [socket, shouts])
const sendMessage = () => {
socket?.emit('shout', {
message,
sticky
});
setMessage('')
}
return (
<div>
<h1>Communication</h1>
{totalUsers > 0 && <p>There {totalUsers>1?"are":"is"} currently {totalUsers} user{totalUsers>1?"s":""} online</p>}
<div style={{height: '80vh', display: 'flex', flexDirection: 'column'}}>
<div style={{flexGrow: 1, backgroundColor: 'white', overflowY: "auto"}}>
{
shouts.map((shout) => {
return (
<div key={shout.data.payload.timestamp} className="message">
<div>{shout.data.payload.message.message}</div>
<div style={{display: 'flex'}}>
<div style={{flexGrow: 1}}></div>
<div
style={{color: "lightgray"}}>{new Date(shout.data.payload.timestamp).toLocaleTimeString()
+ " " + new Date(shout.data.payload.timestamp).toLocaleDateString()}</div>
</div>
</div>
)
})
}
</div>
<form onSubmit={(e) => {
e.preventDefault()
sendMessage()
}} className="send-message search-field" style={{display: 'flex', gap: '10px'}}>
<Switch.Root title="Change sticky message" className="SwitchRoot" checked={sticky}
onCheckedChange={() => {
setSticky(!sticky);
}}>
<Switch.Thumb className="SwitchThumb"/>
</Switch.Root>
<input required value={message} onChange={v=>setMessage(v.target.value)}
style={{width: '100%', paddingRight: '55px', backgroundColor: '#e0e0e0', flexGrow: 1}}/>
<SendHorizonal style={{bottom: '5px', right: '9px', color: '#0f775b'}} onClick={()=>sendMessage()}/>
</form>
</div>
</div>
)
}

View file

@ -28,7 +28,10 @@ export default defineConfig({
'/admin-auth/': { '/admin-auth/': {
target: 'http://localhost:9001', target: 'http://localhost:9001',
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path.replace(/^\/admin-prox/, '/admin/') },
'/stats': {
target: 'http://localhost:9001',
changeOrigin: true,
} }
} }
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "bin", "name": "bin",
"version": "2.0.2", "version": "2.0.3",
"description": "", "description": "",
"main": "checkAllPads.js", "main": "checkAllPads.js",
"directories": { "directories": {
@ -15,9 +15,9 @@
"ueberdb2": "^4.2.63" "ueberdb2": "^4.2.63"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.12.5", "@types/node": "^20.12.7",
"@types/semver": "^7.5.8", "@types/semver": "^7.5.8",
"typescript": "^5.4.4" "typescript": "^5.4.5"
}, },
"scripts": { "scripts": {
"checkPad": "node --import tsx checkPad.ts", "checkPad": "node --import tsx checkPad.ts",

View file

@ -28,7 +28,7 @@ Portal maps the internal userid to an etherpad author.
#### Request #### Request
```http ```http
GET /api/1/createAuthorIfNotExistsFor?apikey=secret&name=Michael&authorMapper=7 GET /api/1/createAuthorIfNotExistsFor?name=Michael&authorMapper=7
``` ```
@ -42,7 +42,7 @@ GET /api/1/createAuthorIfNotExistsFor?apikey=secret&name=Michael&authorMapper=7
> Portal maps the internal userid to an etherpad group: > Portal maps the internal userid to an etherpad group:
```http ```http
GET http://pad.domain/api/1/createGroupIfNotExistsFor?apikey=secret&groupMapper=7 GET http://pad.domain/api/1/createGroupIfNotExistsFor?groupMapper=7
``` ```
### Response ### Response
@ -56,7 +56,7 @@ GET http://pad.domain/api/1/createGroupIfNotExistsFor?apikey=secret&groupMapper=
#### Request #### Request
```http ```http
GET http://pad.domain/api/1/createGroupPad?apikey=secret&groupID=g.s8oes9dhwrvt0zif&padName=samplePad&text=This is the first sentence in the pad GET http://pad.domain/api/1/createGroupPad?groupID=g.s8oes9dhwrvt0zif&padName=samplePad&text=This is the first sentence in the pad
``` ```
#### Response #### Response
@ -70,7 +70,7 @@ GET http://pad.domain/api/1/createGroupPad?apikey=secret&groupID=g.s8oes9dhwrvt0
#### Request #### Request
```http ```http
GET http://pad.domain/api/1/createSession?apikey=secret&groupID=g.s8oes9dhwrvt0zif&authorID=a.s8oes9dhwrvt0zif&validUntil=1312201246 GET http://pad.domain/api/1/createSession?groupID=g.s8oes9dhwrvt0zif&authorID=a.s8oes9dhwrvt0zif&validUntil=1312201246
``` ```
### Response ### Response
@ -87,7 +87,7 @@ A portal (such as WordPress) wants to transform the contents of a pad that multi
Portal retrieves the contents of the pad for entry into the db as a blog post: Portal retrieves the contents of the pad for entry into the db as a blog post:
> Request: `http://pad.domain/api/1/getText?apikey=secret&padID=g.s8oes9dhwrvt0zif$123` > Request: `http://pad.domain/api/1/getText?&padID=g.s8oes9dhwrvt0zif$123`
> >
> Response: `{code: 0, message:"ok", data: {text:"Welcome Text"}}` > Response: `{code: 0, message:"ok", data: {text:"Welcome Text"}}`
@ -108,23 +108,23 @@ The API is accessible via HTTP. Starting from **1.8**, API endpoints can be invo
The URL of the HTTP request is of the form: `/api/$APIVERSION/$FUNCTIONNAME`. $APIVERSION depends on the endpoint you want to use. Depending on the verb you use (GET or POST) **parameters** can be passed differently. The URL of the HTTP request is of the form: `/api/$APIVERSION/$FUNCTIONNAME`. $APIVERSION depends on the endpoint you want to use. Depending on the verb you use (GET or POST) **parameters** can be passed differently.
When invoking via GET (mandatory until **1.7.5** included), parameters must be included in the query string (example: `/api/$APIVERSION/$FUNCTIONNAME?apikey=<APIKEY>&param1=value1`). Please note that starting with nodejs 8.14+ the total size of HTTP request headers has been capped to 8192 bytes. This limits the quantity of data that can be sent in an API request. When invoking via GET (mandatory until **1.7.5** included), parameters must be included in the query string (example: `/api/$APIVERSION/$FUNCTIONNAME?param1=value1`). Please note that starting with nodejs 8.14+ the total size of HTTP request headers has been capped to 8192 bytes. This limits the quantity of data that can be sent in an API request.
Starting from Etherpad **1.8** it is also possible to invoke the HTTP API via POST. In this case, querystring parameters will still be accepted, but **any parameter with the same name sent via POST will take precedence**. If you need to send large chunks of text (for example, for `setText()`) it is advisable to invoke via POST. Starting from Etherpad **1.8** it is also possible to invoke the HTTP API via POST. In this case, querystring parameters will still be accepted, but **any parameter with the same name sent via POST will take precedence**. If you need to send large chunks of text (for example, for `setText()`) it is advisable to invoke via POST.
Example with cURL using GET (toy example, no encoding): Example with cURL using GET (toy example, no encoding):
``` ```
curl "http://pad.domain/api/1/setText?apikey=secret&padID=padname&text=this_text_will_NOT_be_encoded_by_curl_use_next_example" curl "http://pad.domain/api/1/setText?padID=padname&text=this_text_will_NOT_be_encoded_by_curl_use_next_example"
``` ```
Example with cURL using GET (better example, encodes text): Example with cURL using GET (better example, encodes text):
``` ```
curl "http://pad.domain/api/1/setText?apikey=secret&padID=padname" --get --data-urlencode "text=Text sent via GET with proper encoding. For big documents, please use POST" curl "http://pad.domain/api/1/setText?padID=padname" --get --data-urlencode "text=Text sent via GET with proper encoding. For big documents, please use POST"
``` ```
Example with cURL using POST: Example with cURL using POST:
``` ```
curl "http://pad.domain/api/1/setText?apikey=secret&padID=padname" --data-urlencode "text=Text sent via POST with proper encoding. For big texts (>8 KB), use this method" curl "http://pad.domain/api/1/setText?padID=padname" --data-urlencode "text=Text sent via POST with proper encoding. For big texts (>8 KB), use this method"
``` ```
### Response Format ### Response Format
@ -161,7 +161,49 @@ Responses are valid JSON in the following format:
### Authentication ### Authentication
Authentication works via a token that is sent with each request as a post parameter. There is a single token per Etherpad deployment. This token will be random string, generated by Etherpad at the first start. It will be saved in APIKEY.txt in the root folder of Etherpad. Only Etherpad and the requesting application knows this key. Token management will not be exposed through this API. Authentication works via an OAuth token that is sent with each request as an Authorization header, i.e. `Authorization: Bearer YOUR_TOKEN`. You can add new clients that can sign in via the API by adding new entries to the sso section in the settings.json.
#### Example for browser login clients
This example illustrates how to add a new client that can sign in via the API using the browser login method. This method is used for users trying to sign in to the API via the browser. You can log in with the users in the settings.json file. The redirect URI is the URL where the user is redirected after the login. This is normally your etherpad instance url.
```json
{
"client_id": "admin_client",
"client_secret": "admin",
"grant_types": ["authorization_code"],
"response_types": ["code"],
"redirect_uris": ["http://my-etherpad-instance.com"],
}
```
#### Example for services
This example illustrates how to add a new client that can sign in via the API using the client credentials method. This method is used for services trying to sign in to the API where there is no browser.
E.g. a service that creates a pad for a user or a service that inserts a text into a pad. Just make sure that the secret is complex enough as anybody who knows the secret can access the API.
```json
{
"client_id": "client_credentials",
"redirect_uris": [],
"response_types": [],
"grant_types": ["code"],
"client_secret": "client_credentials",
"extraParams": [
{
"name": "admin",
"value": "true"
}
]
}
```
Obtain a Bearer token:
`curl --request POST --url 'https://your.server.tld/oidc/token' --header 'content-type: application/x-www-form-urlencoded' --data grant_type=client_credentials --data client_id=client_credentials --data client_secret=client_credentials`
### Node Interoperability ### Node Interoperability

View file

@ -278,58 +278,43 @@ docker run -d \
## Ready to use Docker Compose ## Ready to use Docker Compose
```yaml ```yaml
version: "3.8"
# Add this file to extend the docker-compose setup, e.g.:
# docker-compose build --no-cache
# docker-compose up -d --build --force-recreate
services: services:
app: app:
build: user: "0:0"
context: . image: etherpad/etherpad:latest
args:
ETHERPAD_PLUGINS:
# change from development to production if needed
target: development
tty: true tty: true
stdin_open: true stdin_open: true
volumes: volumes:
# no volume mapping of node_modules as otherwise the build-time installed plugins will be overwritten with the mount - plugins:/opt/etherpad-lite/src/plugin_packages
# the same applies to package.json and pnpm-lock.yaml in root dir as these would also get overwritten and build time installed plugins will be removed - etherpad-var:/opt/etherpad-lite/var
- ./src:/opt/etherpad-lite/src
- ./bin:/opt/etherpad-lite/bin
depends_on: depends_on:
- postgres - postgres
environment: environment:
# change from development to production if needed NODE_ENV: production
NODE_ENV: development ADMIN_PASSWORD: ${DOCKER_COMPOSE_APP_ADMIN_PASSWORD:-admin}
ADMIN_PASSWORD: ${DOCKER_COMPOSE_APP_DEV_ADMIN_PASSWORD} DB_CHARSET: ${DOCKER_COMPOSE_APP_DB_CHARSET:-utf8mb4}
DB_CHARSET: ${DOCKER_COMPOSE_APP_DEV_ENV_DB_CHARSET:-utf8mb4}
DB_HOST: postgres DB_HOST: postgres
DB_NAME: ${DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_DATABASE:?} DB_NAME: ${DOCKER_COMPOSE_POSTGRES_DATABASE:-etherpad}
DB_PASS: ${DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_PASSWORD:?} DB_PASS: ${DOCKER_COMPOSE_POSTGRES_PASSWORD:-admin}
DB_PORT: ${DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_PORT:-5432} DB_PORT: ${DOCKER_COMPOSE_POSTGRES_PORT:-5432}
DB_TYPE: "postgres" DB_TYPE: "postgres"
DB_USER: ${DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_USER:?} DB_USER: ${DOCKER_COMPOSE_POSTGRES_USER:-admin}
# For now, the env var DEFAULT_PAD_TEXT cannot be unset or empty; it seems to be mandatory in the latest version of etherpad # For now, the env var DEFAULT_PAD_TEXT cannot be unset or empty; it seems to be mandatory in the latest version of etherpad
DEFAULT_PAD_TEXT: ${DOCKER_COMPOSE_APP_DEV_ENV_DEFAULT_PAD_TEXT:- } DEFAULT_PAD_TEXT: ${DOCKER_COMPOSE_APP_DEFAULT_PAD_TEXT:- }
DISABLE_IP_LOGGING: ${DOCKER_COMPOSE_APP_DEV_ENV_DISABLE_IP_LOGGING:-true} DISABLE_IP_LOGGING: ${DOCKER_COMPOSE_APP_DISABLE_IP_LOGGING:-false}
SOFFICE: ${DOCKER_COMPOSE_APP_DEV_ENV_SOFFICE:-null} SOFFICE: ${DOCKER_COMPOSE_APP_SOFFICE:-null}
TRUST_PROXY: ${DOCKER_COMPOSE_APP_DEV_ENV_TRUST_PROXY:-true} TRUST_PROXY: ${DOCKER_COMPOSE_APP_TRUST_PROXY:-true}
restart: always restart: always
ports: ports:
- "${DOCKER_COMPOSE_APP_DEV_PORT_PUBLISHED:-9001}:${DOCKER_COMPOSE_APP_DEV_PORT_TARGET:-9001}" - "${DOCKER_COMPOSE_APP_PORT_PUBLISHED:-9001}:${DOCKER_COMPOSE_APP_PORT_TARGET:-9001}"
postgres: postgres:
image: postgres:15-alpine image: postgres:15-alpine
# Pass config parameters to the mysql server.
# Find more information below when you need to generate the ssl-relevant file your self
environment: environment:
POSTGRES_DB: ${DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_DATABASE:?} POSTGRES_DB: ${DOCKER_COMPOSE_POSTGRES_DATABASE:-etherpad}
POSTGRES_PASSWORD: ${DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_PASSWORD:?} POSTGRES_PASSWORD: ${DOCKER_COMPOSE_POSTGRES_PASSWORD:-admin}
POSTGRES_PORT: ${DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_PORT:-5432} POSTGRES_PORT: ${DOCKER_COMPOSE_POSTGRES_PORT:-5432}
POSTGRES_USER: ${DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_USER:?} POSTGRES_USER: ${DOCKER_COMPOSE_POSTGRES_USER:-admin}
PGDATA: /var/lib/postgresql/data/pgdata PGDATA: /var/lib/postgresql/data/pgdata
restart: always restart: always
# Exposing the port is not needed unless you want to access this database instance from the host. # Exposing the port is not needed unless you want to access this database instance from the host.
@ -341,4 +326,6 @@ services:
volumes: volumes:
postgres_data: postgres_data:
plugins:
etherpad-var:
``` ```

View file

@ -1,6 +1,6 @@
{ {
"devDependencies": { "devDependencies": {
"vitepress": "^1.0.2" "vitepress": "^1.1.3"
}, },
"scripts": { "scripts": {
"docs:dev": "vitepress dev", "docs:dev": "vitepress dev",

63
docker-compose.dev.yml Normal file
View file

@ -0,0 +1,63 @@
version: "3.8"
# Add this file to extend the docker-compose setup, e.g.:
# docker-compose build --no-cache
# docker-compose up -d --build --force-recreate
services:
app:
build:
context: .
args:
ETHERPAD_PLUGINS:
# change from development to production if needed
target: development
tty: true
stdin_open: true
volumes:
# no volume mapping of node_modules as otherwise the build-time installed plugins will be overwritten with the mount
# the same applies to package.json and pnpm-lock.yaml in root dir as these would also get overwritten and build time installed plugins will be removed
- ./src:/opt/etherpad-lite/src
- ./bin:/opt/etherpad-lite/bin
depends_on:
- postgres
environment:
# change from development to production if needed
NODE_ENV: development
ADMIN_PASSWORD: ${DOCKER_COMPOSE_APP_DEV_ADMIN_PASSWORD}
DB_CHARSET: ${DOCKER_COMPOSE_APP_DEV_ENV_DB_CHARSET:-utf8mb4}
DB_HOST: postgres
DB_NAME: ${DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_DATABASE:?}
DB_PASS: ${DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_PASSWORD:?}
DB_PORT: ${DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_PORT:-5432}
DB_TYPE: "postgres"
DB_USER: ${DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_USER:?}
# For now, the env var DEFAULT_PAD_TEXT cannot be unset or empty; it seems to be mandatory in the latest version of etherpad
DEFAULT_PAD_TEXT: ${DOCKER_COMPOSE_APP_DEV_ENV_DEFAULT_PAD_TEXT:- }
DISABLE_IP_LOGGING: ${DOCKER_COMPOSE_APP_DEV_ENV_DISABLE_IP_LOGGING:-true}
SOFFICE: ${DOCKER_COMPOSE_APP_DEV_ENV_SOFFICE:-null}
TRUST_PROXY: ${DOCKER_COMPOSE_APP_DEV_ENV_TRUST_PROXY:-true}
restart: always
ports:
- "${DOCKER_COMPOSE_APP_DEV_PORT_PUBLISHED:-9001}:${DOCKER_COMPOSE_APP_DEV_PORT_TARGET:-9001}"
postgres:
image: postgres:15-alpine
# Pass config parameters to the mysql server.
# Find more information below when you need to generate the ssl-relevant file your self
environment:
POSTGRES_DB: ${DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_DATABASE:?}
POSTGRES_PASSWORD: ${DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_PASSWORD:?}
POSTGRES_PORT: ${DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_PORT:-5432}
POSTGRES_USER: ${DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_USER:?}
PGDATA: /var/lib/postgresql/data/pgdata
restart: always
# Exposing the port is not needed unless you want to access this database instance from the host.
# Be careful when other postgres docker container are running on the same port
# ports:
# - "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data/pgdata
volumes:
postgres_data:

View file

@ -1,55 +1,40 @@
version: "3.8"
# Add this file to extend the docker-compose setup, e.g.:
# docker-compose build --no-cache
# docker-compose up -d --build --force-recreate
services: services:
app: app:
build: user: "0:0"
context: . image: etherpad/etherpad:latest
args:
ETHERPAD_PLUGINS:
# change from development to production if needed
target: development
tty: true tty: true
stdin_open: true stdin_open: true
volumes: volumes:
# no volume mapping of node_modules as otherwise the build-time installed plugins will be overwritten with the mount - plugins:/opt/etherpad-lite/src/plugin_packages
# the same applies to package.json and pnpm-lock.yaml in root dir as these would also get overwritten and build time installed plugins will be removed - etherpad-var:/opt/etherpad-lite/var
- ./src:/opt/etherpad-lite/src
- ./bin:/opt/etherpad-lite/bin
depends_on: depends_on:
- postgres - postgres
environment: environment:
# change from development to production if needed NODE_ENV: production
NODE_ENV: development ADMIN_PASSWORD: ${DOCKER_COMPOSE_APP_ADMIN_PASSWORD:-admin}
ADMIN_PASSWORD: ${DOCKER_COMPOSE_APP_DEV_ADMIN_PASSWORD} DB_CHARSET: ${DOCKER_COMPOSE_APP_DB_CHARSET:-utf8mb4}
DB_CHARSET: ${DOCKER_COMPOSE_APP_DEV_ENV_DB_CHARSET:-utf8mb4}
DB_HOST: postgres DB_HOST: postgres
DB_NAME: ${DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_DATABASE:?} DB_NAME: ${DOCKER_COMPOSE_POSTGRES_DATABASE:-etherpad}
DB_PASS: ${DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_PASSWORD:?} DB_PASS: ${DOCKER_COMPOSE_POSTGRES_PASSWORD:-admin}
DB_PORT: ${DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_PORT:-5432} DB_PORT: ${DOCKER_COMPOSE_POSTGRES_PORT:-5432}
DB_TYPE: "postgres" DB_TYPE: "postgres"
DB_USER: ${DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_USER:?} DB_USER: ${DOCKER_COMPOSE_POSTGRES_USER:-admin}
# For now, the env var DEFAULT_PAD_TEXT cannot be unset or empty; it seems to be mandatory in the latest version of etherpad # For now, the env var DEFAULT_PAD_TEXT cannot be unset or empty; it seems to be mandatory in the latest version of etherpad
DEFAULT_PAD_TEXT: ${DOCKER_COMPOSE_APP_DEV_ENV_DEFAULT_PAD_TEXT:- } DEFAULT_PAD_TEXT: ${DOCKER_COMPOSE_APP_DEFAULT_PAD_TEXT:- }
DISABLE_IP_LOGGING: ${DOCKER_COMPOSE_APP_DEV_ENV_DISABLE_IP_LOGGING:-true} DISABLE_IP_LOGGING: ${DOCKER_COMPOSE_APP_DISABLE_IP_LOGGING:-false}
SOFFICE: ${DOCKER_COMPOSE_APP_DEV_ENV_SOFFICE:-null} SOFFICE: ${DOCKER_COMPOSE_APP_SOFFICE:-null}
TRUST_PROXY: ${DOCKER_COMPOSE_APP_DEV_ENV_TRUST_PROXY:-true} TRUST_PROXY: ${DOCKER_COMPOSE_APP_TRUST_PROXY:-true}
restart: always restart: always
ports: ports:
- "${DOCKER_COMPOSE_APP_DEV_PORT_PUBLISHED:-9001}:${DOCKER_COMPOSE_APP_DEV_PORT_TARGET:-9001}" - "${DOCKER_COMPOSE_APP_PORT_PUBLISHED:-9001}:${DOCKER_COMPOSE_APP_PORT_TARGET:-9001}"
postgres: postgres:
image: postgres:15-alpine image: postgres:15-alpine
# Pass config parameters to the mysql server.
# Find more information below when you need to generate the ssl-relevant file your self
environment: environment:
POSTGRES_DB: ${DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_DATABASE:?} POSTGRES_DB: ${DOCKER_COMPOSE_POSTGRES_DATABASE:-etherpad}
POSTGRES_PASSWORD: ${DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_PASSWORD:?} POSTGRES_PASSWORD: ${DOCKER_COMPOSE_POSTGRES_PASSWORD:-admin}
POSTGRES_PORT: ${DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_PORT:-5432} POSTGRES_PORT: ${DOCKER_COMPOSE_POSTGRES_PORT:-5432}
POSTGRES_USER: ${DOCKER_COMPOSE_POSTGRES_DEV_ENV_POSTGRES_USER:?} POSTGRES_USER: ${DOCKER_COMPOSE_POSTGRES_USER:-admin}
PGDATA: /var/lib/postgresql/data/pgdata PGDATA: /var/lib/postgresql/data/pgdata
restart: always restart: always
# Exposing the port is not needed unless you want to access this database instance from the host. # Exposing the port is not needed unless you want to access this database instance from the host.
@ -61,3 +46,5 @@ services:
volumes: volumes:
postgres_data: postgres_data:
plugins:
etherpad-var:

View file

@ -43,6 +43,6 @@
"type": "git", "type": "git",
"url": "https://github.com/ether/etherpad-lite.git" "url": "https://github.com/ether/etherpad-lite.git"
}, },
"version": "2.0.2", "version": "2.0.3",
"license": "Apache-2.0" "license": "Apache-2.0"
} }

File diff suppressed because it is too large Load diff

View file

@ -669,5 +669,16 @@
"redirect_uris": ["${USER_REDIRECT:http://localhost:9001/}"] "redirect_uris": ["${USER_REDIRECT:http://localhost:9001/}"]
} }
] ]
},
/* Set the time to live for the tokens
This is the time of seconds a user is logged into Etherpad
"ttl": {
"AccessToken": 3600,
"AuthorizationCode": 600,
"ClientCredentials": 3600,
"IdToken": 3600,
"RefreshToken": 86400
} }
*/
} }

View file

@ -671,4 +671,15 @@
} }
] ]
} }
/* Set the time to live for the tokens
This is the time of seconds a user is logged into Etherpad
"ttl": {
"AccessToken": 3600,
"AuthorizationCode": 600,
"ClientCredentials": 3600,
"IdToken": 3600,
"RefreshToken": 86400
}
*/
} }

View file

@ -112,6 +112,12 @@
"hooks": { "hooks": {
"expressPreSession": "ep_etherpad-lite/node/hooks/express/openapi" "expressPreSession": "ep_etherpad-lite/node/hooks/express/openapi"
} }
},
{
"name": "ep_message_all",
"client_hooks": {
"handleClientMessage_shoutMessage": "ep_etherpad-lite/static/js/messageHandler"
}
} }
] ]
} }

View file

@ -8,6 +8,7 @@
"MuratTheTurkish", "MuratTheTurkish",
"Mushviq Abdulla", "Mushviq Abdulla",
"NMW03", "NMW03",
"Nemoralis",
"Neriman2003", "Neriman2003",
"Vesely35", "Vesely35",
"Wertuose" "Wertuose"

View file

@ -55,10 +55,10 @@
"pad.toolbar.import_export.title": "Файлланы башха форматларын (а/дан) импорт/экспорт", "pad.toolbar.import_export.title": "Файлланы башха форматларын (а/дан) импорт/экспорт",
"pad.toolbar.timeslider.title": "Заман шкала", "pad.toolbar.timeslider.title": "Заман шкала",
"pad.toolbar.savedRevision.title": "Версияны сакъла", "pad.toolbar.savedRevision.title": "Версияны сакъла",
"pad.toolbar.settings.title": "Джарашдырыула", "pad.toolbar.settings.title": "Джарашдырыўла",
"pad.toolbar.embed.title": "Бу блокнотну Джай эмда Ичине сал", "pad.toolbar.embed.title": "Бу блокнотну Джай эмда Ичине сал",
"pad.toolbar.showusers.title": "Хайырланыучуланы бу блокнотда кёргюзт", "pad.toolbar.showusers.title": "Хайырланыучуланы бу блокнотда кёргюзт",
"pad.colorpicker.save": "Сакъла", "pad.colorpicker.save": "Сакъландыр",
"pad.colorpicker.cancel": "Ызына ал", "pad.colorpicker.cancel": "Ызына ал",
"pad.loading": "Джюклениу...", "pad.loading": "Джюклениу...",
"pad.noCookie": "Куки табылмадыла. Бразуеригизде кукилени бир джандырсагъыз! Сизни кириулеригизни арасында сессиягъыз эмда джарашдырыуларыгъыз сакъланныкъ тюлдюле. Буну чуруму, бир къауум браузерледе Etherpad iFrame ичинде болгъаны болургъа болур. Тилейбиз, Etherpad эмда аны башындагъы iFrame бир тюбдоменде/доменде болгъанындан ишексиз болугъуз.", "pad.noCookie": "Куки табылмадыла. Бразуеригизде кукилени бир джандырсагъыз! Сизни кириулеригизни арасында сессиягъыз эмда джарашдырыуларыгъыз сакъланныкъ тюлдюле. Буну чуруму, бир къауум браузерледе Etherpad iFrame ичинде болгъаны болургъа болур. Тилейбиз, Etherpad эмда аны башындагъы iFrame бир тюбдоменде/доменде болгъанындан ишексиз болугъуз.",
@ -90,7 +90,7 @@
"pad.modals.reconnecting": "Блокнотугъузгъа джангыдан байлана турады...", "pad.modals.reconnecting": "Блокнотугъузгъа джангыдан байлана турады...",
"pad.modals.forcereconnect": "Джангыдан зор бла байланыу", "pad.modals.forcereconnect": "Джангыдан зор бла байланыу",
"pad.modals.reconnecttimer": "Джангыдан байланыргъа кюрешеди", "pad.modals.reconnecttimer": "Джангыдан байланыргъа кюрешеди",
"pad.modals.cancel": "Ызына алыу", "pad.modals.cancel": "Ызына ал",
"pad.modals.userdup": "Башха терезеде ачыкъды", "pad.modals.userdup": "Башха терезеде ачыкъды",
"pad.modals.userdup.explanation": "Бу блокнот, бу компьютерде бирден аслам бразуре терезеде ачылгъаннга ушайды.", "pad.modals.userdup.explanation": "Бу блокнот, бу компьютерде бирден аслам бразуре терезеде ачылгъаннга ушайды.",
"pad.modals.userdup.advice": "Бу терезени хайырланыб джангыдан байлан", "pad.modals.userdup.advice": "Бу терезени хайырланыб джангыдан байлан",

View file

@ -73,7 +73,7 @@
"pad.settings.chatandusers": "Prikaži klepet in uporabnike", "pad.settings.chatandusers": "Prikaži klepet in uporabnike",
"pad.settings.colorcheck": "Barve avtorstva", "pad.settings.colorcheck": "Barve avtorstva",
"pad.settings.linenocheck": "Številke vrstic", "pad.settings.linenocheck": "Številke vrstic",
"pad.settings.rtlcheck": "Ali naj se vsebina bera od desne proti levi?", "pad.settings.rtlcheck": "Ali naj se vsebina bere od desne proti levi?",
"pad.settings.fontType": "Vrsta pisave:", "pad.settings.fontType": "Vrsta pisave:",
"pad.settings.fontType.normal": "Normalno", "pad.settings.fontType.normal": "Normalno",
"pad.settings.language": "Jezik:", "pad.settings.language": "Jezik:",

View file

@ -88,7 +88,7 @@ exports.socketio = () => {
const sessioninfos:MapArrayType<any> = {}; const sessioninfos:MapArrayType<any> = {};
exports.sessioninfos = sessioninfos; exports.sessioninfos = sessioninfos;
stats.gauge('totalUsers', () => socketio ? socketio.sockets.size : 0); stats.gauge('totalUsers', () => socketio ? socketio.engine.clientsCount : 0);
stats.gauge('activePads', () => { stats.gauge('activePads', () => {
const padIds = new Set(); const padIds = new Set();
for (const {padId} of Object.values(sessioninfos)) { for (const {padId} of Object.values(sessioninfos)) {

View file

@ -87,6 +87,7 @@ exports.socketio = (hookName:string, args:ArgsExpressType, cb:Function) => {
}); });
}); });
socket.on('uninstall', (pluginName:string) => { socket.on('uninstall', (pluginName:string) => {
uninstall(pluginName, (err:ErrorCaused) => { uninstall(pluginName, (err:ErrorCaused) => {
if (err) console.warn(err.stack || err.toString()); if (err) console.warn(err.stack || err.toString());

View file

@ -45,6 +45,28 @@ exports.socketio = (hookName:string, {io}:any) => {
}); });
type ShoutMessage = {
message: string,
sticky: boolean,
}
socket.on('shout', (message: ShoutMessage) => {
const messageToSend = {
type: "COLLABROOM",
data: {
type: "shoutMessage",
payload: {
message: message,
timestamp: Date.now()
}
}
}
io.of('/settings').emit('shout', messageToSend);
io.sockets.emit('shout', messageToSend);
})
socket.on('help', () => { socket.on('help', () => {
const gitCommit = settings.getGitCommit(); const gitCommit = settings.getGitCommit();
const epVersion = settings.getEpVersion(); const epVersion = settings.getEpVersion();
@ -130,7 +152,8 @@ exports.socketio = (hookName:string, {io}:any) => {
lastEdited, lastEdited,
userCount, userCount,
revisionNumber revisionNumber
}})); }
}));
} else { } else {
const currentWinners: PadQueryResult[] = [] const currentWinners: PadQueryResult[] = []
let queryOffsetCounter = 0 let queryOffsetCounter = 0
@ -204,7 +227,6 @@ exports.socketio = (hookName:string, {io}:any) => {
}; };
const searchPad = async (query: PadSearchQuery) => { const searchPad = async (query: PadSearchQuery) => {
} }

View file

@ -9,6 +9,7 @@ import express, {Request, Response} from 'express';
import {format} from 'url' import {format} from 'url'
import {ParsedUrlQuery} from "node:querystring"; import {ParsedUrlQuery} from "node:querystring";
import {Http2ServerRequest, Http2ServerResponse} from "node:http2"; import {Http2ServerRequest, Http2ServerResponse} from "node:http2";
import {MapArrayType} from "../types/MapType";
const configuration: Configuration = { const configuration: Configuration = {
scopes: ['openid', 'profile', 'email'], scopes: ['openid', 'profile', 'email'],
@ -19,7 +20,6 @@ const configuration: Configuration = {
is_admin: boolean; is_admin: boolean;
} }
} }
const usersArray1 = Object.keys(users).map((username) => ({ const usersArray1 = Object.keys(users).map((username) => ({
username, username,
...users[username] ...users[username]
@ -47,13 +47,7 @@ const configuration: Configuration = {
} as Account } as Account
} }
}, },
ttl:{ ttl: settings.ttl,
AccessToken: 1 * 60 * 60, // 1 hour in seconds
AuthorizationCode: 10 * 60, // 10 minutes in seconds
ClientCredentials: 1 * 60 * 60, // 1 hour in seconds
IdToken: 1 * 60 * 60, // 1 hour in seconds
RefreshToken: 1 * 24 * 60 * 60, // 1 day in seconds
},
claims: { claims: {
openid: ['sub'], openid: ['sub'],
email: ['email'], email: ['email'],
@ -99,28 +93,29 @@ export const expressCreateServer = async (hookName: string, args: ArgsExpressTyp
features:{ features:{
userinfo: {enabled: true}, userinfo: {enabled: true},
claimsParameter: {enabled: true}, claimsParameter: {enabled: true},
clientCredentials: {enabled: true},
devInteractions: {enabled: false}, devInteractions: {enabled: false},
resourceIndicators: {enabled: true, defaultResource(ctx) { resourceIndicators: {enabled: true, defaultResource(ctx) {
return ctx.origin; return ctx.origin;
}, },
getResourceServerInfo(ctx, resourceIndicator, client) { getResourceServerInfo(ctx, resourceIndicator, client) {
return { return {
scope: client.scope as string, scope: "openid",
audience: 'account', audience: 'account',
accessTokenFormat: 'jwt', accessTokenFormat: 'jwt',
}; };
}, },
useGrantedResource(ctx, model) { useGrantedResource(ctx, model) {
return true; return true;
},}, },
},
jwtResponseModes: {enabled: true}, jwtResponseModes: {enabled: true},
}, },
clientBasedCORS: (ctx, origin, client) => { clientBasedCORS: (ctx, origin, client) => {
return true return true
}, },
extraParams: [],
extraTokenClaims: async (ctx, token) => { extraTokenClaims: async (ctx, token) => {
if(token.kind === 'AccessToken') { if(token.kind === 'AccessToken') {
// Add your custom claims here. For example: // Add your custom claims here. For example:
const users = settings.users as { const users = settings.users as {
@ -139,6 +134,19 @@ export const expressCreateServer = async (hookName: string, args: ArgsExpressTyp
return { return {
admin: account?.is_admin admin: account?.is_admin
}; };
} else if (token.kind === "ClientCredentials") {
let extraParams: MapArrayType<string> = {}
settings.sso.clients
.filter((client:any) => client.client_id === token.clientId)
.forEach((client:any) => {
if(client.extraParams !== undefined) {
client.extraParams.forEach((param:any) => {
extraParams[param.name] = param.value
})
}
})
return extraParams
} }
}, },
clients: settings.sso.clients clients: settings.sso.clients
@ -252,7 +260,7 @@ export const expressCreateServer = async (hookName: string, args: ArgsExpressTyp
args.app.use('/views/', express.static(path.join(settings.root,'src','static', 'oidc'), {maxAge: 1000 * 60 * 60 * 24})); args.app.use('/views/', express.static(path.join(settings.root,'src','static', 'oidc'), {maxAge: 1000 * 60 * 60 * 24}));
/*
oidc.on('authorization.error', (ctx, error) => { oidc.on('authorization.error', (ctx, error) => {
console.log('authorization.error', error); console.log('authorization.error', error);
}) })
@ -268,7 +276,7 @@ export const expressCreateServer = async (hookName: string, args: ArgsExpressTyp
}) })
oidc.on('revocation.error', (ctx, error) => { oidc.on('revocation.error', (ctx, error) => {
console.log('revocation.error', error); console.log('revocation.error', error);
})*/ })
args.app.use("/oidc", oidc.callback()); args.app.use("/oidc", oidc.callback());
//cb(); //cb();
} }

View file

@ -27,6 +27,7 @@ import {ErrorCaused} from "./types/ErrorCaused";
import log4js from 'log4js'; import log4js from 'log4js';
import pkg from '../package.json'; import pkg from '../package.json';
import {checkForMigration} from "../static/js/pluginfw/installer"; import {checkForMigration} from "../static/js/pluginfw/installer";
import axios from "axios";
const settings = require('./utils/Settings'); const settings = require('./utils/Settings');
@ -37,6 +38,28 @@ if (settings.dumpOnUncleanExit) {
wtfnode = require('wtfnode'); wtfnode = require('wtfnode');
} }
const addProxyToAxios = (url: URL) => {
axios.defaults.proxy = {
host: url.hostname,
port: Number(url.port),
protocol: url.protocol,
}
}
if(process.env['http_proxy']) {
console.log("Using proxy: " + process.env['http_proxy'])
addProxyToAxios(new URL(process.env['http_proxy']));
}
if (process.env['https_proxy']) {
console.log("Using proxy: " + process.env['https_proxy'])
addProxyToAxios(new URL(process.env['https_proxy']));
}
/* /*
* early check for version compatibility before calling * early check for version compatibility before calling
* any modules that require newer versions of NodeJS * any modules that require newer versions of NodeJS

View file

@ -98,6 +98,16 @@ exports.title = 'Etherpad';
*/ */
exports.favicon = null; exports.favicon = null;
exports.ttl = {
AccessToken: 1 * 60 * 60, // 1 hour in seconds
AuthorizationCode: 10 * 60, // 10 minutes in seconds
ClientCredentials: 1 * 60 * 60, // 1 hour in seconds
IdToken: 1 * 60 * 60, // 1 hour in seconds
RefreshToken: 1 * 24 * 60 * 60, // 1 day in seconds
}
/* /*
* Skin name. * Skin name.
* *

View file

@ -1,7 +1,7 @@
'use strict'; 'use strict';
const semver = require('semver'); const semver = require('semver');
const settings = require('./Settings'); const settings = require('./Settings');
const axios = require('axios'); import axios from 'axios';
const headers = { const headers = {
'User-Agent': 'Etherpad/' + settings.getEpVersion(), 'User-Agent': 'Etherpad/' + settings.getEpVersion(),
} }
@ -17,7 +17,7 @@ let lastLoadingTime: number | null = null;
const loadEtherpadInformations = () => { const loadEtherpadInformations = () => {
if (lastLoadingTime !== null && Date.now() - lastLoadingTime < updateInterval) { if (lastLoadingTime !== null && Date.now() - lastLoadingTime < updateInterval) {
return Promise.resolve(infos); return infos;
} }
return axios.get('https://static.etherpad.org/info.json', {headers: headers}) return axios.get('https://static.etherpad.org/info.json', {headers: headers})
@ -29,10 +29,10 @@ const loadEtherpadInformations = () => {
} }
lastLoadingTime = Date.now(); lastLoadingTime = Date.now();
return await Promise.resolve(infos); return infos;
}) })
.catch(async (err: Error) => { .catch(async (err: Error) => {
return await Promise.reject(err); throw err;
}); });
} }
@ -43,15 +43,15 @@ exports.getLatestVersion = () => {
}; };
exports.needsUpdate = async (cb?: Function) => { exports.needsUpdate = async (cb?: Function) => {
await loadEtherpadInformations() try {
.then((info:Infos) => { const info = await loadEtherpadInformations()
if (semver.gt(info.latestVersion, settings.getEpVersion())) { if (semver.gt(info!.latestVersion, settings.getEpVersion())) {
if (cb) return cb(true); if (cb) return cb(true);
} }
}).catch((err: Error) => { } catch (err) {
console.error(`Can not perform Etherpad update check: ${err}`); console.error(`Can not perform Etherpad update check: ${err}`);
if (cb) return cb(false); if (cb) return cb(false);
}); }
}; };
exports.check = () => { exports.check = () => {

View file

@ -35,7 +35,7 @@
"clean-css": "^5.3.3", "clean-css": "^5.3.3",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"cross-spawn": "^7.0.3", "cross-spawn": "^7.0.3",
"ejs": "^3.1.9", "ejs": "^3.1.10",
"etherpad-require-kernel": "^1.0.16", "etherpad-require-kernel": "^1.0.16",
"etherpad-yajsml": "0.0.12", "etherpad-yajsml": "0.0.12",
"express": "4.19.2", "express": "4.19.2",
@ -51,7 +51,7 @@
"jsonminify": "0.4.2", "jsonminify": "0.4.2",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"languages4translatewiki": "0.1.3", "languages4translatewiki": "0.1.3",
"live-plugin-manager": "^0.19.0", "live-plugin-manager": "^0.20.0",
"lodash.clonedeep": "4.5.0", "lodash.clonedeep": "4.5.0",
"log4js": "^6.9.1", "log4js": "^6.9.1",
"measured-core": "^2.0.0", "measured-core": "^2.0.0",
@ -83,7 +83,7 @@
"etherpad-lite": "node/server.ts" "etherpad-lite": "node/server.ts"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.43.0", "@playwright/test": "^1.43.1",
"@types/async": "^3.2.24", "@types/async": "^3.2.24",
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/formidable": "^3.4.5", "@types/formidable": "^3.4.5",
@ -91,7 +91,7 @@
"@types/jsdom": "^21.1.6", "@types/jsdom": "^21.1.6",
"@types/jsonwebtoken": "^9.0.6", "@types/jsonwebtoken": "^9.0.6",
"@types/mocha": "^10.0.6", "@types/mocha": "^10.0.6",
"@types/node": "^20.12.5", "@types/node": "^20.12.7",
"@types/oidc-provider": "^8.4.4", "@types/oidc-provider": "^8.4.4",
"@types/semver": "^7.5.8", "@types/semver": "^7.5.8",
"@types/sinon": "^17.0.3", "@types/sinon": "^17.0.3",
@ -108,7 +108,7 @@
"sinon": "^17.0.1", "sinon": "^17.0.1",
"split-grid": "^1.0.11", "split-grid": "^1.0.11",
"supertest": "^6.3.4", "supertest": "^6.3.4",
"typescript": "^5.4.4" "typescript": "^5.4.5"
}, },
"engines": { "engines": {
"node": ">=18.18.2", "node": ">=18.18.2",
@ -132,6 +132,6 @@
"test-admin": "npx playwright test tests/frontend-new/admin-spec --workers 1", "test-admin": "npx playwright test tests/frontend-new/admin-spec --workers 1",
"test-admin:ui": "npx playwright test tests/frontend-new/admin-spec --ui --workers 1" "test-admin:ui": "npx playwright test tests/frontend-new/admin-spec --ui --workers 1"
}, },
"version": "2.0.2", "version": "2.0.3",
"license": "Apache-2.0" "license": "Apache-2.0"
} }

View file

@ -534,7 +534,7 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
// As the concept of parent's doesn't exist when processing each domline... // As the concept of parent's doesn't exist when processing each domline...
} }
} else { } else {
// Below needs more testin if it's neccesary as _exitList should take care of this. // Below needs more testin if it's necessary as _exitList should take care of this.
// delete state.start; // delete state.start;
// delete state.listNesting; // delete state.listNesting;
// _recalcAttribString(state); // _recalcAttribString(state);

View file

@ -167,7 +167,7 @@ const getUrlVars = () => new URL(window.location.href).searchParams;
const sendClientReady = (isReconnect) => { const sendClientReady = (isReconnect) => {
let padId = document.location.pathname.substring(document.location.pathname.lastIndexOf('/') + 1); let padId = document.location.pathname.substring(document.location.pathname.lastIndexOf('/') + 1);
// unescape neccesary due to Safari and Opera interpretation of spaces // unescape necessary due to Safari and Opera interpretation of spaces
padId = decodeURIComponent(padId); padId = decodeURIComponent(padId);
if (!isReconnect) { if (!isReconnect) {
@ -213,7 +213,7 @@ const sendClientReady = (isReconnect) => {
const handshake = async () => { const handshake = async () => {
let receivedClientVars = false; let receivedClientVars = false;
let padId = document.location.pathname.substring(document.location.pathname.lastIndexOf('/') + 1); let padId = document.location.pathname.substring(document.location.pathname.lastIndexOf('/') + 1);
// unescape neccesary due to Safari and Opera interpretation of spaces // unescape necessary due to Safari and Opera interpretation of spaces
padId = decodeURIComponent(padId); padId = decodeURIComponent(padId);
// padId is used here for sharding / scaling. We prefix the padId with padId: so it's clear // padId is used here for sharding / scaling. We prefix the padId with padId: so it's clear
@ -250,11 +250,27 @@ const handshake = async () => {
socket.on('disconnect', (reason) => { socket.on('disconnect', (reason) => {
// The socket.io client will automatically try to reconnect for all reasons other than "io // The socket.io client will automatically try to reconnect for all reasons other than "io
// server disconnect". // server disconnect".
if (reason !== 'io server disconnect') return; console.log(`Socket disconnected: ${reason}`)
if (reason !== 'io server disconnect' || reason !== 'ping timeout') return;
socketReconnecting(); socketReconnecting();
socket.connect(); socket.connect();
}); });
socket.on('shout', (obj) => {
if(obj.type === "COLLABROOM") {
let date = new Date(obj.data.payload.timestamp);
$.gritter.add({
// (string | mandatory) the heading of the notification
title: 'Admin message',
// (string | mandatory) the text inside the notification
text: '[' + date.toLocaleTimeString() + ']: ' + obj.data.payload.message.message,
// (bool | optional) if you want it to fade out on its own or just sit there
sticky: obj.data.payload.message.sticky
});
}
})
socket.on('reconnecting', socketReconnecting); socket.on('reconnecting', socketReconnecting);
socket.on('reconnect_failed', (error) => { socket.on('reconnect_failed', (error) => {

View file

@ -225,7 +225,7 @@ exports.callAll = (hookName, context) => {
// provides settles (resolves or rejects). If a hook function attempts to settle again (e.g., call // provides settles (resolves or rejects). If a hook function attempts to settle again (e.g., call
// the callback again, or return a value and also call the callback) then the second attempt has no // the callback again, or return a value and also call the callback) then the second attempt has no
// effect except either an error message is logged or an Error object is thrown depending on whether // effect except either an error message is logged or an Error object is thrown depending on whether
// the the subsequent attempt is a duplicate (same value or error) or different, respectively. // the subsequent attempt is a duplicate (same value or error) or different, respectively.
// //
// See the tests in src/tests/backend/specs/hooks.js for examples of supported and prohibited // See the tests in src/tests/backend/specs/hooks.js for examples of supported and prohibited
// behaviors. // behaviors.

View file

@ -60,6 +60,7 @@ const migratePluginsFromNodeModules = async () => {
const cmd = ['pnpm', 'ls', '--long', '--json', '--depth=0', '--no-production']; const cmd = ['pnpm', 'ls', '--long', '--json', '--depth=0', '--no-production'];
const [{dependencies = {}}] = JSON.parse(await runCmd(cmd, const [{dependencies = {}}] = JSON.parse(await runCmd(cmd,
{stdio: [null, 'string']})); {stdio: [null, 'string']}));
await Promise.all(Object.entries(dependencies) await Promise.all(Object.entries(dependencies)
.filter(([pkg, info]) => pkg.startsWith(plugins.prefix) && pkg !== 'ep_etherpad-lite') .filter(([pkg, info]) => pkg.startsWith(plugins.prefix) && pkg !== 'ep_etherpad-lite')
.map(async ([pkg, info]) => { .map(async ([pkg, info]) => {

View file

@ -10,7 +10,7 @@ test('Shows troubleshooting page manager', async ({page}) => {
await page.goto('http://localhost:9001/admin/help') await page.goto('http://localhost:9001/admin/help')
await page.waitForSelector('.menu') await page.waitForSelector('.menu')
const menu = page.locator('.menu'); const menu = page.locator('.menu');
await expect(menu.locator('li')).toHaveCount(4); await expect(menu.locator('li')).toHaveCount(5);
}) })
test('Shows a version number', async function ({page}) { test('Shows a version number', async function ({page}) {

View file

@ -9,7 +9,7 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^5.4.4", "typescript": "^5.4.5",
"vite": "^5.2.8" "vite": "^5.2.9"
} }
} }