domma-cms 0.105.1 → 0.106.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/admin/dist/domma/domma-tools.css +3 -3
- package/admin/dist/domma/domma-tools.min.js +3 -3
- package/admin/js/lib/related.js +1 -1
- package/admin/js/templates/docs/usage-tools.html +4 -0
- package/package.json +2 -2
- package/plugins/_lib/admin/ui/transcript.js +154 -0
- package/plugins/_lib/call/chatStore.js +103 -0
- package/plugins/_lib/call/rooms.js +104 -21
- package/plugins/_lib/call/routes.js +55 -5
- package/plugins/_lib/public/call/call.css +1 -1
- package/plugins/_lib/public/call/call.js +229 -62
- package/plugins/booking/CLAUDE.md +9 -0
- package/plugins/booking/admin/lib/kit.js +1 -0
- package/plugins/booking/admin/views/panels.js +10 -0
- package/plugins/booking/docs/guide.md +1 -1
- package/plugins/booking/plugin.js +41 -3
- package/plugins/booking/plugin.json +1 -1
- package/plugins/booking/tests/api.test.js +64 -1
- package/plugins/free-tier.lock.json +9 -9
- package/public/js/celebrations/index.js +1 -1
- package/public/js/celebrations/themes/christmas-festive.js +1 -1
- package/public/js/celebrations/themes/christmas.js +1 -1
- package/public/js/effects.js +1 -1
- package/server/routes/api/calls.js +26 -6
- package/server/services/instantCalls.js +54 -4
- package/server/templates/page.html +1 -1
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* GET /api/calls/contacts?q= contacts the caller may READ (own + shared), to pick (calls.create)
|
|
11
11
|
* GET /api/calls/contact/:id one contact the caller may read, to fill in (calls.create)
|
|
12
12
|
* POST /api/calls {name, email?, contactId?, notify} -> 201 {id, hostUrl, joinUrl, emailed, emailLimited?, expiresAt}
|
|
13
|
-
* GET /api/calls?contact=<id> the caller's recent calls with that contact
|
|
13
|
+
* GET /api/calls?contact=<id> the caller's recent calls with that contact, with `messages` (calls.read)
|
|
14
|
+
* GET /api/calls/:id/chat the call's saved chat (read-only transcript) - its HOST or an admin (calls.read)
|
|
14
15
|
*
|
|
15
16
|
* Contacts are read through `readableContactIds` (services/hooks.js): the
|
|
16
17
|
* caller's own, plus whatever a sharing plugin (Contacts Pro) lets them read -
|
|
@@ -29,13 +30,14 @@ import {authenticate as defaultAuthenticate, requirePermission as defaultRequire
|
|
|
29
30
|
import {getEntry, listEntries} from '../../services/collections.js';
|
|
30
31
|
import {readableContactIds} from '../../services/hooks.js';
|
|
31
32
|
import {getPermissionsFor} from '../../services/roles.js';
|
|
32
|
-
import {getEffectiveRoles} from '../../services/userRoles.js';
|
|
33
|
+
import {getEffectiveLevel, getEffectiveRoles} from '../../services/userRoles.js';
|
|
33
34
|
import {isToolEnabled} from '../../services/tools.js';
|
|
34
35
|
import {mountCallRoutes} from '../../../plugins/_lib/call/routes.js';
|
|
36
|
+
import {whoOf} from '../../../plugins/_lib/call/rooms.js';
|
|
35
37
|
import {getBaseUrl} from '../public.js';
|
|
36
38
|
import {
|
|
37
|
-
EMAIL_RE, callMailReady, callPaths, callsOfContact, createCall, getCall, instantCallsEnabled, isOpen,
|
|
38
|
-
mayEmailAddress, mayStartCall, sendCallEmail, whoIsCalling
|
|
39
|
+
EMAIL_RE, callChat, callMailReady, callPaths, callsOfContact, chatCountOf, createCall, getCall, instantCallsEnabled, isOpen,
|
|
40
|
+
mayEmailAddress, mayStartCall, saveCallChat, sendCallEmail, whoIsCalling
|
|
39
41
|
} from '../../services/instantCalls.js';
|
|
40
42
|
|
|
41
43
|
const CONTACTS_SLUG = 'contacts-contacts';
|
|
@@ -122,13 +124,31 @@ export async function callsRoutes(fastify, opts = {}) {
|
|
|
122
124
|
if (!contact) return fail(reply, 400, 'contact is required');
|
|
123
125
|
const now = Date.now();
|
|
124
126
|
const list = (await callsOfContact(contact, uidOf(request))).slice(0, 20)
|
|
125
|
-
.map(c => ({id: c.id, name: c.name, createdAt: c.createdAt, expiresAt: c.expiresAt, open: isOpen(c, now)}));
|
|
127
|
+
.map(c => ({id: c.id, name: c.name, createdAt: c.createdAt, expiresAt: c.expiresAt, open: isOpen(c, now), messages: chatCountOf(c)}));
|
|
126
128
|
return reply.send(list);
|
|
127
129
|
});
|
|
128
130
|
|
|
131
|
+
// The call's chat, saved against it: for the host who made the call, or an admin. Anyone else: 404 (not 403 -
|
|
132
|
+
// whether a call exists is the host's business). `mine` marks the host's own lines for the bubbles.
|
|
133
|
+
fastify.get('/:id/chat', can('read'), async (request, reply) => {
|
|
134
|
+
const rec = await getCall(String(request.params.id));
|
|
135
|
+
const uid = uidOf(request);
|
|
136
|
+
let admin = false;
|
|
137
|
+
try { admin = getEffectiveLevel(request.user || {}) <= 1; } catch { admin = false; }
|
|
138
|
+
if (!rec || !uid || (rec.hostId !== uid && !admin)) return fail(reply, 404, 'That call could not be found.');
|
|
139
|
+
const hostWho = whoOf(`${rec.id}:host`, '');
|
|
140
|
+
const mineToo = rec.hostId === uid;
|
|
141
|
+
const items = (await callChat(rec.id)).map(({from, ...i}) => ({...i, mine: mineToo && i.kind === 'chat' && i.who === hostWho}));
|
|
142
|
+
reply.header('Cache-Control', 'no-store');
|
|
143
|
+
return reply.send({id: rec.id, name: rec.name, hostName: rec.hostName, createdAt: rec.createdAt, expiresAt: rec.expiresAt,
|
|
144
|
+
messages: chatCountOf(rec), items});
|
|
145
|
+
});
|
|
146
|
+
|
|
129
147
|
// The call engine: a room per call, guests knock, the host lets them in, open createdAt..expiresAt.
|
|
130
148
|
mountCallRoutes(fastify, {
|
|
131
149
|
prefix: '/room', ns: 'instant', ...(opts.rooms ? {rooms: opts.rooms} : {}),
|
|
132
|
-
resolve: async (request, room, body) => whoIsCalling(await getCall(String(room)), {t: body?.t, h: body?.h})
|
|
150
|
+
resolve: async (request, room, body) => whoIsCalling(await getCall(String(room)), {t: body?.t, h: body?.h}),
|
|
151
|
+
// The chat is saved against the call record, so a call reopened (or after a restart) shows it again.
|
|
152
|
+
chat: {load: (room) => callChat(String(room)), save: (room, item) => saveCallChat(String(room), item)}
|
|
133
153
|
});
|
|
134
154
|
}
|
|
@@ -19,7 +19,10 @@
|
|
|
19
19
|
* actions and list stand aside (checked per request), so nobody sees two.
|
|
20
20
|
*
|
|
21
21
|
* A record: {id, name, email, contactId, hostId, hostName, token, hostKey,
|
|
22
|
-
* createdAt, expiresAt}.
|
|
22
|
+
* createdAt, expiresAt, chat?}. `chat` is the call's chat, saved against the
|
|
23
|
+
* call (the engine's items - typed messages plus joined/left lines - the last
|
|
24
|
+
* CHAT_MAX); the engine seeds a reopened room from it, and the host (or an
|
|
25
|
+
* admin) reads it afterwards through GET /api/calls/:id/chat. Kept in memory and in one JSON file with the sign-in
|
|
23
26
|
* sessions (content/sessions/ - runtime state, never committed; see
|
|
24
27
|
* siteGitignore.js), so a restart keeps the links working. The call itself (who
|
|
25
28
|
* is in it) is the call engine's, in memory. Records are forgotten KEEP_DAYS
|
|
@@ -44,6 +47,8 @@ import {createTransport, escapeHtml, isSmtpConfigured, sendEmail} from './email.
|
|
|
44
47
|
export const INSTANT_HOURS = 3;
|
|
45
48
|
export const KEEP_DAYS = 30;
|
|
46
49
|
export const MAX_KEPT = 2000;
|
|
50
|
+
/** Chat items kept on one call record (the engine's HISTORY_MAX). */
|
|
51
|
+
export const CHAT_MAX = 500;
|
|
47
52
|
export const CALL_PATH = '/_call';
|
|
48
53
|
export const ROOM_API = '/api/calls/room';
|
|
49
54
|
export const ID_RE = /^[\w-]{16,40}$/;
|
|
@@ -141,9 +146,14 @@ function registerActions() {
|
|
|
141
146
|
async list({id, user}) {
|
|
142
147
|
if (!user?.id || !mayCall(user, 'read')) return [];
|
|
143
148
|
const now = Date.now();
|
|
144
|
-
return (await callsOfContact(id, String(user.id))).slice(0, 5).map(c =>
|
|
145
|
-
|
|
146
|
-
|
|
149
|
+
return (await callsOfContact(id, String(user.id))).slice(0, 5).map((c) => {
|
|
150
|
+
const n = chatCountOf(c);
|
|
151
|
+
return {
|
|
152
|
+
id: c.id, title: 'Video call', startAt: c.createdAt, past: !isOpen(c, now), meta: isOpen(c, now) ? 'open now' : '',
|
|
153
|
+
// The admin's Calls list offers "N messages" - a read-only transcript (GET /api/calls/:id/chat).
|
|
154
|
+
...(n ? {transcript: {url: `/calls/${encodeURIComponent(c.id)}/chat`, count: n, title: `Video call with ${c.name || 'a guest'}`}} : {})
|
|
155
|
+
};
|
|
156
|
+
});
|
|
147
157
|
}
|
|
148
158
|
});
|
|
149
159
|
// An email in the mail reader: "Call Sam now…" with the sender filled in.
|
|
@@ -234,6 +244,46 @@ export async function getCall(id) {
|
|
|
234
244
|
return (await load()).get(id) || null;
|
|
235
245
|
}
|
|
236
246
|
|
|
247
|
+
/** The chat item as it is kept on a record; null for junk. System lines other than joined/left are not kept. */
|
|
248
|
+
function chatItem(raw) {
|
|
249
|
+
if (!raw || typeof raw !== 'object' || typeof raw.text !== 'string' || !raw.id) return null;
|
|
250
|
+
const kind = raw.kind === 'system' ? 'system' : 'chat';
|
|
251
|
+
if (kind === 'system' && raw.event !== 'joined' && raw.event !== 'left') return null;
|
|
252
|
+
const item = {id: String(raw.id).slice(0, 64), seq: Number(raw.seq) || 0, at: String(raw.at || ''), kind,
|
|
253
|
+
from: String(raw.from || '').slice(0, 64), who: String(raw.who || '').slice(0, 64),
|
|
254
|
+
name: String(raw.name || '').slice(0, 60), text: raw.text.slice(0, 1000)};
|
|
255
|
+
if (kind === 'system') item.event = raw.event;
|
|
256
|
+
return item;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Save one chat item against its call (the engine's `chat.save`). Queued with
|
|
261
|
+
* every other write to the file; the last CHAT_MAX are kept. A call that is no
|
|
262
|
+
* longer on record (forgotten, or never) keeps nothing.
|
|
263
|
+
*/
|
|
264
|
+
export async function saveCallChat(id, raw) {
|
|
265
|
+
const item = chatItem(raw);
|
|
266
|
+
if (!item || typeof id !== 'string' || !ID_RE.test(id)) return;
|
|
267
|
+
const all = await load();
|
|
268
|
+
const rec = all.get(id);
|
|
269
|
+
if (!rec) return;
|
|
270
|
+
const chat = Array.isArray(rec.chat) ? rec.chat : [];
|
|
271
|
+
if (chat.some(c => c?.id === item.id)) return;
|
|
272
|
+
chat.push(item);
|
|
273
|
+
if (chat.length > CHAT_MAX) chat.splice(0, chat.length - CHAT_MAX);
|
|
274
|
+
rec.chat = chat;
|
|
275
|
+
await save(all);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** A call's saved chat, oldest first ([] for none). */
|
|
279
|
+
export async function callChat(id) {
|
|
280
|
+
const rec = await getCall(id);
|
|
281
|
+
return Array.isArray(rec?.chat) ? rec.chat.map(c => ({...c})) : [];
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** How many typed messages a call's chat holds (not the joined/left lines). */
|
|
285
|
+
export const chatCountOf = (rec) => (Array.isArray(rec?.chat) ? rec.chat.filter(c => c?.kind === 'chat').length : 0);
|
|
286
|
+
|
|
237
287
|
/** One host's calls with one contact, newest first. */
|
|
238
288
|
export async function callsOfContact(contactId, hostId) {
|
|
239
289
|
if (!contactId || !hostId) return [];
|
|
@@ -154,7 +154,7 @@
|
|
|
154
154
|
<script src="/public/js/forms.js?v=20260927-submit-btn" type="module"></script>
|
|
155
155
|
|
|
156
156
|
<!-- Core effects runtime -->
|
|
157
|
-
<script src="/public/js/effects.js?v=20260930-
|
|
157
|
+
<script src="/public/js/effects.js?v=20260930-snowman"></script>
|
|
158
158
|
|
|
159
159
|
<!-- Plugin body-end injection -->
|
|
160
160
|
{{bodyEndInject}}
|