app/api.go
113 lines · 3121 bytes · executable
1package app
2
3import (
4 "encoding/json"
5 "math/rand"
6 "net/url"
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.Header().Del("Cache-Control")
41 a.main.Writer.WriteHeader(status)
42 var response strings.Builder
43 response.WriteString(`{"error":"`)
44 response.WriteString(description)
45 response.WriteString(`"}`)
46 wr(a.main.Writer, response.String())
47}
48
49func (a API) sendMedia(d *devianter.Deviation) {
50 mediaURL, name := devianter.UrlFromMedia(d.Media)
51 a.main.SetFilename(name)
52 if len(mediaURL) == 0 {
53 return
54 }
55
56 if !CFG.Proxy {
57 a.main.Writer.Header().Add("Location", mediaURL)
58 a.main.Writer.WriteHeader(302)
59 return
60 }
61
62 // Parsed, not sliced: the signing token has to reach wixmp as a query
63 // parameter. Passing the raw tail as the path put "?token=..." inside
64 // the path, which wixmp answers with 401.
65 u, err := url.Parse(mediaURL)
66 if err != nil {
67 a.Error("bad media url", 502)
68 return
69 }
70 subdomain := strings.TrimSuffix(strings.TrimPrefix(u.Host, "images-wixmp-"), ".wixmp.com")
71 a.main.Writer.Header().Del("Content-Type")
72 a.main.downloadAndSendMedia(subdomain, strings.TrimPrefix(u.Path, "/"), u.Query().Get("token"))
73}
74
75// fetchDailyDeviations is devianter.GetDailyDeviations behind a variable so
76// tests can script it.
77var fetchDailyDeviations = devianter.GetDailyDeviations
78
79// Random responds with a random artwork's media, picked from the current daily
80// deviations. That page is one upstream call the API cache answers for its
81// TTL, where the previous random searches were up to three uncacheable calls
82// per hit and a cheap way for a bot to burn the instance's upstream budget.
83//
84// TODO: add filters.
85func (a API) Random() {
86 dd, daErr := fetchDailyDeviations(0)
87 if daErr.RAW != nil {
88 a.Error("deviantart returned an error", 502)
89 return
90 }
91
92 var pool []*devianter.Deviation
93 for i := range dd.Deviations {
94 if d := &dd.Deviations[i]; VisibleDeviation(d) {
95 pool = append(pool, d)
96 }
97 }
98 for s := range dd.Strips {
99 for i := range dd.Strips[s].Deviations {
100 if d := &dd.Strips[s].Deviations[i]; VisibleDeviation(d) {
101 pool = append(pool, d)
102 }
103 }
104 }
105 if len(pool) == 0 {
106 a.Error("no daily deviation this instance can show", 404)
107 return
108 }
109
110 // math/rand is deliberate: this picks a random artwork to show, which is not
111 // a security decision and does not need a cryptographic source.
112 a.sendMedia(pool[rand.Intn(len(pool))]) //nolint:gosec // G404
113}