app/api.go

106 lines · 2775 bytes · executable

  1package app
  2
  3import (
  4	"encoding/json"
  5	"math/rand"
  6	"strings"
  7
  8	"github.com/krazywarez/devianter"
  9)
 10
 11// API serves the JSON endpoints under /api, backed by the request its main
 12// field points at.
 13type API struct {
 14	main *skunkyart
 15}
 16
 17type info struct {
 18	Version  string         `json:"version"`
 19	Settings settingsParams `json:"settings"`
 20}
 21
 22// Info responds with this instance's version and its proxy/NSFW/hide-ai settings.
 23func (a API) Info() {
 24	json, err := json.Marshal(info{
 25		Version: a.main.Version,
 26		Settings: settingsParams{
 27			Nsfw:   CFG.Nsfw,
 28			Proxy:  CFG.Proxy,
 29			HideAI: CFG.HideAI,
 30			Theme:  CFG.Theme,
 31		},
 32	})
 33	try(err)
 34	_, _ = a.main.Writer.Write(json)
 35}
 36
 37// Error responds with a JSON error body and the given HTTP status.
 38func (a API) Error(description string, status int) {
 39	a.main.Writer.Header().Del("Cache-Control")
 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		a.main.Writer.Header().Add("Location", mediaURL)
 57		a.main.Writer.WriteHeader(302)
 58		return
 59	}
 60
 61	subdomain, path, token, ok := wixmpMedia(mediaURL)
 62	if !ok {
 63		a.Error("bad media url", 502)
 64		return
 65	}
 66	a.main.Writer.Header().Del("Content-Type")
 67	a.main.downloadAndSendMedia(subdomain, path, token)
 68}
 69
 70// fetchDailyDeviations is devianter.GetDailyDeviations behind a variable so
 71// tests can script it.
 72var fetchDailyDeviations = devianter.GetDailyDeviations
 73
 74// Random responds with a random artwork's media, picked from the current daily
 75// deviations. That page is one upstream call the API cache answers for its
 76// TTL, where the previous random searches were up to three uncacheable calls
 77// per hit and a cheap way for a bot to burn the instance's upstream budget.
 78func (a API) Random() {
 79	dd, daErr := fetchDailyDeviations(0)
 80	if daErr.RAW != nil {
 81		a.Error("deviantart returned an error", 502)
 82		return
 83	}
 84
 85	var pool []*devianter.Deviation
 86	for i := range dd.Deviations {
 87		if d := &dd.Deviations[i]; VisibleDeviation(d) {
 88			pool = append(pool, d)
 89		}
 90	}
 91	for s := range dd.Strips {
 92		for i := range dd.Strips[s].Deviations {
 93			if d := &dd.Strips[s].Deviations[i]; VisibleDeviation(d) {
 94				pool = append(pool, d)
 95			}
 96		}
 97	}
 98	if len(pool) == 0 {
 99		a.Error("no daily deviation this instance can show", 404)
100		return
101	}
102
103	// math/rand is deliberate: this picks a random artwork to show, which is not
104	// a security decision and does not need a cryptographic source.
105	a.sendMedia(pool[rand.Intn(len(pool))]) //nolint:gosec // G404
106}