@devchitchat/chat 0.0.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/README.md +313 -0
- package/index.js +148 -0
- package/migrate/001-drop-channel-invites.js +3 -0
- package/migrate/002-invite-initial-roles.js +5 -0
- package/migrate/003-dm-channels.js +35 -0
- package/migrate/004-notifications.js +15 -0
- package/migrate/005-uploads.js +21 -0
- package/migrate/006-mention-priority.js +3 -0
- package/migrate/007-push-subscriptions.js +14 -0
- package/migrate/008-messages-channel-seq-index.js +3 -0
- package/migrate/009-message-reactions.js +15 -0
- package/migrate/010-edit-messages.js +7 -0
- package/package.json +51 -0
- package/pages/_error.html +12 -0
- package/pages/_layout.html +31 -0
- package/pages/_layout.js +13 -0
- package/pages/admin/_layout.html +52 -0
- package/pages/admin/_layout.js +8 -0
- package/pages/admin/bots/[userId].js +88 -0
- package/pages/admin/bots/[userId].phtml +89 -0
- package/pages/admin/bots/index.js +41 -0
- package/pages/admin/bots/index.phtml +58 -0
- package/pages/admin/index.js +8 -0
- package/pages/admin/invites/index.js +72 -0
- package/pages/admin/invites/index.phtml +88 -0
- package/pages/admin/users/[userId].js +60 -0
- package/pages/admin/users/[userId].phtml +57 -0
- package/pages/admin/users/index.js +20 -0
- package/pages/admin/users/index.phtml +37 -0
- package/pages/api/uploads/index.js +66 -0
- package/pages/api/user/settings.js +26 -0
- package/pages/auth/signout.js +14 -0
- package/pages/channels/[channelId].js +99 -0
- package/pages/channels/[channelId].phtml +173 -0
- package/pages/index.js +33 -0
- package/pages/invite/[token].js +10 -0
- package/pages/login/index.js +57 -0
- package/pages/login/index.phtml +29 -0
- package/pages/public/client/action-sheet.js +77 -0
- package/pages/public/client/app.js +38 -0
- package/pages/public/client/auth-tabs.js +13 -0
- package/pages/public/client/emoji-data.js +197 -0
- package/pages/public/client/islands/call.js +1770 -0
- package/pages/public/client/islands/sidebar.js +1197 -0
- package/pages/public/client/long-press.js +59 -0
- package/pages/public/client/modal.js +50 -0
- package/pages/public/client/router.js +87 -0
- package/pages/public/client/rtc-peer-manager.js +344 -0
- package/pages/public/client/settings-sync.js +76 -0
- package/pages/public/client/shared/messages.js +147 -0
- package/pages/public/client/swipe-nav.js +98 -0
- package/pages/public/client/theme.js +27 -0
- package/pages/public/client/ws.js +71 -0
- package/pages/public/favicon.ico +0 -0
- package/pages/public/favicon.png +0 -0
- package/pages/public/icon.png +0 -0
- package/pages/public/manifest.json +11 -0
- package/pages/public/sw.js +38 -0
- package/pages/public/themes/base.css +1786 -0
- package/pages/public/themes/dark.css +22 -0
- package/pages/public/themes/forest.css +22 -0
- package/pages/public/themes/light.css +23 -0
- package/pages/public/themes/ocean.css +22 -0
- package/pages/public/themes/rose.css +22 -0
- package/pages/registration/index.js +35 -0
- package/pages/registration/index.phtml +38 -0
- package/pages/uploads/[uploadId]/[filename].js +45 -0
- package/src/adapters/InMemoryAuthRepository.js +74 -0
- package/src/adapters/InMemoryChannelRepository.js +138 -0
- package/src/adapters/InMemoryDeliveryRepository.js +52 -0
- package/src/adapters/InMemoryFileStore.js +53 -0
- package/src/adapters/InMemoryHubRepository.js +85 -0
- package/src/adapters/InMemoryMessageRepository.js +35 -0
- package/src/adapters/InMemoryReactionRepository.js +45 -0
- package/src/adapters/InMemorySearchRepository.js +37 -0
- package/src/adapters/InMemorySignalingRepository.js +35 -0
- package/src/adapters/InMemoryUploadRepository.js +36 -0
- package/src/adapters/InMemoryUserSettingsRepository.js +15 -0
- package/src/adapters/LocalFileStore.js +40 -0
- package/src/adapters/SqliteAuthRepository.js +184 -0
- package/src/adapters/SqliteChannelRepository.js +149 -0
- package/src/adapters/SqliteDeliveryRepository.js +53 -0
- package/src/adapters/SqliteHubRepository.js +99 -0
- package/src/adapters/SqliteMessageRepository.js +90 -0
- package/src/adapters/SqlitePushRepository.js +39 -0
- package/src/adapters/SqliteReactionRepository.js +50 -0
- package/src/adapters/SqliteSearchRepository.js +42 -0
- package/src/adapters/SqliteSignalingRepository.js +50 -0
- package/src/adapters/SqliteUploadRepository.js +34 -0
- package/src/adapters/SqliteUserSettingsRepository.js +23 -0
- package/src/adminAuth.js +25 -0
- package/src/config.js +11 -0
- package/src/context.js +77 -0
- package/src/core/dm.js +10 -0
- package/src/core/mentions.js +27 -0
- package/src/core/messages.js +21 -0
- package/src/core/reactions.js +6 -0
- package/src/core/roles.js +5 -0
- package/src/core/uploads.js +107 -0
- package/src/db/initDb.js +225 -0
- package/src/db/openDb.js +18 -0
- package/src/db/runMigrations.js +45 -0
- package/src/db/transaction.js +11 -0
- package/src/ports/IFileStore.js +34 -0
- package/src/services/AuthService.js +208 -0
- package/src/services/BotService.js +148 -0
- package/src/services/ChannelService.js +176 -0
- package/src/services/DeliveryService.js +28 -0
- package/src/services/HubService.js +133 -0
- package/src/services/MessageService.js +122 -0
- package/src/services/NotificationService.js +45 -0
- package/src/services/PresenceService.js +55 -0
- package/src/services/ReactionService.js +57 -0
- package/src/services/SearchService.js +21 -0
- package/src/services/SignalingService.js +177 -0
- package/src/services/UploadService.js +111 -0
- package/src/services/UserSettingsService.js +30 -0
- package/src/services/WebPushService.js +217 -0
- package/src/util/crypto.js +21 -0
- package/src/util/errors.js +14 -0
- package/src/util/ids.js +3 -0
- package/src/util/logger.js +21 -0
- package/src/ws/ChatServer.js +478 -0
- package/src/ws/handlers/authHandlers.js +152 -0
- package/src/ws/handlers/channelHandlers.js +166 -0
- package/src/ws/handlers/hubHandlers.js +82 -0
- package/src/ws/handlers/messageHandlers.js +88 -0
- package/src/ws/handlers/pushHandlers.js +25 -0
- package/src/ws/handlers/reactionHandlers.js +27 -0
- package/src/ws/handlers/rtcHandlers.js +126 -0
- package/styles.css +22 -0
package/README.md
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
# Dev Chit Chat
|
|
2
|
+
|
|
3
|
+
In 2009, at the Velocity conference, a couple of guys who worked at Flickr presented how development and operations fits toghether and gets along ... at Flickr.
|
|
4
|
+
|
|
5
|
+
The premise is kinda of dumb. Like, why was there even a Devs vs Ops mentality? Regardless, it was real. We were all working on systems and under pressure to build stuff that, quite frankly, was hard.
|
|
6
|
+
|
|
7
|
+
Anyways, that was the inspiration. Since then, I've championed just collaborating with each other as we build things together.
|
|
8
|
+
|
|
9
|
+
Along with this, Agile had already been getting traction in corporate America. Scrum was being used to run teams. And Daily Standups were becoing the norm.
|
|
10
|
+
|
|
11
|
+
In 2013 Github released Hubot as open source, it's home grown chat bot.
|
|
12
|
+
|
|
13
|
+
I was at GameStop at this time, managing my first team. I saw first hand what a DevOps culture felt like. We deployed the system every week. It was amazing.
|
|
14
|
+
|
|
15
|
+
Dev Chit Chat came out of that experience and time. Developers meeting daily chit chatting about what they were going to do today, what they learned, etc.
|
|
16
|
+
|
|
17
|
+
# A Story
|
|
18
|
+
|
|
19
|
+
I want a chat system that works like Discord, but I don't need the scalability of Discord. I'm just using it for my friends, small teams, not 1000 member community.
|
|
20
|
+
|
|
21
|
+
Bun is fast and javascript is fine. So let's leverage the accessiblity of both to build a small chat system that does video, audio and screenshare live streaming.
|
|
22
|
+
|
|
23
|
+
I run bun start the first time and I see a bootstrapping invite code in the console. I double-click on it and copy it to pasteboard. Then I visit https://joey-mac-mini.local:3000 (use your machine name instead of mine in hte URL) and enter it on the signup page to create the first account, it's the admin.
|
|
24
|
+
|
|
25
|
+
Upon signing in the first time, there's no communication hubs or channels. So we need to create the first ones first so the app can be in a useable state.
|
|
26
|
+
|
|
27
|
+
The system should just create a default hub and channel. That way, on bootstrap, the system is useable right off the bat. I can start chatting in a channel.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
# Getting Started
|
|
32
|
+
|
|
33
|
+
## Prerequisites
|
|
34
|
+
|
|
35
|
+
- [Bun](https://bun.sh) v1.0 or later
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
bun install
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## HTTPS requirement
|
|
44
|
+
|
|
45
|
+
Text chat works over plain HTTP. However, browsers block camera, microphone, and screen-share access on non-secure origins, so **HTTPS is required for video and audio calls**.
|
|
46
|
+
|
|
47
|
+
### Create a self-signed certificate
|
|
48
|
+
|
|
49
|
+
First, find your machine's hostname and LAN IP:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# macOS
|
|
53
|
+
hostname # e.g. joey-mac-mini.local
|
|
54
|
+
ipconfig getifaddr en0 # e.g. 192.168.1.10
|
|
55
|
+
|
|
56
|
+
# Linux
|
|
57
|
+
hostname -f
|
|
58
|
+
hostname -I | awk '{print $1}'
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Then generate the certificate, substituting your actual hostname and IP:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
mkdir -p certs
|
|
65
|
+
openssl req -x509 -newkey rsa:2048 -sha256 -nodes -days 365 \
|
|
66
|
+
-keyout certs/dev-key.pem \
|
|
67
|
+
-out certs/dev-cert.pem \
|
|
68
|
+
-subj "/CN=joey-mac-mini.local" \
|
|
69
|
+
-addext "subjectAltName = IP:192.168.1.10"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The server looks for `certs/dev-cert.pem` and `certs/dev-key.pem` by default. You can override the paths with `TLS_CERT` and `TLS_KEY` environment variables (see below).
|
|
73
|
+
|
|
74
|
+
On first visit the browser will warn about the self-signed cert — proceed past it and the warning won't reappear.
|
|
75
|
+
|
|
76
|
+
### Trust the cert (optional but recommended)
|
|
77
|
+
|
|
78
|
+
Trusting the cert silences the browser warning permanently.
|
|
79
|
+
|
|
80
|
+
**macOS:**
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
sudo security add-trusted-cert -d -r trustRoot \
|
|
84
|
+
-k /Library/Keychains/System.keychain certs/dev-cert.pem
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**Linux (Debian/Ubuntu):**
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
sudo cp certs/dev-cert.pem /usr/local/share/ca-certificates/devchitchat.crt
|
|
91
|
+
sudo update-ca-certificates
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**Windows:** Double-click `certs/dev-cert.pem`, choose "Install Certificate", place it in the "Trusted Root Certification Authorities" store.
|
|
95
|
+
|
|
96
|
+
## Start the server
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
# Production
|
|
100
|
+
bun start
|
|
101
|
+
|
|
102
|
+
# Development (auto-restarts on file changes)
|
|
103
|
+
bun dev
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The server starts on port `3000` by default. Visit `https://<your-machine>.local:3000`.
|
|
107
|
+
|
|
108
|
+
## First-time bootstrap
|
|
109
|
+
|
|
110
|
+
On the very first run, an invite code is printed to the console:
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
Invite code: https://<your-machine>.local:3000/signup?code=<token>
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Copy that URL and open it in a browser to create the first account, which becomes the admin. From there you can invite other users and set up hubs and channels.
|
|
117
|
+
|
|
118
|
+
## Environment variables
|
|
119
|
+
|
|
120
|
+
| Variable | Default | Description |
|
|
121
|
+
|---|---|---|
|
|
122
|
+
| `PORT` | `3000` | Port the server listens on |
|
|
123
|
+
| `DB_PATH` | `data/chat.db` | Path to the SQLite database file |
|
|
124
|
+
| `NODE_ENV` | `development` | Set to `production` in production |
|
|
125
|
+
| `TLS_CERT` | `certs/dev-cert.pem` | Path to the TLS certificate |
|
|
126
|
+
| `TLS_KEY` | `certs/dev-key.pem` | Path to the TLS private key |
|
|
127
|
+
|
|
128
|
+
## Run tests
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
bun test
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Backup and restore
|
|
135
|
+
|
|
136
|
+
### Backup
|
|
137
|
+
|
|
138
|
+
**Database:**
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
bun backup
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Creates a clean binary copy of the database at `data/backups/chat-<timestamp>.db` using SQLite's `VACUUM INTO`. Safe to run against a live server.
|
|
145
|
+
|
|
146
|
+
| Variable | Default | Description |
|
|
147
|
+
|---|---|---|
|
|
148
|
+
| `DB_PATH` | `data/chat.db` | Path to the source database |
|
|
149
|
+
| `BACKUP_DIR` | `data/backups` | Directory where backups are written |
|
|
150
|
+
|
|
151
|
+
**Uploads:**
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
bun backup-uploads
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Mirrors the uploads directory to a backup location. Not versioned — overwrites the destination with the current state.
|
|
158
|
+
|
|
159
|
+
| Variable | Default | Description |
|
|
160
|
+
|---|---|---|
|
|
161
|
+
| `UPLOAD_DIR` | `data/uploads` | Source uploads directory |
|
|
162
|
+
| `UPLOADS_BACKUP_DIR` | `data/backups/uploads` | Backup destination |
|
|
163
|
+
|
|
164
|
+
### Restore
|
|
165
|
+
|
|
166
|
+
#### Local (bare Bun process)
|
|
167
|
+
|
|
168
|
+
**Database:**
|
|
169
|
+
|
|
170
|
+
1. Stop the server.
|
|
171
|
+
2. Copy the backup over the live database:
|
|
172
|
+
```bash
|
|
173
|
+
cp data/backups/chat-<timestamp>.db data/chat.db
|
|
174
|
+
```
|
|
175
|
+
3. Restart the server.
|
|
176
|
+
|
|
177
|
+
**Uploads:**
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
cp -r data/backups/uploads data/uploads
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
#### Kubernetes (k3s)
|
|
184
|
+
|
|
185
|
+
**Database:**
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
bun restore path/to/chat-<timestamp>.db
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
`scripts/restore.js` handles the full sequence automatically:
|
|
192
|
+
|
|
193
|
+
1. Switches to the correct kubectl context
|
|
194
|
+
2. Scales `chat-web` down to 0 and waits for termination
|
|
195
|
+
3. Starts a temporary `busybox` helper pod mounting the `chat-web-sqlite` PVC
|
|
196
|
+
4. Uploads the backup file to the pod
|
|
197
|
+
5. Moves it into place as `/var/lib/chat/chat.db`
|
|
198
|
+
6. Fixes file permissions so the app can write to the restored database
|
|
199
|
+
7. Removes stale WAL/SHM files so SQLite opens cleanly
|
|
200
|
+
8. Removes the temporary upload file
|
|
201
|
+
9. Deletes the helper pod
|
|
202
|
+
10. Scales `chat-web` back to 1 and waits for rollout
|
|
203
|
+
|
|
204
|
+
If any step fails, the helper pod is cleaned up and the deployment is scaled back to 1 before exiting.
|
|
205
|
+
|
|
206
|
+
**Uploads:**
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
bun restore-uploads path/to/uploads-backup
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Copies the uploads directory into the PVC via a temporary helper pod. The deployment is not scaled down — uploads are static files and the copy is safe against a live server.
|
|
213
|
+
|
|
214
|
+
**Cluster env vars (both restore scripts):**
|
|
215
|
+
|
|
216
|
+
| Variable | Default | Description |
|
|
217
|
+
|---|---|---|
|
|
218
|
+
| `KUBE_CONTEXT` | `k3s-local` | kubectl context |
|
|
219
|
+
| `KUBE_NAMESPACE` | `default` | Kubernetes namespace |
|
|
220
|
+
|
|
221
|
+
#### Useful k8s backup operations
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
# Trigger the backup CronJob immediately (instead of waiting for the schedule)
|
|
225
|
+
bun backup-now
|
|
226
|
+
|
|
227
|
+
# Pull backups from the running pod to your local ../backups directory
|
|
228
|
+
bun backup-pull
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
# Docker / Kubernetes (k3s)
|
|
234
|
+
|
|
235
|
+
The included scripts build a Docker image and load it into a local [k3s](https://k3s.io) cluster.
|
|
236
|
+
|
|
237
|
+
## Cluster setup
|
|
238
|
+
|
|
239
|
+
The cluster runs k3s inside a [Lima](https://lima-vm.io) VM. Lima auto-starts on boot via launchd, so the cluster survives power outages without requiring a user login.
|
|
240
|
+
|
|
241
|
+
**One-time setup:**
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
brew install lima
|
|
245
|
+
limactl start --name=k3s template://k3s
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Merge the kubeconfig so `kubectl` can reach the cluster:
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
limactl kubeconfig k3s >> ~/.kube/config
|
|
252
|
+
# or set KUBECONFIG directly:
|
|
253
|
+
export KUBECONFIG="$HOME/.kube/config:$(limactl list k3s --format '{{.Dir}}/copied-from-guest/kubeconfig.yaml')"
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
The context is named `k3s-local` by default in this project's scripts. Rename it to match if yours differs:
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
kubectl config rename-context default k3s-local
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
**Backup paths:**
|
|
263
|
+
|
|
264
|
+
Lima mounts your home directory into the VM at the same path, so backup paths in `charts/web/values.local.yaml` are regular host paths — no special volume flags needed at cluster creation. Set them in `values.local.yaml` (gitignored):
|
|
265
|
+
|
|
266
|
+
```yaml
|
|
267
|
+
dbBackupNodePath: /Users/yourname/backups/chat-web/db-backups
|
|
268
|
+
uploadsBackupNodePath: /Users/yourname/backups/chat-web/uploads-backups
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Build and import into k3s
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
bun run docker-build
|
|
275
|
+
# or directly:
|
|
276
|
+
./docker-build-k3s.sh
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
This will:
|
|
280
|
+
1. Bump the patch version in `package.json`
|
|
281
|
+
2. Update the image tag in `charts/web/deployment.yaml`
|
|
282
|
+
3. Build the Docker image (`local/chat-web:<version>`)
|
|
283
|
+
4. Import the image into the Lima k3s instance via `limactl shell`
|
|
284
|
+
|
|
285
|
+
`LIMA_INSTANCE` controls which Lima VM the image is imported into — it maps to the `<name>` in `limactl shell <name>`. If you started your VM with `limactl start --name=k3s`, you never need to set it. Only set it if you named your VM something other than `k3s`:
|
|
286
|
+
|
|
287
|
+
```bash
|
|
288
|
+
LIMA_INSTANCE=my-instance bun run docker-build
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
## Deploy to local cluster
|
|
292
|
+
|
|
293
|
+
```bash
|
|
294
|
+
bun run local-deploy
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Applies `charts/web/deployment.yaml` to the `default` namespace of the `k3s-local` context.
|
|
298
|
+
|
|
299
|
+
## Build + deploy in one step
|
|
300
|
+
|
|
301
|
+
```bash
|
|
302
|
+
bun run push
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## Kubernetes environment variables
|
|
306
|
+
|
|
307
|
+
Override defaults via `charts/web/deployment.yaml`:
|
|
308
|
+
|
|
309
|
+
| Variable | Value in chart | Description |
|
|
310
|
+
|---|---|---|
|
|
311
|
+
| `PORT` | `8080` | Port the container exposes |
|
|
312
|
+
| `NODE_ENV` | `production` | Runtime environment |
|
|
313
|
+
| `DB_PATH` | `/var/lib/chat/chat.db` | SQLite path (backed by a 2 Gi PVC) |
|
package/index.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { createServer } from '@devchitchat/index97'
|
|
3
|
+
import { openDatabase } from './src/db/openDb.js'
|
|
4
|
+
import { initDb } from './src/db/initDb.js'
|
|
5
|
+
import { runMigrations } from './src/db/runMigrations.js'
|
|
6
|
+
import { createLogger } from './src/util/logger.js'
|
|
7
|
+
import { ChatServer } from './src/ws/ChatServer.js'
|
|
8
|
+
import { init as initContext, sessionFromRequest } from './src/context.js'
|
|
9
|
+
import { UserSettingsService } from './src/services/UserSettingsService.js'
|
|
10
|
+
import { SqliteUserSettingsRepository } from './src/adapters/SqliteUserSettingsRepository.js'
|
|
11
|
+
import { UploadService } from './src/services/UploadService.js'
|
|
12
|
+
import { LocalFileStore } from './src/adapters/LocalFileStore.js'
|
|
13
|
+
import { SqliteUploadRepository } from './src/adapters/SqliteUploadRepository.js'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Start the devchitchat server.
|
|
17
|
+
*
|
|
18
|
+
* @param {object} config
|
|
19
|
+
* @param {number} [config.port] - Port to listen on. Default: process.env.PORT ?? 3000
|
|
20
|
+
* @param {string} [config.dbPath] - Path to the SQLite database file. Default: process.env.DB_PATH ?? './data/chat.db'
|
|
21
|
+
* @param {string} [config.basePath] - URL subpath to mount the app at, e.g. '/chat'. Default: process.env.BASE_PATH ?? ''
|
|
22
|
+
* @param {boolean} [config.dev] - Enable dev mode. Default: process.env.NODE_ENV !== 'production'
|
|
23
|
+
* @param {string} [config.tlsCert] - Path to TLS certificate. Default: process.env.TLS_CERT ?? './certs/dev-cert.pem'
|
|
24
|
+
* @param {string} [config.tlsKey] - Path to TLS private key. Default: process.env.TLS_KEY ?? './certs/dev-key.pem'
|
|
25
|
+
* @returns {Promise<import('bun').Server>}
|
|
26
|
+
*/
|
|
27
|
+
export async function start(config = {}) {
|
|
28
|
+
const {
|
|
29
|
+
port = Number(process.env.PORT ?? 3000),
|
|
30
|
+
dbPath = process.env.DB_PATH ?? './data/chat.db',
|
|
31
|
+
basePath = (process.env.BASE_PATH ?? '').replace(/\/$/, ''),
|
|
32
|
+
dev = process.env.NODE_ENV !== 'production',
|
|
33
|
+
tlsCert = process.env.TLS_CERT ?? './certs/dev-cert.pem',
|
|
34
|
+
tlsKey = process.env.TLS_KEY ?? './certs/dev-key.pem',
|
|
35
|
+
} = config
|
|
36
|
+
|
|
37
|
+
const p = path => `${basePath}${path}`
|
|
38
|
+
|
|
39
|
+
const logger = createLogger()
|
|
40
|
+
const db = openDatabase(dbPath)
|
|
41
|
+
initDb(db)
|
|
42
|
+
await runMigrations(db, { logger })
|
|
43
|
+
|
|
44
|
+
const chat = new ChatServer({ db, logger })
|
|
45
|
+
const userSettingsService = new UserSettingsService({ userSettingsRepo: new SqliteUserSettingsRepository({ db }) })
|
|
46
|
+
const uploadService = new UploadService({
|
|
47
|
+
uploadRepo: new SqliteUploadRepository({ db }),
|
|
48
|
+
fileStore: new LocalFileStore(),
|
|
49
|
+
channelService: chat.channelService,
|
|
50
|
+
})
|
|
51
|
+
chat.messageService.setUploadService(uploadService)
|
|
52
|
+
|
|
53
|
+
// Wire service context so page handlers (pages/**/*.js) can access services
|
|
54
|
+
initContext({
|
|
55
|
+
auth: chat.auth,
|
|
56
|
+
hubService: chat.hubService,
|
|
57
|
+
channelService: chat.channelService,
|
|
58
|
+
messageService: chat.messageService,
|
|
59
|
+
deliveryService: chat.deliveryService,
|
|
60
|
+
searchService: chat.searchService,
|
|
61
|
+
presenceService: chat.presenceService,
|
|
62
|
+
signalingService: chat.signalingService,
|
|
63
|
+
botService: chat.botService,
|
|
64
|
+
userSettingsService,
|
|
65
|
+
uploadService,
|
|
66
|
+
reactionService: chat.reactionService,
|
|
67
|
+
logger,
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
async function getTlsIfAvailable() {
|
|
71
|
+
const cert = Bun.file(tlsCert)
|
|
72
|
+
if (await cert.exists()) {
|
|
73
|
+
return { cert, key: Bun.file(tlsKey) }
|
|
74
|
+
}
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const server = await createServer({
|
|
79
|
+
pagesDir: import.meta.dir + '/pages',
|
|
80
|
+
port,
|
|
81
|
+
dev,
|
|
82
|
+
basePath,
|
|
83
|
+
// Allow camera/mic/display for WebRTC
|
|
84
|
+
permissionsPolicy: 'camera=(self), microphone=(self), display-capture=(self)',
|
|
85
|
+
// CSP: allow WebSocket connections to self
|
|
86
|
+
csp: "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; connect-src 'self' ws: wss:",
|
|
87
|
+
// So the SSE doesn't timeout.
|
|
88
|
+
idleTimeout: 0,
|
|
89
|
+
// WebSocket upgrade route — authenticate via session cookie before the first message
|
|
90
|
+
routes: {
|
|
91
|
+
[p('/ws')]: (req, server) => {
|
|
92
|
+
const session = sessionFromRequest(req)
|
|
93
|
+
if (server.upgrade(req, {
|
|
94
|
+
data: session ? { userId: session.user.user_id, sessionId: session.session_id, displayName: session.user.display_name } : {}
|
|
95
|
+
})) return
|
|
96
|
+
return new Response('WebSocket upgrade required', { status: 426 })
|
|
97
|
+
},
|
|
98
|
+
[p('/vendor/rdbl.js')]: () => {
|
|
99
|
+
return new Response(Bun.file(new URL(import.meta.resolve('@devchitchat/rdbljs/src/rdbl.js'))), {
|
|
100
|
+
headers: { 'Content-Type': 'text/javascript' },
|
|
101
|
+
})
|
|
102
|
+
},
|
|
103
|
+
// Service worker at basePath scope so it can receive push events for all app pages
|
|
104
|
+
[p('/sw.js')]: () => new Response(
|
|
105
|
+
Bun.file(import.meta.dir + '/pages/public/sw.js'),
|
|
106
|
+
{ headers: { 'Content-Type': 'application/javascript; charset=utf-8', 'Service-Worker-Allowed': `${basePath}/`, 'Cache-Control': 'no-cache, no-store' } }
|
|
107
|
+
),
|
|
108
|
+
// Dynamic PWA manifest — start_url and icon.src must reflect basePath
|
|
109
|
+
[p('/manifest.json')]: () => new Response(
|
|
110
|
+
JSON.stringify({
|
|
111
|
+
name: 'devchitchat',
|
|
112
|
+
short_name: 'devchitchat',
|
|
113
|
+
start_url: `${basePath}/`,
|
|
114
|
+
scope: `${basePath}/`,
|
|
115
|
+
display: 'standalone',
|
|
116
|
+
background_color: '#1a1b1e',
|
|
117
|
+
theme_color: '#141517',
|
|
118
|
+
icons: [
|
|
119
|
+
{ src: `${basePath}/icon.png`, sizes: '300x300', type: 'image/png', purpose: 'any maskable' }
|
|
120
|
+
]
|
|
121
|
+
}),
|
|
122
|
+
{ headers: { 'Content-Type': 'application/manifest+json', 'Cache-Control': 'no-cache' } }
|
|
123
|
+
),
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
// Bun native WebSocket handler (new index97 passthrough)
|
|
127
|
+
websocket: chat.websocket,
|
|
128
|
+
tls: await getTlsIfAvailable(),
|
|
129
|
+
onShutdown: (server) => {
|
|
130
|
+
logger.info('server.shutdown', {})
|
|
131
|
+
server.stop()
|
|
132
|
+
db.close()
|
|
133
|
+
process.exit(0)
|
|
134
|
+
},
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
// Give ChatServer a reference to the Bun server so it can publish to topics
|
|
138
|
+
chat.attachServer(server)
|
|
139
|
+
|
|
140
|
+
logger.info('server.ready', { port: server.port, dev, basePath })
|
|
141
|
+
|
|
142
|
+
return server
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Run directly when invoked as CLI / bin
|
|
146
|
+
if (import.meta.main) {
|
|
147
|
+
await start()
|
|
148
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Make channels.hub_id nullable so DM channels (kind = 'dm') can exist
|
|
3
|
+
* without a hub. SQLite does not support ALTER COLUMN, so we recreate the table.
|
|
4
|
+
*
|
|
5
|
+
* Each statement is a separate db.exec() call — bun:sqlite does not reliably
|
|
6
|
+
* execute all statements in a single multi-statement exec() inside a transaction.
|
|
7
|
+
*/
|
|
8
|
+
export function run(db) {
|
|
9
|
+
db.exec(`
|
|
10
|
+
CREATE TABLE channels_new (
|
|
11
|
+
channel_id TEXT PRIMARY KEY,
|
|
12
|
+
hub_id TEXT,
|
|
13
|
+
kind TEXT NOT NULL,
|
|
14
|
+
name TEXT NOT NULL,
|
|
15
|
+
topic TEXT,
|
|
16
|
+
visibility TEXT NOT NULL,
|
|
17
|
+
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
18
|
+
created_by_user_id TEXT NOT NULL,
|
|
19
|
+
created_at INTEGER NOT NULL,
|
|
20
|
+
deleted_at INTEGER,
|
|
21
|
+
FOREIGN KEY(created_by_user_id) REFERENCES users(user_id)
|
|
22
|
+
)
|
|
23
|
+
`)
|
|
24
|
+
|
|
25
|
+
db.exec(`
|
|
26
|
+
INSERT INTO channels_new
|
|
27
|
+
SELECT channel_id, hub_id, kind, name, topic, visibility,
|
|
28
|
+
sort_order, created_by_user_id, created_at, deleted_at
|
|
29
|
+
FROM channels
|
|
30
|
+
`)
|
|
31
|
+
|
|
32
|
+
db.exec(`DROP TABLE channels`)
|
|
33
|
+
|
|
34
|
+
db.exec(`ALTER TABLE channels_new RENAME TO channels`)
|
|
35
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Add mention tracking and message priority to support the notifications system.
|
|
3
|
+
*
|
|
4
|
+
* deliveries.mention_seq — seq of the last unread @mention for this user in this channel.
|
|
5
|
+
* 0 = no pending mention. Cleared when the user's after_seq advances past it.
|
|
6
|
+
*
|
|
7
|
+
* messages.priority — sender-chosen urgency: 'normal' | 'async' | 'now'.
|
|
8
|
+
* 'normal' → passive catch-up only (default)
|
|
9
|
+
* 'async' → subtle sound + queued into reconnect digest
|
|
10
|
+
* 'now' → push + sound, bypasses DND
|
|
11
|
+
*/
|
|
12
|
+
export function run(db) {
|
|
13
|
+
try { db.exec(`ALTER TABLE deliveries ADD COLUMN mention_seq INTEGER NOT NULL DEFAULT 0`) } catch { /* already exists */ }
|
|
14
|
+
try { db.exec(`ALTER TABLE messages ADD COLUMN priority TEXT NOT NULL DEFAULT 'normal'`) } catch { /* already exists */ }
|
|
15
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function run(db) {
|
|
2
|
+
db.exec(`
|
|
3
|
+
CREATE TABLE IF NOT EXISTS uploads (
|
|
4
|
+
upload_id TEXT PRIMARY KEY,
|
|
5
|
+
uploader_user_id TEXT NOT NULL REFERENCES users(user_id),
|
|
6
|
+
channel_id TEXT NOT NULL REFERENCES channels(channel_id),
|
|
7
|
+
msg_id TEXT REFERENCES messages(msg_id),
|
|
8
|
+
original_name TEXT NOT NULL,
|
|
9
|
+
stored_name TEXT NOT NULL,
|
|
10
|
+
mime_type TEXT NOT NULL,
|
|
11
|
+
size_bytes INTEGER NOT NULL,
|
|
12
|
+
created_at INTEGER NOT NULL
|
|
13
|
+
)
|
|
14
|
+
`)
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
db.exec(`ALTER TABLE messages ADD COLUMN attachments_json TEXT`)
|
|
18
|
+
} catch {
|
|
19
|
+
// column already exists — idempotent
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function run(db) {
|
|
2
|
+
db.exec(`
|
|
3
|
+
CREATE TABLE IF NOT EXISTS push_subscriptions (
|
|
4
|
+
sub_id TEXT PRIMARY KEY,
|
|
5
|
+
user_id TEXT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
|
|
6
|
+
endpoint TEXT NOT NULL UNIQUE,
|
|
7
|
+
p256dh TEXT NOT NULL,
|
|
8
|
+
auth TEXT NOT NULL,
|
|
9
|
+
created_at INTEGER NOT NULL,
|
|
10
|
+
last_used_at INTEGER
|
|
11
|
+
);
|
|
12
|
+
CREATE INDEX IF NOT EXISTS idx_push_subscriptions_user_id ON push_subscriptions (user_id);
|
|
13
|
+
`)
|
|
14
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function run(db) {
|
|
2
|
+
db.exec(`
|
|
3
|
+
CREATE TABLE IF NOT EXISTS message_reactions (
|
|
4
|
+
reaction_id TEXT PRIMARY KEY,
|
|
5
|
+
msg_id TEXT NOT NULL REFERENCES messages(msg_id) ON DELETE CASCADE,
|
|
6
|
+
channel_id TEXT NOT NULL,
|
|
7
|
+
user_id TEXT NOT NULL,
|
|
8
|
+
emoji TEXT NOT NULL,
|
|
9
|
+
ts INTEGER NOT NULL,
|
|
10
|
+
UNIQUE (msg_id, user_id, emoji)
|
|
11
|
+
);
|
|
12
|
+
CREATE INDEX IF NOT EXISTS idx_reactions_msg ON message_reactions (msg_id);
|
|
13
|
+
CREATE INDEX IF NOT EXISTS idx_reactions_channel ON message_reactions (channel_id);
|
|
14
|
+
`)
|
|
15
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@devchitchat/chat",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "A small chat app. p2p video and screenshare.",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "bun --watch index.js",
|
|
7
|
+
"start": "bun index.js",
|
|
8
|
+
"migrate": "bun scripts/migrate.js",
|
|
9
|
+
"test": "bun test",
|
|
10
|
+
"generate-vapid": "bun scripts/generate-vapid.js",
|
|
11
|
+
"create-secrets": "kubectl create secret generic chat-web-vapid --from-env-file=.env --namespace=${KUBE_NAMESPACE:-default} --dry-run=client -o yaml | kubectl apply -f -",
|
|
12
|
+
"docker-build": "./docker-build-k3s.sh",
|
|
13
|
+
"local-deploy": "kubectl config use-context ${KUBE_CONTEXT:-k3s-local} && kubectl apply -f charts/web/templates/deployment.yaml -n ${KUBE_NAMESPACE:-default}",
|
|
14
|
+
"backup": "bun scripts/backup.js",
|
|
15
|
+
"backup-uploads": "bun scripts/backup-uploads.js",
|
|
16
|
+
"deploy-backup": "kubectl config use-context ${KUBE_CONTEXT:-k3s-local} && helm upgrade --install chat-web charts/web -f charts/web/values.local.yaml -n ${KUBE_NAMESPACE:-default}",
|
|
17
|
+
"backup-now": "kubectl config use-context ${KUBE_CONTEXT:-k3s-local} && kubectl create job --from=cronjob/chat-backup chat-backup-$(date +%s) -n ${KUBE_NAMESPACE:-default}",
|
|
18
|
+
"backup-pull": "kubectl config use-context ${KUBE_CONTEXT:-k3s-local} && kubectl cp $(kubectl get pod -l app=chat-web -o jsonpath='{.items[0].metadata.name}' -n ${KUBE_NAMESPACE:-default}):/var/lib/chat/backups ../backups -n ${KUBE_NAMESPACE:-default}",
|
|
19
|
+
"restore": "bun scripts/restore.js",
|
|
20
|
+
"restore-uploads": "bun scripts/restore-uploads.js",
|
|
21
|
+
"push": "bun docker-build && bun local-deploy",
|
|
22
|
+
"purge-cf-cache": "bun scripts/purge-cf-cache.js"
|
|
23
|
+
},
|
|
24
|
+
"type": "module",
|
|
25
|
+
"bin": {
|
|
26
|
+
"devchitchat": "index.js"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"index.js",
|
|
30
|
+
"src/",
|
|
31
|
+
"pages/",
|
|
32
|
+
"migrate/",
|
|
33
|
+
"styles.css"
|
|
34
|
+
],
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@devchitchat/index97": "^2.1.2",
|
|
37
|
+
"@devchitchat/rdbljs": "^2.0.1"
|
|
38
|
+
},
|
|
39
|
+
"main": "index.js",
|
|
40
|
+
"module": "index.js",
|
|
41
|
+
"exports": {
|
|
42
|
+
".": "./index.js"
|
|
43
|
+
},
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "https://github.com/devchitchat/chat.git"
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
}
|
|
51
|
+
}
|