aboutsummaryrefslogtreecommitdiff
path: root/misc.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 /misc.go
parentb70d3dd588c8f9d6c9ed0ff111af06b76fa1f6ba (diff)
downloaddevianter-0.3.2.tar.gz
devianter-0.3.2.tar.bz2
devianter-0.3.2.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 'misc.go')
-rw-r--r--misc.go73
1 files changed, 55 insertions, 18 deletions
diff --git a/misc.go b/misc.go
index 6b34050..9778615 100644
--- a/misc.go
+++ b/misc.go
@@ -2,7 +2,6 @@ package devianter
import (
"errors"
- "log"
"math"
"net/url"
"strconv"
@@ -10,20 +9,28 @@ import (
)
/* AVATARS AND EMOJIS */
+// AEmedia fetches a user's avatar or a site emoji by name. t selects which:
+// 'a' for an avatar, 'e' for an emoji.
+//
+// It returns the image data itself, not a URL. DeviantArt does not say which
+// format a given name is stored in, so this tries .jpg, .png, and .gif in turn
+// and returns the first that exists — up to three requests per call, and three
+// for a name that does not exist.
+//
+// Passing any other t returns an error without making a request.
func AEmedia(name string, t rune) (string, error) {
if len(name) < 2 {
return "", errors.New("name must be specified")
}
- // список всех возможных расширений
var extensions = [3]string{
".jpg",
".png",
".gif",
}
- // надо
name = strings.ToLower(name)
- // построение ссылок. билдер потому что он быстрее обычного сложения строк.
+ // Avatars and emoji are sharded into directories by the leading characters of
+ // the name; avatars additionally normalise dashes to underscores first.
var b strings.Builder
switch t {
case 'a':
@@ -38,11 +45,11 @@ func AEmedia(name string, t rune) (string, error) {
b.WriteString(name[:1])
b.WriteString("/")
default:
- log.Fatalln("Invalid type.\n- 'a' -- avatar;\n- 'e' -- emoji.")
+ return "", errors.New("invalid type: want 'a' (avatar) or 'e' (emoji)")
}
b.WriteString(name)
- // проверка ссылки на доступность
+ // Probe each extension; the first 200 is the real format.
for x := 0; x < len(extensions); x++ {
req := request(b.String() + extensions[x])
if req.Status == 200 {
@@ -54,6 +61,9 @@ func AEmedia(name string, t rune) (string, error) {
}
/* DAILY DEVIATIONS */
+// DailyDeviations is the staff-curated front page selection. The picks are
+// grouped into Strips, each a titled row as the site presents it; Deviations is
+// the ungrouped listing.
type DailyDeviations struct {
HasMore bool
Strips []struct {
@@ -64,30 +74,54 @@ type DailyDeviations struct {
Deviations []Deviation
}
+// GetDailyDeviations retrieves a page of the daily deviation selection. Pages
+// are zero-based; check the returned HasMore before asking for the next.
func GetDailyDeviations(page int) (dd DailyDeviations, err Error) {
err = ujson("dabrowse/networkbar/rfy/deviations?page="+strconv.Itoa(page), &dd)
return
}
/* SEARCH */
+// Search is a page of search results. Read the matches from Results, which
+// [PerformSearch] populates whichever field the endpoint used.
+//
+// Total is DeviantArt's own estimate and is approximate. Pages is derived from
+// it and capped at 417, the depth a guest session can reach before the API stops
+// paginating.
type Search struct {
- Total int `json:"estTotal"`
- Pages int // only for 'a' and 'g' scope.
- HasMore bool
- Results []Deviation `json:"deviations"`
+ Total int `json:"estTotal"`
+ Pages int // only for 'a' and 'g' scope.
+ HasMore bool
+ Results []Deviation `json:"deviations"`
+ // ResultsGalleryTemp receives the results of gallery and collection searches,
+ // which return them under a different key. PerformSearch copies it into
+ // Results; callers should not need this field.
ResultsGalleryTemp []Deviation `json:"results"`
}
+// PerformSearch searches DeviantArt. scope selects what is being searched:
+//
+// 'a' — everything, by title and description
+// 't' — by tag
+// 'g' — within one user's or group's gallery
+// 'f' — within one user's or group's collections (favourites)
+//
+// Scopes 'g' and 'f' search a particular account, so they require the username
+// as the final argument and return an error without it. The other two ignore it.
+//
+// Pages are zero-based. A guest session cannot page beyond roughly 417 pages
+// deep regardless of how many results Total claims.
+//
+// Passing any other scope returns an error without making a request.
func PerformSearch(query string, page int, scope rune, user ...string) (ss Search, daError Error, err error) {
var buildurl strings.Builder
- // о5 построение ссылок.
switch scope {
- case 'a': // поиск артов по названию
+ case 'a':
buildurl.WriteString("dabrowse/search/all?q=")
- case 't': // поиск артов по тегам
+ case 't':
buildurl.WriteString("dabrowse/networkbar/tag/deviations?tag=")
- case 'g', 'f': // поиск артов пользователя или группы
+ case 'g', 'f':
if user == nil {
err = errors.New("missing username (last argument)")
return
@@ -103,13 +137,15 @@ func PerformSearch(query string, page int, scope rune, user ...string) (ss Searc
}
buildurl.WriteString("&order=most-recent&init=true&limit=50&q=")
default:
- log.Fatalln("Invalid type.\n- 'a' -- all;\n- 't' -- tag;\n- 'g' - gallery\n- 'f' - folders.")
+ err = errors.New("invalid scope: want 'a' (all), 't' (tag), 'g' (gallery) or 'f' (favourites)")
+ return
}
buildurl.WriteString(url.QueryEscape(query))
- if scope != 'g' { // если область поиска не равна поиску по группам, то активируется этот код
+ // Gallery search paginates by item offset rather than page number.
+ if scope != 'g' {
buildurl.WriteString("&page=")
- } else { // иначе вместо страницы будет оффсет и страница умножится на 50
+ } else {
buildurl.WriteString("&offset=")
page = 50 * page
}
@@ -121,7 +157,8 @@ func PerformSearch(query string, page int, scope rune, user ...string) (ss Searc
ss.Results = ss.ResultsGalleryTemp
}
- // расчёт, сколько всего страниц по запросу. без токена, 417 страниц - максимум
+ // Derive the page count from the result estimate, clamped to the 417 pages a
+ // guest session can actually reach.
totalfloat := int(math.Round(float64(ss.Total / 25)))
for x := 0; x < totalfloat; x++ {
if x <= 417 {