@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.
Files changed (131) hide show
  1. package/README.md +313 -0
  2. package/index.js +148 -0
  3. package/migrate/001-drop-channel-invites.js +3 -0
  4. package/migrate/002-invite-initial-roles.js +5 -0
  5. package/migrate/003-dm-channels.js +35 -0
  6. package/migrate/004-notifications.js +15 -0
  7. package/migrate/005-uploads.js +21 -0
  8. package/migrate/006-mention-priority.js +3 -0
  9. package/migrate/007-push-subscriptions.js +14 -0
  10. package/migrate/008-messages-channel-seq-index.js +3 -0
  11. package/migrate/009-message-reactions.js +15 -0
  12. package/migrate/010-edit-messages.js +7 -0
  13. package/package.json +51 -0
  14. package/pages/_error.html +12 -0
  15. package/pages/_layout.html +31 -0
  16. package/pages/_layout.js +13 -0
  17. package/pages/admin/_layout.html +52 -0
  18. package/pages/admin/_layout.js +8 -0
  19. package/pages/admin/bots/[userId].js +88 -0
  20. package/pages/admin/bots/[userId].phtml +89 -0
  21. package/pages/admin/bots/index.js +41 -0
  22. package/pages/admin/bots/index.phtml +58 -0
  23. package/pages/admin/index.js +8 -0
  24. package/pages/admin/invites/index.js +72 -0
  25. package/pages/admin/invites/index.phtml +88 -0
  26. package/pages/admin/users/[userId].js +60 -0
  27. package/pages/admin/users/[userId].phtml +57 -0
  28. package/pages/admin/users/index.js +20 -0
  29. package/pages/admin/users/index.phtml +37 -0
  30. package/pages/api/uploads/index.js +66 -0
  31. package/pages/api/user/settings.js +26 -0
  32. package/pages/auth/signout.js +14 -0
  33. package/pages/channels/[channelId].js +99 -0
  34. package/pages/channels/[channelId].phtml +173 -0
  35. package/pages/index.js +33 -0
  36. package/pages/invite/[token].js +10 -0
  37. package/pages/login/index.js +57 -0
  38. package/pages/login/index.phtml +29 -0
  39. package/pages/public/client/action-sheet.js +77 -0
  40. package/pages/public/client/app.js +38 -0
  41. package/pages/public/client/auth-tabs.js +13 -0
  42. package/pages/public/client/emoji-data.js +197 -0
  43. package/pages/public/client/islands/call.js +1770 -0
  44. package/pages/public/client/islands/sidebar.js +1197 -0
  45. package/pages/public/client/long-press.js +59 -0
  46. package/pages/public/client/modal.js +50 -0
  47. package/pages/public/client/router.js +87 -0
  48. package/pages/public/client/rtc-peer-manager.js +344 -0
  49. package/pages/public/client/settings-sync.js +76 -0
  50. package/pages/public/client/shared/messages.js +147 -0
  51. package/pages/public/client/swipe-nav.js +98 -0
  52. package/pages/public/client/theme.js +27 -0
  53. package/pages/public/client/ws.js +71 -0
  54. package/pages/public/favicon.ico +0 -0
  55. package/pages/public/favicon.png +0 -0
  56. package/pages/public/icon.png +0 -0
  57. package/pages/public/manifest.json +11 -0
  58. package/pages/public/sw.js +38 -0
  59. package/pages/public/themes/base.css +1786 -0
  60. package/pages/public/themes/dark.css +22 -0
  61. package/pages/public/themes/forest.css +22 -0
  62. package/pages/public/themes/light.css +23 -0
  63. package/pages/public/themes/ocean.css +22 -0
  64. package/pages/public/themes/rose.css +22 -0
  65. package/pages/registration/index.js +35 -0
  66. package/pages/registration/index.phtml +38 -0
  67. package/pages/uploads/[uploadId]/[filename].js +45 -0
  68. package/src/adapters/InMemoryAuthRepository.js +74 -0
  69. package/src/adapters/InMemoryChannelRepository.js +138 -0
  70. package/src/adapters/InMemoryDeliveryRepository.js +52 -0
  71. package/src/adapters/InMemoryFileStore.js +53 -0
  72. package/src/adapters/InMemoryHubRepository.js +85 -0
  73. package/src/adapters/InMemoryMessageRepository.js +35 -0
  74. package/src/adapters/InMemoryReactionRepository.js +45 -0
  75. package/src/adapters/InMemorySearchRepository.js +37 -0
  76. package/src/adapters/InMemorySignalingRepository.js +35 -0
  77. package/src/adapters/InMemoryUploadRepository.js +36 -0
  78. package/src/adapters/InMemoryUserSettingsRepository.js +15 -0
  79. package/src/adapters/LocalFileStore.js +40 -0
  80. package/src/adapters/SqliteAuthRepository.js +184 -0
  81. package/src/adapters/SqliteChannelRepository.js +149 -0
  82. package/src/adapters/SqliteDeliveryRepository.js +53 -0
  83. package/src/adapters/SqliteHubRepository.js +99 -0
  84. package/src/adapters/SqliteMessageRepository.js +90 -0
  85. package/src/adapters/SqlitePushRepository.js +39 -0
  86. package/src/adapters/SqliteReactionRepository.js +50 -0
  87. package/src/adapters/SqliteSearchRepository.js +42 -0
  88. package/src/adapters/SqliteSignalingRepository.js +50 -0
  89. package/src/adapters/SqliteUploadRepository.js +34 -0
  90. package/src/adapters/SqliteUserSettingsRepository.js +23 -0
  91. package/src/adminAuth.js +25 -0
  92. package/src/config.js +11 -0
  93. package/src/context.js +77 -0
  94. package/src/core/dm.js +10 -0
  95. package/src/core/mentions.js +27 -0
  96. package/src/core/messages.js +21 -0
  97. package/src/core/reactions.js +6 -0
  98. package/src/core/roles.js +5 -0
  99. package/src/core/uploads.js +107 -0
  100. package/src/db/initDb.js +225 -0
  101. package/src/db/openDb.js +18 -0
  102. package/src/db/runMigrations.js +45 -0
  103. package/src/db/transaction.js +11 -0
  104. package/src/ports/IFileStore.js +34 -0
  105. package/src/services/AuthService.js +208 -0
  106. package/src/services/BotService.js +148 -0
  107. package/src/services/ChannelService.js +176 -0
  108. package/src/services/DeliveryService.js +28 -0
  109. package/src/services/HubService.js +133 -0
  110. package/src/services/MessageService.js +122 -0
  111. package/src/services/NotificationService.js +45 -0
  112. package/src/services/PresenceService.js +55 -0
  113. package/src/services/ReactionService.js +57 -0
  114. package/src/services/SearchService.js +21 -0
  115. package/src/services/SignalingService.js +177 -0
  116. package/src/services/UploadService.js +111 -0
  117. package/src/services/UserSettingsService.js +30 -0
  118. package/src/services/WebPushService.js +217 -0
  119. package/src/util/crypto.js +21 -0
  120. package/src/util/errors.js +14 -0
  121. package/src/util/ids.js +3 -0
  122. package/src/util/logger.js +21 -0
  123. package/src/ws/ChatServer.js +478 -0
  124. package/src/ws/handlers/authHandlers.js +152 -0
  125. package/src/ws/handlers/channelHandlers.js +166 -0
  126. package/src/ws/handlers/hubHandlers.js +82 -0
  127. package/src/ws/handlers/messageHandlers.js +88 -0
  128. package/src/ws/handlers/pushHandlers.js +25 -0
  129. package/src/ws/handlers/reactionHandlers.js +27 -0
  130. package/src/ws/handlers/rtcHandlers.js +126 -0
  131. 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,3 @@
1
+ export function run(db) {
2
+ db.exec(`DROP TABLE IF EXISTS channel_invites`)
3
+ }
@@ -0,0 +1,5 @@
1
+ export function run(db) {
2
+ try {
3
+ db.exec(`ALTER TABLE invites ADD COLUMN initial_roles_json TEXT NOT NULL DEFAULT '["user"]'`)
4
+ } catch { /* column already exists — initDb creates it on fresh installs */ }
5
+ }
@@ -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,3 @@
1
+ export function run(db) {
2
+ try { db.exec(`ALTER TABLE deliveries ADD COLUMN mention_priority TEXT NOT NULL DEFAULT 'normal'`) } catch { /* already exists */ }
3
+ }
@@ -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,3 @@
1
+ export function run(db) {
2
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_messages_channel_seq ON messages (channel_id, seq)`)
3
+ }
@@ -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
+ }
@@ -0,0 +1,7 @@
1
+ export function run(db) {
2
+ try {
3
+ db.exec(`ALTER TABLE messages ADD COLUMN edited_at INTEGER`)
4
+ } catch (e) {
5
+ if (!e.message.includes('duplicate column name')) throw e
6
+ }
7
+ }
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
+ }
@@ -0,0 +1,12 @@
1
+ <!-- pages/_error.html -->
2
+ <!DOCTYPE html>
3
+ <html lang="en">
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <title>{{status}} — {{title}}</title>
7
+ </head>
8
+ <body>
9
+ <h1>{{status}} — {{title}}</h1>
10
+ <p>{{message}}</p>
11
+ </body>
12
+ </html>