Artifact 6b2c2ae540dea76387f976c5da5210a4b3f534cca339677db5023e9278169b27:
- File
internal/api/subs.go
— part of check-in
[893885b3d5]
at
2026-08-21 19:49:17
on branch quarry-forum
— forum subscriptions: auto-sub on authoring, reply/mention fanout, unsub sticks
Fanout rides the write path only (a reindex can never re-notify); delivery/dedup stays in the notify companion via TxnID. (user: dmestas, size: 2993) [annotate] [blame] [check-ins using] [more...]
// subs.go — forum subscriptions and their notifications. Delivery, // inbox, per-recipient dedup (TxnID) and email are the notify // companion's job; quarry's job is deciding WHO hears about WHAT. // Notifications fire only on the WRITE path — never at fold — so a // reindex can never re-notify (0001 §5: replay must be silent). package api import ( "context" "fmt" "craftdesign.group/quarry/internal/ops" ) // forumFanout is everything that happens socially after a post is born: // the author auto-subscribes (never overriding an explicit unsub), // thread subscribers other than the actor hear about it, and @mentions // route to their people. func forumFanout(d Deps, repo, thread, hash, actor, title, body string) { if d.Forum == nil { return } d.Forum.AutoSubscribe(repo, actor, thread) link := "/r/" + repo + "/forum/" + thread[:min(16, len(thread))] subs, err := d.Forum.ForumSubscribers(repo, thread) if err == nil { for _, who := range subs { if who == actor { continue } d.fireNotify(NotifyEvent{ Workflow: "quarry-forum-reply", To: who, Payload: map[string]any{"repo": repo, "from": actor, "title": title, "text": body, "link": link}, // Post hash + recipient: replays and retries collapse. TxnID: "forum-reply:" + hash + ":" + who, }) } } for _, who := range mentions(body) { if who == actor { continue } d.fireNotify(NotifyEvent{ Workflow: "quarry-mention", To: who, Payload: map[string]any{"repo": repo, "from": actor, "text": body, "link": link}, TxnID: "mention:forum:" + hash + ":" + who, }) } } type forumSubIn struct { Repo string `json:"repo" validate:"required,max=64"` Root string `json:"root" validate:"required,min=4,max=64"` State string `json:"state" validate:"oneof=sub|unsub"` } type forumSubOut struct { Root string `json:"root"` State string `json:"state"` } func registerSubs(r *ops.Registry, d Deps) error { return ops.Register(r, ops.Op[forumSubIn, forumSubOut]{ Name: "forum.subscribe", Doc: "Subscribe to (or leave) a thread. An explicit unsubscribe outlives later replies — posting again never quietly re-subscribes.", Kind: ops.Command, Auth: ops.Token, Route: "POST /api/v1/r/{repo}/forum/{root}/subscribe", Run: func(ctx context.Context, in forumSubIn) (forumSubOut, error) { repo, err := d.repoFor(ctx, in.Repo) if err != nil { return forumSubOut{}, err } account := ops.User(ctx) if account == "" { return forumSubOut{}, fmt.Errorf("subscribing needs a signed-in account: %w", ops.ErrInvalid) } _, thread, ok, err := repo.Index.ForumPostThread(in.Root) if err != nil { return forumSubOut{}, err } if !ok { return forumSubOut{}, fmt.Errorf("no forum thread %q: %w", in.Root, ops.ErrNotFound) } if err := d.Forum.SetForumSub(in.Repo, account, thread, in.State); err != nil { return forumSubOut{}, err } return forumSubOut{Root: thread, State: in.State}, nil }, }) }