feat: email a recipient about messages that arrive while they are away - #2
Merged
Merged
Conversation
The unread badge only reaches someone who has the site open. Nothing told the person who had closed it. A recipient counts as away when their UserHub holds no WebSocket: the frontend opens one per visit covering the whole site, so a closed conversation is not being away, a closed site is. ChatRoom's existing is_offline flag answers a different question (nobody else in *this* room) and still drives only the log line on Django's side. At most one email per conversation per unread streak. The hub records the room in emailNotifiedRooms and clears it when the recipient opens the conversation, via the mark-read path the frontend already calls. The mark is written before the request goes out, not after: the send is a fire-and-forget waitUntil nobody awaits, and one missed email while Django is down beats one email per message for as long as it stays down. The Worker does not send mail itself. It calls the DJANGO_WEBHOOK_URL it already has, with an `event` discriminator, and Django owns the templates, the address, and every "should this person be mailed at all" rule — a deactivated account, a conversation they deleted. The decision is a pure function in offlineEmailPolicy.ts so it can be tested without a Durable Object or a workerd runtime, like the image URL policy next to it. ChatRoom's private webhook-URL guard moved to webhookUrlPolicy.ts now that both DOs call out, so the SSRF check cannot end up being the one UserHub forgot. Verified end to end against a real Django and Mailjet, not a mock: four messages with a mark-read in the middle produced exactly two emails (the first, and the one after the read), and a message sent while the recipient's hub was connected produced none.
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
cfedgechat | 7f6e4c2 | Sep 13 2026, 06:10 AM |
This was referenced Sep 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
未讀通知的第一段:判斷收訊者是否「不在線上」,以及每個對話只寄一次的去重。實際寄信在 UniBooks-BE 那側。
規則
UserHub上有沒有 WebSocket。前端每次造訪整站只開一條,所以「沒打開那個對話」不算離線,「沒打開網頁」才算。ChatRoom原有的is_offline(房間裡沒有別人)是另一個問題,維持只驅動 Django 端的 log。emailNotifiedRooms記錄已寄過的 room,收訊者點開對話(既有的 mark-read 路徑)時清除。標記是在送出請求「之前」寫入的:送信是沒人等待結果的
waitUntil,Django 掛掉時漏一封,好過它掛著的期間每則訊息各寄一封。實作
Worker 不自己寄信,而是呼叫既有的
DJANGO_WEBHOOK_URL,用event欄位區分;信件模板、收件地址、以及「這個人到底該不該收信」(停用帳號、自己刪掉的對話)都留在 Django。offlineEmailPolicy.ts(新):判斷邏輯抽成純函式,不需要 Durable Object 或 workerd 就能測,比照旁邊的 image URL policy。webhookUrlPolicy.ts(新):ChatRoom原本私有的 webhook URL 檢查移出共用 — 兩個 DO 都會對外呼叫了,這道 SSRF 防護不能變成「只有其中一個記得做」。驗證
npm run typecheck乾淨。相依
需要搭配 CycleUni/UniBooks-BE 的對應 PR 才會真的寄出信件;單獨合併此 PR 只會讓 webhook 多送一個 Django 尚未認識的 event,Django 會走既有分支處理、不會出錯。