krz/skunky-art

Alternative privacy frontend for DeviantArt.

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

v1.4.0: 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/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}