Files
go-fa-api/notes_send.go
2026-05-26 20:21:55 +02:00

74 lines
2.2 KiB
Go

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
}