package fa import ( "context" "fmt" "net/url" "strings" "github.com/PuerkitoBio/goquery" "git.anthrove.art/public/go-fa-api/internal/urls" ) // SendNote sends a new private message (note) to a user. Requires login. // // FA's note form posts to /msg/send/ with a per-form CSRF key that must // be scraped from the notes inbox (or any page that renders the send // form). This method does the scrape + post in one call. // // Returns nil on success. ErrUnauthorized when not logged in. // *SystemMessageError when FA rejects the send (recipient blocked, rate // limited, etc.). func (c *Client) SendNote(ctx context.Context, to string, subject string, body string, opts ...Option) error { to = strings.TrimSpace(to) if to == "" { return fmt.Errorf("fa: SendNote: empty recipient") } if subject == "" { return fmt.Errorf("fa: SendNote: empty subject") } if body == "" { return fmt.Errorf("fa: SendNote: empty body") } // Scrape the form key from /msg/pms/. The notes inbox renders the same // CSRF key in any note-related form on the page; we don't actually need // to be replying to anything to harvest it. var key string err := c.fetch(ctx, urls.MsgPMs(), func(doc *goquery.Document) error { key = findNoteKey(doc) if key == "" { return fmt.Errorf("%w: SendNote: could not locate form key on /msg/pms/", ErrUnauthorized) } return nil }, opts...) if err != nil { return err } v := url.Values{} v.Set("key", key) v.Set("to", to) v.Set("subject", subject) v.Set("message", body) v.Set("send", "Send Note") _, err = c.postForm(ctx, urls.Host+"/msg/send/", v, opts...) return err } // findNoteKey looks for a hidden input named "key" inside any form that // targets the notes subsystem (/msg/send/ or /msg/pms/). FA reuses the // same key value across these forms within a session. func findNoteKey(doc *goquery.Document) string { var key string doc.Find("form[action='/msg/send/'] input[name='key'], form[action='/msg/pms/'] input[name='key'], form[action^='/msg/'] input[name='key']").EachWithBreak(func(_ int, sel *goquery.Selection) bool { if v := strings.TrimSpace(trimAttr(sel, "value")); v != "" { key = v return false } return true }) return key }