krz/skunky-art
Alternative privacy frontend for DeviantArt.
clone: git clone https://gitbay.org/krz/skunky-art.git
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/hide-ai 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 HideAI: CFG.HideAI,
31 Theme: CFG.Theme,
32 },
33 })
34 try(err)
35 _, _ = a.main.Writer.Write(json)
36}
37
38// Error responds with a JSON error body and the given HTTP status.
39func (a API) Error(description string, status int) {
40 a.main.Writer.WriteHeader(status)
41 var response strings.Builder
42 response.WriteString(`{"error":"`)
43 response.WriteString(description)
44 response.WriteString(`"}`)
45 wr(a.main.Writer, response.String())
46}
47
48func (a API) sendMedia(d *devianter.Deviation) {
49 mediaURL, name := devianter.UrlFromMedia(d.Media)
50 a.main.SetFilename(name)
51 if len(mediaURL) == 0 {
52 return
53 }
54
55 if CFG.Proxy {
56 mediaURL = mediaURL[21:]
57 dot := strings.Index(mediaURL, ".")
58 a.main.Writer.Header().Del("Content-Type")
59 a.main.DownloadAndSendMedia(mediaURL[:dot], mediaURL[dot+11:])
60 } else {
61 a.main.Writer.Header().Add("Location", mediaURL)
62 a.main.Writer.WriteHeader(302)
63 }
64}
65
66// Random responds with a random artwork's media, retrying a bounded number of
67// times when a search comes back empty or NSFW-filtered.
68//
69// TODO: add filters.
70func (a API) Random() {
71 // Bounded retries: the loop used to be unbounded, and the DeviantArt-error
72 // path never incremented attempt, so a single request could spin forever
73 // hammering the API (and get this instance's egress IP banned).
74 const maxAttempts = 3
75
76 // math/rand is deliberate: this picks a random artwork to show, which is not
77 // a security decision and does not need a cryptographic source.
78 for range maxAttempts {
79 // strconv.Itoa, not string(): string(65) is "A", not "65".
80 s, daErr, err := devianter.PerformSearch(strconv.Itoa(rand.Intn(999)), rand.Intn(30), 'a') //nolint:gosec // G404
81 try(err)
82 if daErr.RAW != nil {
83 continue
84 }
85
86 // rand.Intn panics on 0, so an empty result set must be skipped.
87 if len(s.Results) == 0 {
88 continue
89 }
90
91 deviation := &s.Results[rand.Intn(len(s.Results))] //nolint:gosec // G404: see above
92 if deviation.NSFW && !CFG.Nsfw {
93 continue
94 }
95
96 a.sendMedia(deviation)
97 return
98 }
99
100 a.Error("Sorry, butt NSFW on this are disabled, and the instance failed to find a random art without NSFW", 500)
101}