// Package urls is the single source of truth for every FA URL the SDK // constructs. Centralising route building here keeps fragile path // concatenation out of the public API and makes the site's URL scheme // trivial to swap (e.g., were FA to move endpoints). package urls import ( "fmt" "net/url" "strconv" "strings" ) // Host is the canonical FA host. Exported so callers can override for // proxies or local mirrors, but the production value is what every // builder below uses. const Host = "https://www.furaffinity.net" // Submission returns the canonical URL for viewing a submission. func Submission(id int64) string { return Host + "/view/" + strconv.FormatInt(id, 10) + "/" } // User returns the URL for a user's profile page. func User(name string) string { return Host + "/user/" + safeName(name) + "/" } // Gallery returns the URL for a user's main gallery page. func Gallery(name string, page int) string { return Host + "/gallery/" + safeName(name) + "/" + pageSegment(page) } // Scraps returns the URL for a user's scraps page. func Scraps(name string, page int) string { return Host + "/scraps/" + safeName(name) + "/" + pageSegment(page) } // Favorites returns the URL for a user's favorites page. FA uses a numeric // page parameter; the first page is 1. func Favorites(name string, page int) string { return Host + "/favorites/" + safeName(name) + "/" + pageSegment(page) } // Journal returns the URL for a single journal entry. func Journal(id int64) string { return Host + "/journal/" + strconv.FormatInt(id, 10) + "/" } // UserJournals returns the URL for a user's journals listing. func UserJournals(name string, page int) string { return Host + "/journals/" + safeName(name) + "/" + pageSegment(page) } // MsgSubmissions returns the URL for the new-submission inbox. Requires auth. func MsgSubmissions() string { return Host + "/msg/submissions/" } // InboxPageSize is FA's fixed page size for the submission inbox its // pagination links and "Next N" label are always built around 72 items. const InboxPageSize = 72 // MsgSubmissionsCursor returns the URL for the new-submission inbox page // that begins just below submission id FA's "new~{id}@72" cursor scheme. // Used to keep crawling when FA omits the rendered "Next 72" link. func MsgSubmissionsCursor(id int64) string { return Host + "/msg/submissions/new~" + strconv.FormatInt(id, 10) + "@" + strconv.Itoa(InboxPageSize) + "/" } // MsgOthers returns the URL for the watch/journal/comment/fav notifications // page. Requires auth. func MsgOthers() string { return Host + "/msg/others/" } // MsgPMs returns the URL for the private-message inbox. Requires auth. func MsgPMs() string { return Host + "/msg/pms/" } // ViewMessage returns the URL for a single private message (note) by ID. // Requires auth. func ViewMessage(id int64) string { return Host + "/viewmessage/" + strconv.FormatInt(id, 10) + "/" } // Search returns the URL for a keyword search. FA accepts the query string // directly; pagination is a query param rather than a path segment. func Search(query string, page int) string { u := Host + "/search/" q := url.Values{} q.Set("q", query) if page > 1 { q.Set("page", strconv.Itoa(page)) } if e := q.Encode(); e != "" { u += "?" + e } return u } // Browse returns the URL for /browse/ with optional page index. FA's // browse UI navigates via POST forms, but a GET with ?page=N is honoured // for the rendered results page, which is all this SDK needs. func Browse(page int) string { if page <= 1 { return Host + "/browse/" } return Host + "/browse/?page=" + strconv.Itoa(page) } // safeName lower-cases and URL-escapes a username segment. FA folds names // to lowercase for URL routing. func safeName(name string) string { return url.PathEscape(strings.ToLower(strings.TrimSpace(name))) } // pageSegment renders a 1-based page index as a trailing path segment. // Returns the empty string for page <= 1 so the first page URL matches the // canonical form FA emits in its own "next page" links. func pageSegment(page int) string { if page <= 1 { return "" } return strconv.Itoa(page) + "/" } // AbsoluteCDN turns an //d.furaffinity.net/... or /art/... reference into a // fully qualified https URL. Returns s unchanged if it already has a scheme. func AbsoluteCDN(s string) string { s = strings.TrimSpace(s) switch { case s == "": return "" case strings.HasPrefix(s, "http://"), strings.HasPrefix(s, "https://"): return s case strings.HasPrefix(s, "//"): return "https:" + s case strings.HasPrefix(s, "/"): return Host + s default: return fmt.Sprintf("%s/%s", Host, s) } }