Skip to content

Commit 9b43f27

Browse files
markmnlclaude
andauthored
Notify participant domains and push recipients_added on add-to (#34)
AddRecipients now records one msg_add_to_notify row per remote participant domain of the message -- the domains of from and every to address, excluding domains hosting one of the batch's new recipients (they learn through normal delivery) and the local domain (this database is its record) -- so fmsgd delivers the add-to message to every participant domain per SPEC §10.2, not only the new batch's. Addresses already added to the message are now rejected up front: msg_add_to is unique per (msg, addr), so re-adding silently no-opped and could leave a batch with no recipients, which fmsgd would deliver as an invalid add-to message. Re-adding an original to recipient stays allowed (SPEC §10.3 NOTE II). The websocket hub listens on the new recipients_added channel (fired by fmsgd's dd.sql when a batch is recorded, whether added locally or received from a remote host) and pushes the refreshed message to every connected participant, closing the gap where existing participants got no realtime signal that recipients were added. Requires fmsgd's updated dd.sql (msg_add_to_notify table and recipients_added trigger). Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent bd43ee3 commit 9b43f27

2 files changed

Lines changed: 64 additions & 3 deletions

File tree

internal/handlers/hub.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ import (
1515
// Event type discriminators for the WebSocket envelope. Adding a new event
1616
// type means adding a constant here and a producer that dispatches it.
1717
const (
18-
eventNewMsg = "new_msg"
19-
eventDelivered = "delivered"
18+
eventNewMsg = "new_msg"
19+
eventDelivered = "delivered"
20+
eventRecipientsAdded = "recipients_added"
2021
)
2122

2223
// wsEnvelope is the JSON shape of every frame pushed over a WebSocket. The
@@ -128,7 +129,10 @@ func (h *Hub) listen(ctx context.Context, onConnected func()) error {
128129
if _, err := conn.Exec(ctx, "LISTEN delivered"); err != nil {
129130
return err
130131
}
131-
log.Println("ws hub: listening on new_msg, delivered")
132+
if _, err := conn.Exec(ctx, "LISTEN recipients_added"); err != nil {
133+
return err
134+
}
135+
log.Println("ws hub: listening on new_msg, delivered, recipients_added")
132136
onConnected()
133137

134138
for {
@@ -155,6 +159,12 @@ func (h *Hub) listen(ctx context.Context, onConnected func()) error {
155159
// addr here is the message's sender (see notify_delivered in
156160
// dd.sql), not a recipient -- no Web Push for this event yet.
157161
h.dispatch(ctx, msgID, addr, eventDelivered)
162+
case "recipients_added":
163+
// An add-to batch was recorded against the message; addr is one
164+
// of its participants (see notify_recipients_added in fmsgd's
165+
// dd.sql). Pushes the refreshed message so clients can show the
166+
// updated recipient list.
167+
h.dispatch(ctx, msgID, addr, eventRecipientsAdded)
158168
default:
159169
log.Printf("ws hub: ignoring notification on unknown channel %q", n.Channel)
160170
}

internal/handlers/messages.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -917,6 +917,30 @@ func (h *MessageHandler) AddRecipients(c *gin.Context) {
917917
return
918918
}
919919

920+
// Reject addresses already added to this message: msg_add_to is unique per
921+
// (msg, addr), so re-adding would silently no-op and could leave a batch
922+
// with no recipients — which fmsgd would then deliver as an invalid add-to
923+
// message. Re-adding an original to recipient stays allowed (SPEC §10.3
924+
// NOTE II — it re-sends the message to a recipient who may no longer have
925+
// it).
926+
loweredAddTo := make([]string, len(input.AddTo))
927+
for i, addr := range input.AddTo {
928+
loweredAddTo[i] = strings.ToLower(addr)
929+
}
930+
var alreadyAdded int
931+
if err = h.DB.Pool.QueryRow(ctx,
932+
"SELECT COUNT(*) FROM msg_add_to WHERE msg_id = $1 AND lower(addr) = ANY($2)",
933+
msgID, loweredAddTo,
934+
).Scan(&alreadyAdded); err != nil {
935+
log.Printf("add recipients: check existing for msg %d: %v", msgID, err)
936+
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to add recipients"})
937+
return
938+
}
939+
if alreadyAdded > 0 {
940+
c.JSON(http.StatusBadRequest, gin.H{"error": "address(es) already added to this message"})
941+
return
942+
}
943+
920944
// Insert the new add_to recipients and record who added them. Both run in a
921945
// single transaction so a partial failure leaves the message unchanged.
922946
tx, err := h.DB.Pool.Begin(ctx)
@@ -952,6 +976,33 @@ func (h *MessageHandler) AddRecipients(c *gin.Context) {
952976
}
953977
}
954978

979+
// SPEC §10.2: an add-to message is delivered to every participant domain
980+
// of the message being added to — the domains of from and every to address
981+
// — not only the domains hosting the new recipients, so all existing
982+
// participants learn recipients were added. Domains hosting one of this
983+
// batch's new recipients learn through normal delivery, and the local
984+
// domain's record is this database itself, so neither needs a notify row.
985+
newDomains := make([]string, 0, len(input.AddTo))
986+
for _, addr := range input.AddTo {
987+
_, domain := parseAddr(addr)
988+
newDomains = append(newDomains, strings.ToLower(domain))
989+
}
990+
if _, err = tx.Exec(ctx, `
991+
INSERT INTO msg_add_to_notify (batch_id, domain)
992+
SELECT DISTINCT $1::bigint, lower(split_part(p.addr, '@', 3))
993+
FROM (
994+
SELECT from_addr AS addr FROM msg WHERE id = $2
995+
UNION
996+
SELECT addr FROM msg_to WHERE msg_id = $2
997+
) p
998+
WHERE lower(split_part(p.addr, '@', 3)) <> lower($3)
999+
AND NOT (lower(split_part(p.addr, '@', 3)) = ANY($4))
1000+
`, batchID, msgID, h.LocalDomain, newDomains); err != nil {
1001+
log.Printf("add recipients: insert notify rows for msg %d: %v", msgID, err)
1002+
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to add recipients"})
1003+
return
1004+
}
1005+
9551006
if err = tx.Commit(ctx); err != nil {
9561007
log.Printf("add recipients: commit tx for msg %d: %v", msgID, err)
9571008
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to add recipients"})

0 commit comments

Comments
 (0)