krz/skunky-art

Alternative privacy frontend for DeviantArt.

clone: git clone https://gitbay.org/krz/skunky-art.git

v1.3.6: app/api.go · raw

 1package app
 2
 3import (
 4	"encoding/json"
 5	"math/rand"
 6	"strconv"
 7	"strings"
 8
 9	"github.com/zerolabsco/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}