domma-cms 1.8.32 → 1.8.34
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/CHANGELOG.md +9 -0
- package/docs/DommaFeatures.md +13 -1
- package/package.json +1 -1
- package/plugins/_lib/admin/games/games.css +1 -1
- package/plugins/_lib/admin/games/kit.js +329 -124
- package/plugins/_lib/games/http.js +11 -1
- package/plugins/_lib/games/plays.js +28 -7
- package/plugins/crossword/CLAUDE.md +37 -0
- package/plugins/crossword/admin/css/index.css +18 -1
- package/plugins/crossword/admin/views/crossword.js +337 -205
- package/plugins/crossword/docs/guide.md +53 -0
- package/plugins/crossword/inject/body.html +1 -1
- package/plugins/crossword/plugin.js +38 -5
- package/plugins/crossword/plugin.json +4 -3
- package/plugins/crossword/public/crossword.css +36 -1
- package/plugins/crossword/public/crossword.js +468 -6
- package/plugins/docs/CLAUDE.md +318 -0
- package/plugins/docs/admin/css/docs.css +783 -0
- package/plugins/docs/admin/lib/api.js +340 -0
- package/plugins/docs/admin/lib/convert.js +482 -0
- package/plugins/docs/admin/lib/drafts.js +115 -0
- package/plugins/docs/admin/lib/editor.js +851 -0
- package/plugins/docs/admin/lib/find-replace.js +426 -0
- package/plugins/docs/admin/lib/folders.js +203 -0
- package/plugins/docs/admin/lib/panel-guard.js +73 -0
- package/plugins/docs/admin/lib/pdf-view.js +126 -0
- package/plugins/docs/admin/lib/send.js +156 -0
- package/plugins/docs/admin/lib/settings.js +142 -0
- package/plugins/docs/admin/lib/share.js +323 -0
- package/plugins/docs/admin/lib/shortcuts.js +44 -0
- package/plugins/docs/admin/lib/templates.js +194 -0
- package/plugins/docs/admin/lib/versions.js +192 -0
- package/plugins/docs/admin/templates/docs.html +92 -0
- package/plugins/docs/admin/templates/editor.html +86 -0
- package/plugins/docs/admin/views/docs.js +777 -0
- package/plugins/docs/admin/views/editor.js +978 -0
- package/plugins/docs/config.js +24 -0
- package/plugins/docs/docs/guide.md +270 -0
- package/plugins/docs/lib/extensions.js +213 -0
- package/plugins/docs/lib/importer.js +72 -0
- package/plugins/docs/lib/pdf-worker.js +48 -0
- package/plugins/docs/lib/pdf.js +222 -0
- package/plugins/docs/lib/store.js +748 -0
- package/plugins/docs/plugin.js +1108 -0
- package/plugins/docs/plugin.json +89 -0
- package/plugins/docs/plugin.public.js +114 -0
- package/plugins/docs/templates/public-doc.html +36 -0
- package/plugins/docs/tests/_env.js +10 -0
- package/plugins/docs/tests/_pdf.js +73 -0
- package/plugins/docs/tests/convert.test.js +95 -0
- package/plugins/docs/tests/drafts.test.js +82 -0
- package/plugins/docs/tests/pdf.test.js +86 -0
- package/plugins/docs/tests/plugin.test.js +816 -0
- package/plugins/docs/tests/shortcuts.test.js +30 -0
- package/plugins/free-tier.lock.json +147 -1
- package/plugins/garage/CLAUDE.md +51 -0
- package/plugins/garage/admin/css/garage.css +73 -0
- package/plugins/garage/admin/lib/garage.js +236 -0
- package/plugins/garage/admin/templates/garage.html +86 -0
- package/plugins/garage/admin/views/garage.js +581 -0
- package/plugins/garage/collections/garage-vehicles/schema.json +101 -0
- package/plugins/garage/config.js +18 -0
- package/plugins/garage/docs/guide.md +118 -0
- package/plugins/garage/plugin.js +471 -0
- package/plugins/garage/plugin.json +68 -0
- package/plugins/garage/tests/access.test.js +134 -0
- package/plugins/garage/tests/garage.test.js +106 -0
- package/plugins/messages/CLAUDE.md +230 -0
- package/plugins/messages/admin/css/messages.css +117 -0
- package/plugins/messages/admin/lib/messages.js +452 -0
- package/plugins/messages/admin/templates/messages.html +76 -0
- package/plugins/messages/admin/views/messages.js +1219 -0
- package/plugins/messages/collections/user-message-threads/schema.json +22 -0
- package/plugins/messages/collections/user-messages/schema.json +23 -0
- package/plugins/messages/config.js +14 -0
- package/plugins/messages/docs/guide.md +176 -0
- package/plugins/messages/lib/call.js +138 -0
- package/plugins/messages/plugin.js +994 -0
- package/plugins/messages/plugin.json +59 -0
- package/plugins/messages/plugin.public.js +52 -0
- package/plugins/messages/templates/call.html +6 -0
- package/plugins/messages/tests/after-send.test.js +22 -0
- package/plugins/messages/tests/api.test.js +187 -0
- package/plugins/messages/tests/call.test.js +497 -0
- package/plugins/messages/tests/drafts.test.js +25 -0
- package/plugins/messages/tests/messages.test.js +167 -0
- package/plugins/quiz/CLAUDE.md +43 -0
- package/plugins/quiz/admin/css/index.css +13 -1
- package/plugins/quiz/admin/views/quiz.js +423 -245
- package/plugins/quiz/docs/guide.md +61 -0
- package/plugins/quiz/inject/body.html +1 -1
- package/plugins/quiz/plugin.js +39 -5
- package/plugins/quiz/plugin.json +4 -3
- package/plugins/quiz/public/quiz.css +36 -1
- package/plugins/quiz/public/quiz.js +298 -6
- package/plugins/theme-roller/CLAUDE.md +65 -0
- package/plugins/theme-roller/admin/css/theme-roller.css +43 -0
- package/plugins/theme-roller/admin/lib/prompt.js +22 -0
- package/plugins/theme-roller/admin/lib/themes.js +196 -0
- package/plugins/theme-roller/admin/lib/view.js +88 -0
- package/plugins/theme-roller/admin/templates/theme-roller.html +73 -0
- package/plugins/theme-roller/admin/views/theme-roller-view.js +639 -0
- package/plugins/theme-roller/config.js +1 -0
- package/plugins/theme-roller/docs/guide.md +104 -0
- package/plugins/theme-roller/plugin.js +263 -0
- package/plugins/theme-roller/plugin.json +42 -0
- package/plugins/theme-roller/tests/api.test.js +161 -0
- package/plugins/theme-roller/tests/prompt-values.test.js +22 -0
- package/plugins/theme-roller/tests/themes.test.js +77 -0
- package/plugins/theme-roller/tests/view.test.js +55 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"slug": "user-message-threads",
|
|
3
|
+
"title": "Message Threads",
|
|
4
|
+
"description": "Conversation threads managed by the Messages plugin.",
|
|
5
|
+
"plugin": "messages",
|
|
6
|
+
"fields": [
|
|
7
|
+
{ "name": "kind", "label": "Kind", "type": "select", "options": ["dm", "group-adhoc", "group-managed"], "required": true },
|
|
8
|
+
{ "name": "name", "label": "Name", "type": "text" },
|
|
9
|
+
{ "name": "participants", "label": "Participants", "type": "array", "items": "string", "default": [] },
|
|
10
|
+
{ "name": "createdBy", "label": "Created By", "type": "text", "required": true },
|
|
11
|
+
{ "name": "retentionDays", "label": "Retention Days", "type": "number" },
|
|
12
|
+
{ "name": "lastMessageAt", "label": "Last Message At", "type": "datetime" },
|
|
13
|
+
{ "name": "lastMessagePreview","label": "Last Preview", "type": "text" }
|
|
14
|
+
],
|
|
15
|
+
"api": {
|
|
16
|
+
"create": { "enabled": false, "access": "admin" },
|
|
17
|
+
"read": { "enabled": false, "access": "admin" },
|
|
18
|
+
"update": { "enabled": false, "access": "admin" },
|
|
19
|
+
"delete": { "enabled": false, "access": "admin" }
|
|
20
|
+
},
|
|
21
|
+
"storage": {}
|
|
22
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"slug": "user-messages",
|
|
3
|
+
"title": "Messages",
|
|
4
|
+
"description": "Individual messages managed by the Messages plugin.",
|
|
5
|
+
"plugin": "messages",
|
|
6
|
+
"fields": [
|
|
7
|
+
{ "name": "threadId", "label": "Thread ID", "type": "text", "required": true },
|
|
8
|
+
{ "name": "senderId", "label": "Sender ID", "type": "text", "required": true },
|
|
9
|
+
{ "name": "body", "label": "Body", "type": "textarea" },
|
|
10
|
+
{ "name": "attachments", "label": "Attachments", "type": "array", "items": "object", "default": [] },
|
|
11
|
+
{ "name": "createdAt", "label": "Created At", "type": "datetime", "required": true },
|
|
12
|
+
{ "name": "editedAt", "label": "Edited At", "type": "datetime" },
|
|
13
|
+
{ "name": "deletedAt", "label": "Deleted At", "type": "datetime" },
|
|
14
|
+
{ "name": "readBy", "label": "Read By", "type": "array", "items": "string", "default": [] }
|
|
15
|
+
],
|
|
16
|
+
"api": {
|
|
17
|
+
"create": { "enabled": false, "access": "admin" },
|
|
18
|
+
"read": { "enabled": false, "access": "admin" },
|
|
19
|
+
"update": { "enabled": false, "access": "admin" },
|
|
20
|
+
"delete": { "enabled": false, "access": "admin" }
|
|
21
|
+
},
|
|
22
|
+
"storage": {}
|
|
23
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Messages defaults. There is no `scope` or `storage` key (both were listed
|
|
3
|
+
* until 1.3.1 and nothing read either): a conversation's participants are its
|
|
4
|
+
* scope, and the two collections' own schemas decide their storage - the file
|
|
5
|
+
* adapter unless a collection is moved to MongoDB (Collections > Storage).
|
|
6
|
+
*/
|
|
7
|
+
export default {
|
|
8
|
+
/** Hard upper limit on per-thread retention; users cannot exceed this. */
|
|
9
|
+
globalRetentionCeilingDays: 365,
|
|
10
|
+
/** Default retention when a thread has no per-thread override. */
|
|
11
|
+
defaultRetentionDays: 90,
|
|
12
|
+
/** Minutes after send during which the sender may edit a message. */
|
|
13
|
+
editWindowMinutes: 15
|
|
14
|
+
};
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Guide
|
|
3
|
+
order: 1
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Messages is private chat for the people who sign in to your admin. Send direct messages, run group conversations and share files. New messages arrive straight away, without reloading the page.
|
|
7
|
+
|
|
8
|
+
## What it does
|
|
9
|
+
|
|
10
|
+
- Direct messages between two people. There is only ever one per pair: starting it again opens the one you have.
|
|
11
|
+
- Groups: a name and any number of people.
|
|
12
|
+
- Managed groups: standing teams, such as Editors, that only an admin can create.
|
|
13
|
+
- File attachments, up to 20 per message.
|
|
14
|
+
- Video calls from any conversation, for up to 6 people.
|
|
15
|
+
- Inbox, Unread and Archived lists, and a search.
|
|
16
|
+
- Drafts kept per conversation, including attached files you have not sent yet.
|
|
17
|
+
- An unread badge on the Messages sidebar entry.
|
|
18
|
+
- A notification if a message sits unread.
|
|
19
|
+
|
|
20
|
+
## Getting started
|
|
21
|
+
|
|
22
|
+
1. Open [Messages](#/plugins/messages) in the sidebar.
|
|
23
|
+
2. Click **New conversation** in the banner.
|
|
24
|
+
3. Choose the **Kind**: **Direct message** or **Group**. For a group, enter a **Group name**.
|
|
25
|
+
4. Under **People**, use **Find someone...** to pick who to talk to.
|
|
26
|
+
5. Start the conversation (Ctrl+Enter also starts it).
|
|
27
|
+
6. Type in the message box and click **Send**, or press Ctrl+Enter.
|
|
28
|
+
|
|
29
|
+
## Screens
|
|
30
|
+
|
|
31
|
+
### Conversation list
|
|
32
|
+
|
|
33
|
+
On the left of the screen.
|
|
34
|
+
|
|
35
|
+
- Filters: **Inbox**, **Unread** and **Archived**, each with a count.
|
|
36
|
+
- A search box to find a conversation.
|
|
37
|
+
- Each conversation shows its unread count.
|
|
38
|
+
|
|
39
|
+
Right-click a conversation, or use its menu button, for:
|
|
40
|
+
|
|
41
|
+
| Action | Who sees it |
|
|
42
|
+
|---|---|
|
|
43
|
+
| Open | Everyone |
|
|
44
|
+
| Mark as read / Mark as unread | Everyone |
|
|
45
|
+
| Archive / Move to inbox | Everyone. Archiving only affects your own list |
|
|
46
|
+
| Rename... | Groups only: whoever started it, and admins |
|
|
47
|
+
| Keep messages for... | Whoever started the conversation, and admins |
|
|
48
|
+
| Leave conversation... | Everyone |
|
|
49
|
+
|
|
50
|
+
### Conversation
|
|
51
|
+
|
|
52
|
+
The messages, with the message box underneath.
|
|
53
|
+
|
|
54
|
+
- **Attach files** uploads files to your site's Media library and adds them to the message. This needs the Media > Upload permission (see Permissions and roles).
|
|
55
|
+
- **Send**, or Ctrl+Enter, sends it.
|
|
56
|
+
- What you type is kept as a draft for each conversation, so you can switch away without losing it.
|
|
57
|
+
|
|
58
|
+
Right-click a message, or use its menu button, for:
|
|
59
|
+
|
|
60
|
+
- **Copy text**.
|
|
61
|
+
- **Edit...**: your own messages only, for 15 minutes after sending. The menu shows how many minutes are left.
|
|
62
|
+
- **Delete...**: the message is replaced with "message deleted".
|
|
63
|
+
|
|
64
|
+
### Calls
|
|
65
|
+
|
|
66
|
+
Click **Call** at the top of a conversation to start a video call with everyone in it.
|
|
67
|
+
|
|
68
|
+
1. The call opens in a new tab. Check your camera and microphone, then click **Join the call**.
|
|
69
|
+
2. Everyone else in the conversation who has Messages open sees a notice at the bottom right: "Sam is calling", with **Join**. They also get a notification.
|
|
70
|
+
3. They click **Join** and go straight in. There is no waiting room: they are your signed-in team.
|
|
71
|
+
4. The conversation shows "Sam started a call". While the call is on, the button reads **Join call** and the line has a **Join** button.
|
|
72
|
+
5. When the last person leaves, the conversation shows "Call ended" with how long it lasted.
|
|
73
|
+
|
|
74
|
+
What you type in the call's chat goes into the conversation as your own message, marked "in the call", so it is all there afterwards.
|
|
75
|
+
|
|
76
|
+
To call people without opening a conversation first, click **Call…** in the banner (or on the empty screen when no conversation is open):
|
|
77
|
+
|
|
78
|
+
1. Tick the people to call - 1 to 5, since a call holds 6 people including you.
|
|
79
|
+
2. Click **Call** (or press Ctrl+Enter).
|
|
80
|
+
3. One person: the call is in your conversation with them. Several people: it is in your group with exactly those people, or a new group if you have none. A new group can have a name; left empty, it is named after everyone in it, such as "Alex, Priya and Sam". A group you archived is not used.
|
|
81
|
+
4. The call opens in a new tab and the conversation opens here, showing "started a call".
|
|
82
|
+
|
|
83
|
+
You can also right-click a conversation and choose **Call**.
|
|
84
|
+
|
|
85
|
+
**Ring someone else into the call.** In the call, click **Add people** and pick a team member who is not in the conversation. They get a notification ("Sam is calling you - Join") and a message from you in Messages, each with a link of their own (tick **Email** to email it too). The link lets them straight into this call while it runs - it does not add them to the conversation, they do not see what was said in the call before they came, and it stops working when the call ends. Anyone in the conversation can ring people in; people rung in cannot ring others.
|
|
86
|
+
|
|
87
|
+
Other Tools' calls (Booking's online appointments, the Calendar's online meetings and "Call Sam now…") use Messages too: when their host adds a team member, that person gets the link as a message from the host.
|
|
88
|
+
|
|
89
|
+
Good to know:
|
|
90
|
+
|
|
91
|
+
- A call holds 6 people. In a bigger group the first 6 to join get in; the next person is told the call is full.
|
|
92
|
+
- Your browser asks to use your camera and microphone the first time.
|
|
93
|
+
- If your browser blocks the new tab, an **Open the call** button appears instead.
|
|
94
|
+
- Your link to a call is yours alone and works for 2 hours. It stops working if you leave the conversation.
|
|
95
|
+
- A call nobody joins ends after 5 minutes.
|
|
96
|
+
- Calls are not recorded, and restarting the site ends any call in progress.
|
|
97
|
+
- Calls need Domma CMS 0.103 or later.
|
|
98
|
+
|
|
99
|
+
### New conversation
|
|
100
|
+
|
|
101
|
+
Opened from the banner. Fields: Kind, Group name (groups only) and People.
|
|
102
|
+
|
|
103
|
+
### Managed groups
|
|
104
|
+
|
|
105
|
+
Admins only. Click **Managed groups** in the banner to see the existing managed groups and create a new one with a name and its members.
|
|
106
|
+
|
|
107
|
+
## Who can message whom
|
|
108
|
+
|
|
109
|
+
This is checked by the server, not just the screen.
|
|
110
|
+
|
|
111
|
+
- Admins can message every active account.
|
|
112
|
+
- Everyone else can message admins, people who share one of their roles, and people they already have a conversation with.
|
|
113
|
+
|
|
114
|
+
## Deleting and leaving
|
|
115
|
+
|
|
116
|
+
- You can delete your own messages at any time.
|
|
117
|
+
- Whoever started a group can delete other people's messages in that group. This never applies to direct messages.
|
|
118
|
+
- Admins can delete any message.
|
|
119
|
+
- Leaving a conversation stops you receiving its messages. If you are the last person in it, the conversation and its messages are deleted.
|
|
120
|
+
- A new message in an archived conversation moves it back to everyone's inbox.
|
|
121
|
+
|
|
122
|
+
## Settings
|
|
123
|
+
|
|
124
|
+
There is no settings screen. The defaults are:
|
|
125
|
+
|
|
126
|
+
| Setting | Default | Meaning |
|
|
127
|
+
|---|---|---|
|
|
128
|
+
| Default retention | 90 days | How long messages are kept when a conversation has no retention of its own |
|
|
129
|
+
| Retention ceiling | 365 days | The longest anyone can set for a conversation |
|
|
130
|
+
| Edit window | 15 minutes | How long after sending a message can be edited |
|
|
131
|
+
|
|
132
|
+
A message holds up to 10,000 characters and a conversation's name up to 80; anything longer is cut.
|
|
133
|
+
|
|
134
|
+
These can be changed by whoever manages your site's configuration (`config/plugins.json`). A restart is needed afterwards.
|
|
135
|
+
|
|
136
|
+
### Keeping messages
|
|
137
|
+
|
|
138
|
+
**Keep messages for...** sets how many days a conversation's messages are kept, for everyone in it. Enter a whole number of 1 or more, or leave it empty for the site default. It cannot go past the ceiling. Older messages are removed the next time the conversation is opened.
|
|
139
|
+
|
|
140
|
+
## Permissions and roles
|
|
141
|
+
|
|
142
|
+
In System > Roles, Messages adds the permission **Messages** with one action:
|
|
143
|
+
|
|
144
|
+
| Action | Label | Meaning | Granted by default to |
|
|
145
|
+
|---|---|---|---|
|
|
146
|
+
| read | Use | Send, read and manage your own conversations | Admin, User |
|
|
147
|
+
|
|
148
|
+
Super Admins always have it. Remove it from a role and that role has no Messages entry in the sidebar and no access to messages.
|
|
149
|
+
|
|
150
|
+
Attaching files uploads them to Media, so it also needs the **Media** permission's **Upload** action. Admins have it; the default **User** role does not, so users can only send text until you give their role Upload.
|
|
151
|
+
|
|
152
|
+
Admin rights inside Messages (managed groups, deleting anyone's messages, reaching every account) go to Admins and Super Admins, including anyone given the Admin role as an additional role.
|
|
153
|
+
|
|
154
|
+
## Notifications
|
|
155
|
+
|
|
156
|
+
Messages adds two sources under Notifications > Sources:
|
|
157
|
+
|
|
158
|
+
- **Unread messages** checks every 15 minutes. A message left unread for 10 minutes or more (and less than 2 days old) sends its recipient one notice per conversation. Reading or archiving the conversation clears it.
|
|
159
|
+
- **Calls**: someone starts a call. Everyone else in the conversation gets a notice. It clears when the call ends.
|
|
160
|
+
|
|
161
|
+
## Tips
|
|
162
|
+
|
|
163
|
+
- Use Archive to tidy your inbox. It only affects your own list.
|
|
164
|
+
- Use Mark as unread to come back to a conversation later.
|
|
165
|
+
- An attachment is a file in your Media library. If that file is deleted from Media, the message still lists it but the download no longer works.
|
|
166
|
+
- Drafts are kept in your browser, so a draft started on one computer does not appear on another.
|
|
167
|
+
|
|
168
|
+
## Limitations
|
|
169
|
+
|
|
170
|
+
- Messages are for signed-in admin users only. The only public page is the call page, and it opens only with a call link made for you.
|
|
171
|
+
- There is no settings screen for retention or the edit window.
|
|
172
|
+
- Only admins can create managed groups.
|
|
173
|
+
- Live delivery and calls assume the site runs as a single process, which is the normal set-up.
|
|
174
|
+
- Calls hold 6 people and cannot be recorded.
|
|
175
|
+
|
|
176
|
+
The Messages sidebar count shows how many unread messages are waiting in your unarchived conversations. It remains visible as **0** when everything is read (Messages 1.7.1, CMS 0.109.5 or later).
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calls in a conversation (1.4): who may join which call, and the join links.
|
|
3
|
+
*
|
|
4
|
+
* The call itself is the engine's (`_lib/call`, core 0.103+). A room is a
|
|
5
|
+
* CONVERSATION (room = thread id); everyone in it is a signed-in member of the
|
|
6
|
+
* site, so there is no waiting room - everyone goes straight in, up to the
|
|
7
|
+
* engine's six.
|
|
8
|
+
*
|
|
9
|
+
* The call page is a public page (`/messages-call/<thread>`), so it cannot see
|
|
10
|
+
* the admin sign-in. Instead POST /threads/:id/call hands the member a
|
|
11
|
+
* SHORT-LIVED link of their own: `?u=<user>&x=<expiry ms>&s=<HMAC>`, the HMAC
|
|
12
|
+
* over `thread|user|expiry` with a random secret the plugin makes once and
|
|
13
|
+
* keeps in its data folder. The join checks it again, and that the person is
|
|
14
|
+
* STILL in the conversation.
|
|
15
|
+
*
|
|
16
|
+
* "Add people" (1.7): someone in the call rings another team member who is NOT
|
|
17
|
+
* in the conversation. They get a link of their own, signed as an invitation
|
|
18
|
+
* (`&i=1`, the HMAC over `thread|user|expiry|invite`), which lets them into
|
|
19
|
+
* THIS call while it runs (plugin.js keeps who was invited with the live call) -
|
|
20
|
+
* they are not added to the conversation, and they cannot add others.
|
|
21
|
+
*
|
|
22
|
+
* @module messages/lib/call
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import crypto from 'node:crypto';
|
|
26
|
+
import fs from 'node:fs';
|
|
27
|
+
import path from 'node:path';
|
|
28
|
+
|
|
29
|
+
export const CALL_PATH = '/messages-call';
|
|
30
|
+
export const CALL_API = '/api/plugins/messages/call';
|
|
31
|
+
export const LINK_TTL_MS = 2 * 3600e3;
|
|
32
|
+
export const MAX_IN_CALL = 6;
|
|
33
|
+
/** A call nobody has joined yet is given up after this long. */
|
|
34
|
+
export const RING_MS = 5 * 60e3;
|
|
35
|
+
|
|
36
|
+
const ROOM_RE = /^[\w-]{1,120}$/;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The link secret, made once (32 random bytes) and kept as `call-secret` in
|
|
40
|
+
* `dir`. Losing it only stops links already handed out (they last two hours).
|
|
41
|
+
*/
|
|
42
|
+
export function loadSecret(dir) {
|
|
43
|
+
const file = path.join(dir, 'call-secret');
|
|
44
|
+
try {
|
|
45
|
+
const s = fs.readFileSync(file, 'utf8').trim();
|
|
46
|
+
if (/^[0-9a-f]{64}$/.test(s)) return s;
|
|
47
|
+
} catch { /* not made yet */ }
|
|
48
|
+
const s = crypto.randomBytes(32).toString('hex');
|
|
49
|
+
fs.mkdirSync(dir, {recursive: true});
|
|
50
|
+
// Another process may have made one meanwhile: keep theirs.
|
|
51
|
+
try { fs.writeFileSync(file, s + '\n', {mode: 0o600, flag: 'wx'}); return s; } catch { /* exists */ }
|
|
52
|
+
return fs.readFileSync(file, 'utf8').trim();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const signJoin = (secret, threadId, userId, expiry, invited = false) =>
|
|
56
|
+
crypto.createHmac('sha256', String(secret)).update(`${threadId}|${userId}|${expiry}${invited ? '|invite' : ''}`).digest('base64url');
|
|
57
|
+
|
|
58
|
+
/** A member's own link into a conversation's call. */
|
|
59
|
+
export function joinLink(secret, threadId, userId, now = Date.now()) {
|
|
60
|
+
const x = now + LINK_TTL_MS;
|
|
61
|
+
return `${CALL_PATH}/${encodeURIComponent(threadId)}?u=${encodeURIComponent(userId)}&x=${x}&s=${signJoin(secret, threadId, userId, x)}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** An invited team member's own link into a running call (1.7) - not a member of the conversation. */
|
|
65
|
+
export function inviteLink(secret, threadId, userId, now = Date.now()) {
|
|
66
|
+
const x = now + LINK_TTL_MS;
|
|
67
|
+
return `${CALL_PATH}/${encodeURIComponent(threadId)}?u=${encodeURIComponent(userId)}&x=${x}&i=1&s=${signJoin(secret, threadId, userId, x, true)}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The link's own words, trimmed to size (from a query string or a join body). */
|
|
71
|
+
export const credOf = (q = {}) => ({u: String(q?.u ?? '').slice(0, 64), x: String(q?.x ?? '').slice(0, 16), s: String(q?.s ?? '').slice(0, 64),
|
|
72
|
+
...(String(q?.i ?? '') === '1' ? {i: '1'} : {})});
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The user a link was made for, or null: forged, altered, for another
|
|
76
|
+
* conversation, expired, or dated further ahead than a link ever is. A member's
|
|
77
|
+
* link only - an invitation (`i=1`) is checkInvite's.
|
|
78
|
+
*/
|
|
79
|
+
export function checkJoin(secret, threadId, cred, now = Date.now(), invited = false) {
|
|
80
|
+
const {u, x, s, i} = credOf(cred);
|
|
81
|
+
if (Boolean(i) !== invited) return null;
|
|
82
|
+
if (!secret || !ROOM_RE.test(String(threadId || '')) || !u || !/^\d{1,15}$/.test(x) || !s) return null;
|
|
83
|
+
const exp = Number(x);
|
|
84
|
+
if (exp <= now || exp > now + LINK_TTL_MS + 60e3) return null;
|
|
85
|
+
const want = Buffer.from(signJoin(secret, threadId, u, x, invited));
|
|
86
|
+
const got = Buffer.from(s);
|
|
87
|
+
return want.length === got.length && crypto.timingSafeEqual(want, got) ? u : null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The user an INVITATION link (1.7) was made for, or null - the same checks as checkJoin. */
|
|
91
|
+
export const checkInvite = (secret, threadId, cred, now = Date.now()) => checkJoin(secret, threadId, cred, now, true);
|
|
92
|
+
|
|
93
|
+
/** What the call is called: a group's name, or the two people in a DM. */
|
|
94
|
+
export function callTitle(thread, names = {}) {
|
|
95
|
+
if (!thread) return 'Call';
|
|
96
|
+
if (thread.kind !== 'dm') return String(thread.name || 'Group call').slice(0, 80);
|
|
97
|
+
const who = (thread.participants || []).map(id => names[id]).filter(Boolean);
|
|
98
|
+
return who.length ? `Call: ${who.join(' and ')}`.slice(0, 80) : 'Call';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Who the caller is in a conversation's call, or null (the engine answers 404).
|
|
103
|
+
* Everyone in the conversation is let straight in (autoAdmit) - there is
|
|
104
|
+
* nobody to wait for, so nobody is the host (the engine's host only lets
|
|
105
|
+
* people in, records, and is captioned "(host)"). No recording.
|
|
106
|
+
*
|
|
107
|
+
* @param {object|null} thread - {id, kind, name, participants}
|
|
108
|
+
* @param {object|null} user - the account the link names ({id, name, disabled?})
|
|
109
|
+
* @param {string|null} userId - checkJoin()'s answer (or checkInvite()'s, with `invited`)
|
|
110
|
+
* @param {{invited?: boolean}} [opts] - an invited team member (1.7): not in the conversation, let in all the same;
|
|
111
|
+
* the caller has checked they were invited to the call that is running. Members may add people; invitees may not.
|
|
112
|
+
*/
|
|
113
|
+
export function whoIsCalling(thread, user, userId, names = {}, {invited = false} = {}) {
|
|
114
|
+
if (!thread || !userId || !user || user.id !== userId) return null;
|
|
115
|
+
if (user.disabled === true || user.active === false || user.isActive === false) return null;
|
|
116
|
+
const member = Array.isArray(thread.participants) && thread.participants.includes(userId);
|
|
117
|
+
if (!member && !invited) return null;
|
|
118
|
+
return {
|
|
119
|
+
mayInvite: member,
|
|
120
|
+
inviteExternal: false,
|
|
121
|
+
title: callTitle(thread, names),
|
|
122
|
+
host: false,
|
|
123
|
+
autoAdmit: true,
|
|
124
|
+
name: String(user.name || '').trim() || 'Member',
|
|
125
|
+
max: MAX_IN_CALL,
|
|
126
|
+
recordingAllowed: false,
|
|
127
|
+
identity: `${thread.id}:${userId}`
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** "Call ended (12 min)", "Call ended (under a minute)", or nobody came. */
|
|
132
|
+
export function endedText(startedAt, endedAt, anyoneJoined) {
|
|
133
|
+
if (!anyoneJoined) return 'Call ended - nobody joined';
|
|
134
|
+
const min = Math.round((endedAt - startedAt) / 60e3);
|
|
135
|
+
return min < 1 ? 'Call ended (under a minute)' : `Call ended (${min} min)`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export const escHtml = (s) => String(s ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|