inital commit

This commit is contained in:
2026-05-26 20:21:55 +02:00
parent 965f9d6ad4
commit 0ae20aa68d
21 changed files with 651 additions and 111 deletions

View File

@@ -20,20 +20,20 @@ import (
// on the "+Fav" anchor on the submission page. We fetch the submission to
// scrape the key, then follow the link. The whole exchange happens
// through the rate-limited transport.
func (c *Client) Fav(ctx context.Context, id SubmissionID) error {
return c.toggleFavorite(ctx, id, true)
func (c *Client) Fav(ctx context.Context, id SubmissionID, opts ...Option) error {
return c.toggleFavorite(ctx, id, true, opts)
}
// Unfav removes a submission from the logged-in user's favorites.
// Idempotent: if not favorited, no-op.
func (c *Client) Unfav(ctx context.Context, id SubmissionID) error {
return c.toggleFavorite(ctx, id, false)
func (c *Client) Unfav(ctx context.Context, id SubmissionID, opts ...Option) error {
return c.toggleFavorite(ctx, id, false, opts)
}
// toggleFavorite implements both Fav and Unfav: scrape the per-state link
// off the submission page and follow it. wantFav=true means "fav if not
// already faved"; wantFav=false means "unfav if currently faved".
func (c *Client) toggleFavorite(ctx context.Context, id SubmissionID, wantFav bool) error {
func (c *Client) toggleFavorite(ctx context.Context, id SubmissionID, wantFav bool, reqOpts []Option) error {
if id <= 0 {
return fmt.Errorf("fa: toggleFavorite: id must be > 0")
}
@@ -54,30 +54,30 @@ func (c *Client) toggleFavorite(ctx context.Context, id SubmissionID, wantFav bo
return fmt.Errorf("%w: submission %d: no fav/unfav link on page (not logged in?)", ErrUnauthorized, id)
}
return nil
})
}, reqOpts...)
if err != nil {
return err
}
if alreadyInDesiredState {
return nil
}
return c.followAction(ctx, actionURL)
return c.followAction(ctx, actionURL, reqOpts...)
}
// Watch starts watching a user. Idempotent: if already watching, no-op.
// The key + endpoint live on a button on the user's profile (or any page
// that user is the "owner" of, like a journal). We scrape the profile.
func (c *Client) Watch(ctx context.Context, name string) error {
return c.toggleWatch(ctx, name, true)
func (c *Client) Watch(ctx context.Context, name string, opts ...Option) error {
return c.toggleWatch(ctx, name, true, opts)
}
// Unwatch stops watching a user. Idempotent.
func (c *Client) Unwatch(ctx context.Context, name string) error {
return c.toggleWatch(ctx, name, false)
func (c *Client) Unwatch(ctx context.Context, name string, opts ...Option) error {
return c.toggleWatch(ctx, name, false, opts)
}
// toggleWatch fetches the user page, picks watch or unwatch link by state.
func (c *Client) toggleWatch(ctx context.Context, name string, wantWatch bool) error {
func (c *Client) toggleWatch(ctx context.Context, name string, wantWatch bool, reqOpts []Option) error {
name = strings.TrimSpace(name)
if name == "" {
return fmt.Errorf("fa: toggleWatch: empty name")
@@ -99,14 +99,14 @@ func (c *Client) toggleWatch(ctx context.Context, name string, wantWatch bool) e
return fmt.Errorf("%w: user %q: no watch/unwatch link on page", ErrUnauthorized, name)
}
return nil
})
}, reqOpts...)
if err != nil {
return err
}
if alreadyInDesiredState {
return nil
}
return c.followAction(ctx, actionURL)
return c.followAction(ctx, actionURL, reqOpts...)
}
// findFavLinks scans a submission view page for the "+Fav" and "Fav"
@@ -155,10 +155,11 @@ func findWatchLinks(doc *goquery.Document, name string) (watchURL, unwatchURL st
// the transport+classifier; 5xx becomes HTTPError. We don't parse the
// response body FA's success states are too varied to verify reliably
// from HTML alone.
func (c *Client) followAction(ctx context.Context, actionURL string) error {
func (c *Client) followAction(ctx context.Context, actionURL string, opts ...Option) error {
if actionURL == "" {
return fmt.Errorf("fa: followAction: empty URL")
}
ctx = c.applyRequestOptions(ctx, opts)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, actionURL, nil)
if err != nil {
return err
@@ -192,7 +193,8 @@ func (c *Client) followAction(ctx context.Context, actionURL string) error {
//
// Returns the response body for callers that need to parse a confirmation
// (e.g., to extract a newly-posted comment's ID).
func (c *Client) postForm(ctx context.Context, rawURL string, form url.Values) ([]byte, error) {
func (c *Client) postForm(ctx context.Context, rawURL string, form url.Values, opts ...Option) ([]byte, error) {
ctx = c.applyRequestOptions(ctx, opts)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, rawURL, strings.NewReader(form.Encode()))
if err != nil {
return nil, err