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}