krz/skunky-art
Alternative privacy frontend for DeviantArt.
clone: git clone https://gitbay.org/krz/skunky-art.git
ddb8aa297962379fa7731032140221ba35f84b50
unsigned
author: Christian Cleberg <hello@cleberg.net> ยท 2026-08-07T23:09:39Z
SETUP.txt | 2 ++ app/config.go | 1 + app/parsers.go | 3 +++ app/parsers_test.go | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++++ config.example.json | 3 ++- 5 files changed, 66 insertions(+), 1 deletion(-) @@ -35,6 +35,8 @@ Time units: DeviantArt's CDN. Required by `cache`; when off, clients fetch images from wixmp directly. * `nsfw` โ Show mature content. +* `hide-ai` โ Omit AI-generated deviations (those flagged `[๐ค]`) from all + listings: search, daily deviations, galleries and favourites. # Setting up reverse proxy Pretty much business as usual, except for the [`X-Forwarded-Proto`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto) header setting. @@ -34,6 +34,7 @@ type config struct { Cache cacheConfig `json:"cache"` Proxy bool `json:"proxy"` Nsfw bool `json:"nsfw"` + HideAI bool `json:"hide-ai"` UserAgent string `json:"user-agent"` DownloadProxy string `json:"download-proxy"` StaticPath string `json:"static-path"` @@ -92,6 +92,9 @@ func (s skunkyart) DeviationList(devs []devianter.Deviation, allowAtom bool, con for i, l := 0, len(devs); i < l; i++ { data := &devs[i] + if data.AI && CFG.HideAI { + continue + } if preview, fullview := ParseMedia(s.Host, data.Media, 320), ParseMedia(s.Host, data.Media); !data.NSFW || CFG.Nsfw { if allowAtom && s.Atom { s.Writer.Header().Add("Content-Type", "application/atom+xml") new file mode 100644 @@ -0,0 +1,58 @@ +package app + +import ( + "strings" + "testing" + + "github.com/krazywarez/devianter" +) + +// aiListDevs returns one human-made and one AI-flagged deviation, each with a +// distinct title so the rendered output can be searched for either one. +func aiListDevs() []devianter.Deviation { + human := devianter.Deviation{Title: "HumanArt"} + human.Author.Username = "alice" + + robot := devianter.Deviation{Title: "RobotArt", AI: true} + robot.Author.Username = "bob" + + return []devianter.Deviation{human, robot} +} + +// TestDeviationListHidesAIWhenConfigured is the regression test for the hide-ai +// option: with it on, deviations flagged data.AI must not appear in a listing at +// all, while human-made ones are untouched. +func TestDeviationListHidesAIWhenConfigured(t *testing.T) { + nsfw, hide := CFG.Nsfw, CFG.HideAI + CFG.Nsfw, CFG.HideAI = true, true + defer func() { CFG.Nsfw, CFG.HideAI = nsfw, hide }() + + out := skunkyart{Host: "http://localhost"}.DeviationList(aiListDevs(), false) + + if !strings.Contains(out, "HumanArt") { + t.Error("human deviation was dropped, want it kept") + } + if strings.Contains(out, "RobotArt") { + t.Error("AI deviation rendered while hide-ai is on, want it omitted") + } + if strings.Contains(out, "๐ค") { + t.Error("AI marker present while hide-ai is on") + } +} + +// TestDeviationListShowsAIByDefault pins down that the filter is opt-in: with +// hide-ai off the AI deviation is still listed and still carries its marker. +func TestDeviationListShowsAIByDefault(t *testing.T) { + nsfw, hide := CFG.Nsfw, CFG.HideAI + CFG.Nsfw, CFG.HideAI = true, false + defer func() { CFG.Nsfw, CFG.HideAI = nsfw, hide }() + + out := skunkyart{Host: "http://localhost"}.DeviationList(aiListDevs(), false) + + if !strings.Contains(out, "RobotArt") { + t.Error("AI deviation dropped while hide-ai is off, want it kept") + } + if !strings.Contains(out, "๐ค") { + t.Error("AI marker missing while hide-ai is off") + } +} @@ -13,5 +13,6 @@ "download-proxy": "", "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36", "proxy": true, - "nsfw": false + "nsfw": false, + "hide-ai": false }