krz/skunky-art
Alternative privacy frontend for DeviantArt.
clone: git clone https://gitbay.org/krz/skunky-art.git
sonarcloud-cleanup: app/api.go · raw
1package app
2
3import (
4 "encoding/json"
5 "math/rand"
6 "strconv"
7 "strings"
8
9 "github.com/krazywarez/devianter"
10)
11
12// API serves the JSON endpoints under /api, backed by the request its main
13// field points at.
14type API struct {
15 main *skunkyart
16}
17
18type info struct {
19 Version string `json:"version"`
20 Settings settingsParams `json:"settings"`
21}
22
23// Info responds with this instance's version and its proxy/NSFW settings.
24func (a API) Info() {
25 json, err := json.Marshal(info{
26 Version: a.main.Version,
27 Settings: settingsParams{
28 Nsfw: CFG.Nsfw,
29 Proxy: CFG.Proxy,
30 },
31 })
32 try(err)
33 _, _ = a.main.Writer.Write(json)
34}
35
36// Error responds with a JSON error body and the given HTTP status.
37func (a API) Error(description string, status int) {
38 a.main.Writer.WriteHeader(status)
39 var response strings.Builder
40 response.WriteString(`{"error":"`)
41 response.WriteString(description)
42 response.WriteString(`"}`)
43 wr(a.main.Writer, response.String())
44}
45
46func (a API) sendMedia(d *devianter.Deviation) {
47 mediaURL, name := devianter.UrlFromMedia(d.Media)
48 a.main.SetFilename(name)
49 if len(mediaURL) == 0 {
50 return
51 }
52
53 if CFG.Proxy {
54 mediaURL = mediaURL[21:]
55 dot := strings.Index(mediaURL, ".")
56 a.main.Writer.Header().Del("Content-Type")
57 a.main.DownloadAndSendMedia(mediaURL[:dot], mediaURL[dot+11:])
58 } else {
59 a.main.Writer.Header().Add("Location", mediaURL)
60 a.main.Writer.WriteHeader(302)
61 }
62}
63
64// Random responds with a random artwork's media, retrying a bounded number of
65// times when a search comes back empty or NSFW-filtered.
66//
67// TODO: add filters.
68func (a API) Random() {
69 // Bounded retries: the loop used to be unbounded, and the DeviantArt-error
70 // path never incremented attempt, so a single request could spin forever
71 // hammering the API (and get this instance's egress IP banned).
72 const maxAttempts = 3
73
74 // math/rand is deliberate: this picks a random artwork to show, which is not
75 // a security decision and does not need a cryptographic source.
76 for range maxAttempts {
77 // strconv.Itoa, not string(): string(65) is "A", not "65".
78 s, daErr, err := devianter.PerformSearch(strconv.Itoa(rand.Intn(999)), rand.Intn(30), 'a') //nolint:gosec // G404
79 try(err)
80 if daErr.RAW != nil {
81 continue
82 }
83
84 // rand.Intn panics on 0, so an empty result set must be skipped.
85 if len(s.Results) == 0 {
86 continue
87 }
88
89 deviation := &s.Results[rand.Intn(len(s.Results))] //nolint:gosec // G404: see above
90 if deviation.NSFW && !CFG.Nsfw {
91 continue
92 }
93
94 a.sendMedia(deviation)
95 return
96 }
97
98 a.Error("Sorry, butt NSFW on this are disabled, and the instance failed to find a random art without NSFW", 500)
99}