Artifact bb4c3571110653742bc52b8b8172ab1d941d7b9185ea0c298863b5c5f77ae756:
- File
internal/api/subs.go
— part of check-in
[31437ec328]
at
2026-08-22 16:11:52
on branch fossil-caps
— caps phases 2-3: ops declare fossil capabilities; router and web gate on them
RFC 0004 §4 table encoded on all ~45 ops; RouterWithCaps refines each Auth floor with EffectiveCaps; web gates and TrustedAuthor read caps with role fallback. Harness proves o/3/a separation end to end. (user: dmestas, size: 3385) [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, repliedTo 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 { // The author being ANSWERED hears about it whether or not they // subscribe — fossil's reply DAG, carried into delivery. The // TxnID matches the subscriber path's, so notify dedups when // they are both. if repliedTo != "" && repliedTo != actor { subs = append(subs, repliedTo) } seen := map[string]bool{} for _, who := range subs { if who == actor || seen[who] { continue } seen[who] = true 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", Caps: "3", 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 }, }) }