Skip to content
quarry Artifact [bdf4a664f1]
Login

Artifact [bdf4a664f1]

Artifact bdf4a664f1741e47228f6ecfe981cced65e69bb6fb12d3c2807c74b5c44c508d:


// read.go — forum read-state. Marking a thread read is an EXPLICIT act
// with its own op, not a side effect of fetching one. forum.get stays a
// pure query, so an API client, a crawler or a feed reader can read a
// thread's contents without silently spending someone's unread state.
// The web thread page calls this because a person opening a page really
// is reading it; nothing else has to.
package api

import (
	"context"
	"fmt"

	"craftdesign.group/quarry/internal/ops"
)

type forumReadIn struct {
	Repo string `json:"repo" validate:"required,max=64"`
	// Root empty means the whole repo: mark everything read up to now.
	// One watermark write instead of one row per thread.
	Root string `json:"root" validate:"max=64"`
}

type forumReadOut struct {
	Root string `json:"root,omitempty"`
	All  bool   `json:"all,omitempty"`
}

func registerRead(r *ops.Registry, d Deps) error {
	return ops.Register(r, ops.Op[forumReadIn, forumReadOut]{
		Name:  "forum.read",
		Caps:  "3",
		Doc:   "Mark a forum thread read, or the whole forum when root is omitted. Read means seen-with-nothing-new-since, so a later reply makes a thread unread again on its own.",
		Kind:  ops.Command,
		Auth:  ops.Token,
		Route: "POST /api/v1/r/{repo}/forum/read",
		Run: func(ctx context.Context, in forumReadIn) (forumReadOut, error) {
			repo, err := d.forumRepoFor(ctx, in.Repo)
			if err != nil {
				return forumReadOut{}, err
			}
			// Read-state is keyed by account for the same reason a vote
			// is: without one, every stranger would share a single pile
			// of "already seen".
			account := ops.User(ctx)
			if account == "" {
				return forumReadOut{}, fmt.Errorf("marking read needs a signed-in account: %w", ops.ErrInvalid)
			}
			if in.Root == "" {
				return forumReadOut{All: true}, d.Forum.MarkAllForumRead(in.Repo, account)
			}
			// The thread's own newest post is the mark, not the wall
			// clock: it is what a later reply must beat to make the
			// thread unread again, and it cannot drift with a slow
			// render or a clock skew between forge and index.
			_, posts, ok, err := repo.Index.ForumThreadPosts(in.Root)
			if err != nil {
				return forumReadOut{}, err
			}
			if !ok || len(posts) == 0 {
				return forumReadOut{}, fmt.Errorf("no forum thread %q: %w", in.Root, ops.ErrNotFound)
			}
			root, last := posts[0].Hash, posts[0].Date
			for _, p := range posts {
				if p.Date > last {
					last = p.Date
				}
			}
			return forumReadOut{Root: root}, d.Forum.MarkForumRead(in.Repo, account, root, last)
		},
	})
}