aboutsummaryrefslogtreecommitdiff
path: root/util.go
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-07-15 17:26:22 -0500
committerGitHub <[email protected]>2026-07-15 17:26:22 -0500
commitb80180e77eebad8476aa66c8a5433764da672c62 (patch)
tree3078b122345c45a4bd8a5477ae21ad63fa56f13d /util.go
parentb70d3dd588c8f9d6c9ed0ff111af06b76fa1f6ba (diff)
downloaddevianter-b80180e77eebad8476aa66c8a5433764da672c62.tar.gz
devianter-b80180e77eebad8476aa66c8a5433764da672c62.tar.bz2
devianter-b80180e77eebad8476aa66c8a5433764da672c62.zip
docs: document the exported API, and fix five bugs found writing it (#2)v0.3.2
* docs: translate Russian comments and document exported API The package carried 35 Russian comments and no doc comments on its exported identifiers, so pkg.go.dev rendered a bare list of signatures. Translate the Russian to English and give every exported identifier a doc comment in godoc form. Add doc.go with a package overview covering the guest-session requirement, the Error-as-struct convention, and CloudFront blocking. The comments record what the signatures cannot: that UpdateCSRF must run first and that its token expires, that Error is a struct and so is never nil, that Thread is one comment rather than a thread, and that GetComments' page parameter costs one request per page. Comments only; the struct field realignment is gofmt's, from doc comments splitting alignment groups. Refs #1 * add CODEOWNERS * fix: stop the library killing its caller, and flatten text correctly Writing doc comments for the exported API surfaced behavior too alarming to document and leave alone. A library should never terminate the process that imports it. Crashes: - GetComments panicked on a comment with an empty body: the shape check read m[0] and m[len(m)-1] with no length check. - Group.Favourites and Group.Gallery indexed their variadic folderid without checking len, so omitting it — which the signature invites — panicked. Omitting it now means 0. - AEmedia and PerformSearch called log.Fatalln on a bad argument rune, terminating the caller. Both now return an error, which their signatures already allowed for. Draft.js flattening, in the same code both callers share: - The block loop assigned rather than accumulated, so every block but the last was dropped and a multi-paragraph body came back as its closing line alone. Blocks are block-level elements, so join them with newlines. - GetDeviation guarded on txt[1] == '{', the second character of a body that opens with {"blocks". It never fired, so descriptions were handed back as raw Draft.js JSON. It now shares flattenComment with GetComments rather than keeping its own copy. Both flattening fixes change output for existing callers. Refs #1
Diffstat (limited to 'util.go')
-rw-r--r--util.go63
1 files changed, 55 insertions, 8 deletions
diff --git a/util.go b/util.go
index 1f5c0e3..e24afaf 100644
--- a/util.go
+++ b/util.go
@@ -10,13 +10,22 @@ import (
"time"
)
-// функция для высера ошибки в stderr
+// try prints a non-nil error to stderr and swallows it. It is how this package
+// reports problems it does not propagate, such as a response that parsed only
+// partially.
func try(txt error) {
if txt != nil {
println(txt.Error())
}
}
+// ujson fetches a _puppy endpoint and unmarshals the response into output.
+// data is the path and query string after the endpoint root, without a leading
+// slash and without the csrf_token parameter, which puppy appends.
+//
+// A malformed response is reported through try and leaves output partially
+// populated, so a returned Error with an empty Reason does not by itself
+// guarantee that output is complete.
func ujson(data string, output any) Error {
input, err := puppy(data)
if err == nil {
@@ -25,12 +34,23 @@ func ujson(data string, output any) Error {
return APIError(err)
}
+// Error is a failed API call. It is a struct rather than an error interface, so
+// a zero value means success: test Reason for emptiness rather than comparing
+// against nil.
+//
+// For errors DeviantArt itself reports, Reason and Error hold its machine and
+// human readable descriptions. For anything else (a transport failure, or a
+// CloudFront block page) Reason is "request_failed" and Error carries the
+// underlying message.
type Error struct {
Reason string `json:"error"`
Error string `json:"errorDescription"`
RAW []byte `json:"-"`
}
+// APIError converts an error from the request layer into an [Error], decoding
+// DeviantArt's JSON error body when that is what it is. A nil input yields the
+// zero Error, which signals success.
func APIError(inputError error) (err Error) {
if inputError != nil {
err.RAW = []byte(inputError.Error())
@@ -46,7 +66,8 @@ func APIError(inputError error) (err Error) {
}
/* REQUEST SECTION */
-// структура для ответа сервера
+// reqrt is a completed HTTP response, flattened into the pieces this package
+// needs. On a transport failure Err is set and every other field is zero.
type reqrt struct {
Body string
Status int
@@ -56,17 +77,21 @@ type reqrt struct {
Err error
}
-// функция для совершения запроса
+// UserAgent overrides the browser User-Agent this package sends by default.
+// Setting it to something that identifies your client is polite, but DeviantArt
+// is more likely to serve a block page to a non-browser agent.
var UserAgent string
// Timeout bounds a single request end-to-end (dial, response, body read).
// Without it, a hung connection blocks its caller forever.
var Timeout = 30 * time.Second
+// request performs a GET and never panics or returns a partial response without
+// saying so: any failure is reported in reqrt.Err. An optional second argument
+// supplies the Cookie header.
func request(uri string, other ...string) reqrt {
var r reqrt
- // создаём новый запрос
// Transport is deliberately left nil so http.DefaultTransport applies: that
// keeps HTTPS_PROXY support and lets callers wrap it (e.g. to rate-limit).
cli := &http.Client{Timeout: Timeout}
@@ -77,9 +102,10 @@ func request(uri string, other ...string) reqrt {
return r
}
+ // Impersonate a browser by default: the endpoints are the web frontend's own,
+ // and an unfamiliar agent draws a block page.
req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0.0")
- // куки и UA-шник
if UserAgent != "" {
req.Header.Set("User-Agent", UserAgent)
}
@@ -107,7 +133,6 @@ func request(uri string, other ...string) reqrt {
r.Err = e
}
- // заполняем структуру
r.Body = string(body)
r.Cookies = resp.Cookies()
r.Headers = resp.Header
@@ -145,7 +170,11 @@ func describe(r reqrt) string {
}
/* PUPPY aka DeviantArt API */
-// получение или обновление токена
+// The guest session: a cookie from the _puppy endpoint and a CSRF token scraped
+// from the homepage. UpdateCSRF populates both; puppy sends them on every call.
+//
+// These are package-level and unsynchronised, so a program that calls UpdateCSRF
+// concurrently with any other function of this package races on them.
var cookie string
var token string
@@ -154,6 +183,18 @@ const (
xhrMarker = "window.__XHR_LOCAL__"
)
+// UpdateCSRF establishes the guest session that every other call in this package
+// depends on, and must be called before them. It fetches a session cookie (only
+// on the first call; later calls reuse it) and scrapes a fresh CSRF token from
+// the DeviantArt homepage.
+//
+// Tokens expire, so a long-running program should call this again when requests
+// begin to fail. It is not safe to call concurrently with other functions of
+// this package.
+//
+// An error means the session was not established: the homepage was blocked,
+// served a challenge, or changed its markup such that the token is no longer
+// where this package looks for it.
func UpdateCSRF() error {
if cookie == "" {
req := request("https://www.deviantart.com/_puppy")
@@ -187,6 +228,13 @@ func UpdateCSRF() error {
return nil
}
+// puppy calls a _puppy endpoint with the guest session applied and returns the
+// raw JSON body. data is a path and query string; the CSRF token and API version
+// are appended to it, so it must already end in a parameter (callers conclude
+// theirs with a trailing "&" or a final value).
+//
+// It returns an error for a transport failure, a non-200 status, or a 200 whose
+// body is not JSON, which is how a CDN block page arrives.
func puppy(data string) (string, error) {
var url strings.Builder
url.WriteString("https://www.deviantart.com/_puppy/")
@@ -200,7 +248,6 @@ func puppy(data string) (string, error) {
return "", body.Err
}
- // если код ответа не 200, возвращается ошибка
if body.Status != 200 {
return "", errors.New(describe(body))
}