krz/skunky-art

Alternative privacy frontend for DeviantArt.

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

7f1ab1b73d1e7a0d8a730354bbc3136c5584fea3

unsigned

author: lost+skunk <skunky@ebloid.ru> · 2024-07-05T21:46:25Z

Реализация конфига
 app/config.go       |  69 ++++++++++----
 app/router.go       |  30 ++++--
 app/util.go         | 257 +++++++++++++++++++++++++++++++++++++---------------
 app/wraper.go       |  91 ++++++++++++-------
 atom.xml            |  43 ---------
 config.example.json |   7 +-
 css/skunky.css      |   5 +-
 html/about.htm      |  12 ++-
 html/deviantion.htm |  38 ++++----
 html/gruser.htm     |   8 +-
 html/index.htm      |   6 +-
 html/list.htm       |   8 +-
 html/search.htm     |   6 +-
 main.go             |   1 +
 14 files changed, 366 insertions(+), 215 deletions(-)

diff --git a/app/config.go b/app/config.go
index 9a5aef1..1df6aaf 100644
--- a/app/config.go
+++ b/app/config.go
@@ -1,26 +1,31 @@
 package app
 
 import (
+	"encoding/json"
+	"errors"
 	"os"
 )
 
 type cache_config struct {
-	Enabled            bool
-	Path               string
-	Max_size, Lifetime int64
+	Enabled  bool
+	Path     string
+	MaxSize  int64 `json:"max-size"`
+	Lifetime int64
 }
 
 type config struct {
-	cfg              string
-	Listen, Base_uri string
-	Cache            cache_config
-	Proxy, Nsfw      bool
+	cfg         string
+	Listen      string
+	BasePath    string `json:"base-path"`
+	Cache       cache_config
+	Proxy, Nsfw bool
+	WixmpProxy  string `json:"wixmp-proxy"`
 }
 
 var CFG = config{
 	cfg:      "config.json",
 	Listen:   "127.0.0.1:3003",
-	Base_uri: "/",
+	BasePath: "/",
 	Cache: cache_config{
 		Enabled: true,
 		Path:    "cache",
@@ -29,20 +34,46 @@ var CFG = config{
 	Nsfw:  true,
 }
 
-func execcfg() {
+func ExecuteConfig() {
+	try := func(err error, exitcode int) {
+		if err != nil {
+			println(err.Error())
+			os.Exit(exitcode)
+		}
+	}
+
 	a := os.Args
-	for num, val := range a {
-		switch val {
-		case "-conf":
-			CFG.cfg = a[num]
-		case "-help":
-			println(`SkunkyArt v 1.3 [refactoring]
+	if l := len(a); l > 1 {
+		switch a[1] {
+		case "-c", "--config":
+			if l >= 3 {
+				CFG.cfg = a[2]
+			} else {
+				try(errors.New("Not enought arguments"), 1)
+			}
+		case "-h", "--help":
+			try(errors.New(`SkunkyArt v1.3 [refactoring]
 Usage:
-	- -conf - path to config
-	- -help this message
+	- [-c|--config] - path to config
+	- [-h|--help]	- returns this message
 Example:
-	./skunkyart -conf config.json
-Copyright lost+skunk, X11. https://git.macaw.me/skunky/skunkyart/src/tag/v1.3`)
+	./skunkyart -c config.json
+Copyright lost+skunk, X11. https://git.macaw.me/skunky/skunkyart/src/tag/v1.3`), 0)
+		default:
+			try(errors.New("Unreconginzed argument: "+a[1]), 1)
+		}
+		if CFG.cfg != "" {
+			f, err := os.ReadFile(CFG.cfg)
+			try(err, 1)
+
+			try(json.Unmarshal(f, &CFG), 1)
+			if CFG.Cache.Enabled && !CFG.Proxy {
+				try(errors.New("Incompatible settings detected: cannot use caching media content without proxy"), 1)
+			}
+
+			if CFG.Cache.MaxSize != 0 || CFG.Cache.Lifetime != 0 {
+				go InitCacheSystem()
+			}
 		}
 	}
 }
diff --git a/app/router.go b/app/router.go
index d7f48b8..e833628 100644
--- a/app/router.go
+++ b/app/router.go
@@ -9,11 +9,14 @@ import (
 	"strings"
 )
 
-const addr string = "0.0.0.0:3003"
-
-// роутер
 func Router() {
 	parsepath := func(path string) map[int]string {
+		if l := len(CFG.BasePath); len(path) > l {
+			path = path[l-1:]
+		} else {
+			path = "/"
+		}
+
 		parsedpath := make(map[int]string)
 		for x := 0; true; x++ {
 			slash := strings.Index(path, "/") + 1
@@ -52,16 +55,20 @@ func Router() {
 		var skunky skunkyart
 		skunky.Args = r.URL.Query()
 		skunky.Writer = w
+		skunky.BasePath = CFG.BasePath
 
 		arg := skunky.Args.Get
-		skunky.Query = u.QueryEscape(arg("q"))
+		skunky.QueryRaw = arg("q")
+		skunky.Query = u.QueryEscape(skunky.QueryRaw)
+
 		if t := arg("type"); len(t) > 0 {
 			skunky.Type = rune(t[0])
 		}
 		p, _ := strconv.Atoi(arg("p"))
 		skunky.Page = p
+
 		if arg("atom") == "true" {
-			skunky.atom = true
+			skunky.Atom = true
 		}
 
 		// пути
@@ -69,7 +76,7 @@ func Router() {
 		default:
 			skunky.ReturnHTTPError(404)
 		case "":
-			open_n_send("html/index.htm")
+			skunky.ExecuteTemplate("html/index.htm", &CFG.BasePath)
 		case "post":
 			skunky.Deviation(path[2], path[3])
 		case "search":
@@ -80,9 +87,14 @@ func Router() {
 			skunky.GRUser()
 
 		case "media":
-			skunky.Emojitar(path[2])
+			switch path[2] {
+			case "file":
+				skunky.DownloadAndSendMedia(path[3], next(path, 4))
+			case "emojitar":
+				skunky.Emojitar(path[3])
+			}
 		case "about":
-			open_n_send("html/about.htm")
+			skunky.About()
 		case "gui":
 			w.Header().Add("content-type", "text/css")
 			open_n_send(next(path, 2))
@@ -90,7 +102,7 @@ func Router() {
 	}
 
 	http.HandleFunc("/", handle)
-	http.ListenAndServe(addr, nil)
+	http.ListenAndServe(CFG.Listen, nil)
 }
 
 func err(e error) {
diff --git a/app/util.go b/app/util.go
index 0d36fd8..9e1784d 100644
--- a/app/util.go
+++ b/app/util.go
@@ -1,11 +1,17 @@
 package app
 
 import (
+	"encoding/base64"
 	"encoding/json"
+	"io"
 	"net/http"
+	u "net/url"
+	"os"
 	"strconv"
 	"strings"
+	"syscall"
 	"text/template"
+	"time"
 
 	"git.macaw.me/skunky/devianter"
 )
@@ -19,11 +25,15 @@ func (s skunkyart) ExecuteTemplate(file string, data any) {
 	wr(s.Writer, buf.String())
 }
 
-func (s skunkyart) UrlBuilder(strs ...string) string {
+func UrlBuilder(strs ...string) string {
 	var str strings.Builder
-	for _, x := range strs {
+	l := len(strs)
+	str.WriteString(CFG.BasePath)
+	for n, x := range strs {
 		str.WriteString(x)
-		str.WriteString("/")
+		if n+1 < l && !(strs[n+1][0] == '?' || strs[n+1][0] == '&') && !(x[0] == '?' || x[0] == '&') {
+			str.WriteString("/")
+		}
 	}
 	return str.String()
 }
@@ -31,10 +41,10 @@ func (s skunkyart) UrlBuilder(strs ...string) string {
 func (s skunkyart) ReturnHTTPError(status int) {
 	s.Writer.WriteHeader(status)
 
-	// пострйока с помощью strings.Builder, потому что такой метод быстрее обычного сложения
 	var msg strings.Builder
-	msg.WriteString(`<html><link rel="stylesheet" href="/gui/css/skunky.css" />`)
-	msg.WriteString("<h1>")
+	msg.WriteString(`<html><link rel="stylesheet" href="`)
+	msg.WriteString(UrlBuilder("gui", "css", "skunky.css"))
+	msg.WriteString(`" /><h1>`)
 	msg.WriteString(strconv.Itoa(status))
 	msg.WriteString(" - ")
 	msg.WriteString(http.StatusText(status))
@@ -43,6 +53,15 @@ func (s skunkyart) ReturnHTTPError(status int) {
 	wr(s.Writer, msg.String())
 }
 
+func (s skunkyart) ConvertDeviantArtUrlToSkunkyArt(url string) (output string) {
+	if len(url) > 32 && url[27:32] != "stash" {
+		url = url[27:]
+		toart := strings.Index(url, "/art/")
+		output = UrlBuilder("post", url[:toart], url[toart+5:])
+	}
+	return
+}
+
 type text struct {
 	TXT  string
 	from int
@@ -200,80 +219,78 @@ func (s skunkyart) NavBase(c dlist) string {
 
 func (s skunkyart) DeviationList(devs []devianter.Deviation, content ...dlist) string {
 	var list strings.Builder
-	if !s.atom {
-		list.WriteString(`<div class="content">`)
-	} else {
-		list.WriteString(`<?xml version="1.0" encoding="UTF-8"?><feed xmlns:media="http://search.yahoo.com/mrss" xmlns="http://www.w3.org/2005/Atom">`)
+	if s.Atom && s.Page > 1 {
+		s.ReturnHTTPError(400)
+		return ""
+	} else if s.Atom {
+		list.WriteString(`<?xml version="1.0" encoding="UTF-8"?><feed xmlns:media="http://search.yahoo.com/mrss/" xmlns="http://www.w3.org/2005/Atom">`)
 		list.WriteString(`<title>SkunkyArt</title>`)
 		// list.WriteString(`<link rel="alternate" href="HOMEPAGE_URL"/><link href="FEED_URL" rel="self"/>`)
+	} else {
+		list.WriteString(`<div class="content">`)
 	}
 	for _, data := range devs {
-		url := devianter.UrlFromMedia(data.Media)
-		if s.atom {
-			id := strconv.Itoa(data.ID)
-			list.WriteString(`<entry><author><name>`)
-			list.WriteString(data.Author.Username)
-			list.WriteString(`</name></author><title>`)
-			list.WriteString(data.Title)
-			list.WriteString(`</title><link rel="alternate" type="text/html" href="`)
-			list.WriteString(CFG.Base_uri)
-			list.WriteString("/post/")
-			list.WriteString(data.Author.Username)
-			list.WriteString("/atom-")
-			list.WriteString(id)
-			list.WriteString(`"/><id>`)
-			list.WriteString(id)
-			list.WriteString(`</id><published>`)
-			list.WriteString(data.PublishedTime.UTC().Format("Mon, 02 Jan 2006 15:04:05 -0700"))
-			list.WriteString(`</published>`)
-			list.WriteString(`<media:group><media:title>`)
-			list.WriteString(data.Title)
-			list.WriteString(`</media:title><media:thumbinal url="`)
-			list.WriteString(url)
-			list.WriteString(`"/></media:group><content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><a href="`)
-			list.WriteString(data.Url)
-			list.WriteString(`"><img src="`)
-			list.WriteString(url)
-			list.WriteString(`"/></a><p>`)
-			list.WriteString(ParseDescription(data.TextContent))
-			list.WriteString(`</p></div></content></entry>`)
-		} else {
-			list.WriteString(`<div class="block">`)
-			if url != "" {
-				list.WriteString(`<a title="open/download" href="`)
+		if !(data.NSFW && !CFG.Nsfw) {
+			url := s.ParseMedia(data.Media)
+			if s.Atom {
+				id := strconv.Itoa(data.ID)
+				list.WriteString(`<entry><author><name>`)
+				list.WriteString(data.Author.Username)
+				list.WriteString(`</name></author><title>`)
+				list.WriteString(data.Title)
+				list.WriteString(`</title><link rel="alternate" type="text/html" href="`)
+				list.WriteString(UrlBuilder("post", data.Author.Username, "atom-"+id))
+				list.WriteString(`"/><id>`)
+				list.WriteString(id)
+				list.WriteString(`</id><published>`)
+				list.WriteString(data.PublishedTime.UTC().Format("Mon, 02 Jan 2006 15:04:05 -0700"))
+				list.WriteString(`</published>`)
+				list.WriteString(`<media:group><media:title>`)
+				list.WriteString(data.Title)
+				list.WriteString(`</media:title><media:thumbinal url="`)
 				list.WriteString(url)
+				list.WriteString(`"/></media:group><content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><a href="`)
+				list.WriteString(data.Url)
 				list.WriteString(`"><img src="`)
 				list.WriteString(url)
-				list.WriteString(`" width="15%"></a>`)
+				list.WriteString(`"/></a><p>`)
+				list.WriteString(ParseDescription(data.TextContent))
+				list.WriteString(`</p></div></content></entry>`)
 			} else {
-				list.WriteString(`<h1>[ TEXT ]</h1>`)
-			}
-			list.WriteString(`<br><a href="`)
-			list.WriteString("/post/")
-			list.WriteString(data.Author.Username)
-			list.WriteString("/")
-			list.WriteString(data.Url[27:][strings.Index(data.Url[27:], "/art/")+5:])
-			list.WriteString(`">`)
-			list.WriteString(data.Author.Username)
-			list.WriteString(" - ")
-			list.WriteString(data.Title)
+				list.WriteString(`<div class="block">`)
+				if url != "" {
+					list.WriteString(`<a title="open/download" href="`)
+					list.WriteString(url)
+					list.WriteString(`"><img src="`)
+					list.WriteString(url)
+					list.WriteString(`" width="15%"></a>`)
+				} else {
+					list.WriteString(`<h1>[ TEXT ]</h1>`)
+				}
+				list.WriteString(`<br><a href="`)
+				list.WriteString(s.ConvertDeviantArtUrlToSkunkyArt(data.Url))
+				list.WriteString(`">`)
+				list.WriteString(data.Author.Username)
+				list.WriteString(" - ")
+				list.WriteString(data.Title)
+
+				// шильдики нсфв, аи и ежедневного поста
+				if data.NSFW {
+					list.WriteString(` [<span class="nsfw">NSFW</span>]`)
+				}
+				if data.AI {
+					list.WriteString(" [🤖]")
+				}
+				if data.DD {
+					list.WriteString(` [<span class="dd">DD</span>]`)
+				}
 
-			// шильдики нсфв, аи и ежедневного поста
-			if data.NSFW {
-				list.WriteString(` [<span class="nsfw">NSFW</span>]`)
-			}
-			if data.AI {
-				list.WriteString(" [🤖]")
-			}
-			if data.DD {
-				list.WriteString(` [<span class="dd">DD</span>]`)
+				list.WriteString("</a></div>")
 			}
-
-			list.WriteString("</a></div>")
 		}
 	}
 
-	if s.atom {
+	if s.Atom {
 		list.WriteString("</feed>")
 		s.Writer.Write([]byte(list.String()))
 		return ""
@@ -301,10 +318,10 @@ func (s skunkyart) ParseComments(c devianter.Comments) string {
 		}
 		cmmts.WriteString(`"><p id="`)
 		cmmts.WriteString(strconv.Itoa(x.ID))
-		cmmts.WriteString(`"><img src="/media/`)
-		cmmts.WriteString(x.User.Username)
-		cmmts.WriteString(`?type=a" width="30px" height="30px"><a href="/group_user?type=about&q=`)
-		cmmts.WriteString(x.User.Username)
+		cmmts.WriteString(`"><img src="`)
+		cmmts.WriteString(UrlBuilder("media", "emojitar", x.User.Username, "?type=a"))
+		cmmts.WriteString(`" width="30px" height="30px"><a href="`)
+		cmmts.WriteString(UrlBuilder("group_user", "?q=", x.User.Username, "&type=a"))
 		cmmts.WriteString(`"><b`)
 		cmmts.WriteString(` class="`)
 		if x.User.Banned {
@@ -346,3 +363,101 @@ func (s skunkyart) ParseComments(c devianter.Comments) string {
 	cmmts.WriteString("</details>")
 	return cmmts.String()
 }
+
+func (s skunkyart) ParseMedia(media devianter.Media) string {
+	url := devianter.UrlFromMedia(media)
+	if len(url) != 0 {
+		url = url[21:]
+		dot := strings.Index(url, ".")
+
+		return UrlBuilder("media", "file", url[:dot], "/", url[dot+10:])
+	}
+	return ""
+}
+
+func (s skunkyart) DownloadAndSendMedia(subdomain, path string) {
+	var url strings.Builder
+	url.WriteString("https://images-wixmp-")
+	url.WriteString(subdomain)
+	url.WriteString(".wixmp.com/")
+	url.WriteString(path)
+	url.WriteString("?token=")
+	url.WriteString(s.Args.Get("token"))
+
+	download := func() (body []byte, status int, headers http.Header) {
+		cli := &http.Client{}
+		if CFG.WixmpProxy != "" {
+			u, e := u.Parse(CFG.WixmpProxy)
+			err(e)
+			cli.Transport = &http.Transport{Proxy: http.ProxyURL(u)}
+		}
+
+		req, e := http.NewRequest("GET", url.String(), nil)
+		err(e)
+		req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0.0")
+
+		resp, e := cli.Do(req)
+		err(e)
+		defer resp.Body.Close()
+
+		b, e := io.ReadAll(resp.Body)
+		err(e)
+		return b, resp.StatusCode, resp.Header
+	}
+
+	if CFG.Cache.Enabled {
+		os.Mkdir(CFG.Cache.Path, 0700)
+		fname := CFG.Cache.Path + "/" + base64.StdEncoding.EncodeToString([]byte(subdomain+path))
+		file, e := os.Open(fname)
+
+		if e != nil {
+			b, status, headers := download()
+			if status == 200 && headers["Content-Type"][0][:5] == "image" {
+				err(os.WriteFile(fname, b, 0700))
+				s.Writer.Write(b)
+			}
+		} else {
+			file, e := io.ReadAll(file)
+			err(e)
+			s.Writer.Write(file)
+		}
+	} else if CFG.Proxy {
+		b, _, _ := download()
+		s.Writer.Write(b)
+	} else {
+		s.Writer.Write([]byte("Sorry, butt proxy on this instance disabled."))
+	}
+}
+
+func InitCacheSystem() {
+	c := &CFG.Cache
+	for {
+		dir, e := os.Open(c.Path)
+		err(e)
+		stat, e := dir.Stat()
+		err(e)
+
+		dirnames, e := dir.Readdirnames(-1)
+		err(e)
+		for _, a := range dirnames {
+			a = c.Path + "/" + a
+			if c.Lifetime != 0 {
+				now := time.Now().UnixMilli()
+
+				f, _ := os.Stat(a)
+				stat := f.Sys().(*syscall.Stat_t)
+				time := time.Unix(stat.Ctim.Unix()).UnixMilli()
+
+				if time+c.Lifetime <= now {
+					os.RemoveAll(a)
+				}
+			}
+			if c.MaxSize != 0 && stat.Size() > c.MaxSize {
+				os.RemoveAll(a)
+			}
+		}
+
+		dir.Close()
+		time.Sleep(time.Second)
+	}
+}
diff --git a/app/wraper.go b/app/wraper.go
index 757602c..f42a9af 100644
--- a/app/wraper.go
+++ b/app/wraper.go
@@ -15,13 +15,27 @@ import (
 var wr = io.WriteString
 
 type skunkyart struct {
-	Writer    http.ResponseWriter
-	Args      url.Values
-	Type      rune
-	Query     string
-	Page      int
-	atom      bool
-	Templates struct {
+	Writer          http.ResponseWriter
+	Args            url.Values
+	BasePath        string
+	Type            rune
+	Query, QueryRaw string
+	Page            int
+	Atom            bool
+	Templates       struct {
+		About struct {
+			Proxy bool
+			Nsfw  bool
+		}
+
+		SomeList  string
+		Deviation struct {
+			Post       devianter.Post
+			StringTime string
+			Tags       string
+			Comments   string
+		}
+
 		GroupUser struct {
 			GR           devianter.GRuser
 			Admins       string
@@ -51,6 +65,19 @@ type skunkyart struct {
 	}
 }
 
+// var Templates struct {
+//	Index string
+//	About string
+//
+//	GRuser string
+//	Deviation string
+//	List string
+//	Search string
+// }
+
+// //go:embed ../html/*
+// var Templates embed.FS
+
 func (s skunkyart) GRUser() {
 	if len(s.Query) < 1 {
 		s.ReturnHTTPError(400)
@@ -65,7 +92,7 @@ func (s skunkyart) GRUser() {
 	switch s.Type {
 	case 'a':
 		g := group.GR
-		s.atom = false
+		s.Atom = false
 		for _, x := range g.Gruser.Page.Modules {
 			switch x.Name {
 			case "about", "group_about":
@@ -110,17 +137,15 @@ func (s skunkyart) GRUser() {
 
 			case "cover_deviation":
 				group.About.BGMeta = x.ModuleData.CoverDeviation.Deviation
-				group.About.BG = devianter.UrlFromMedia(group.About.BGMeta.Media)
+				group.About.BGMeta.Url = s.ConvertDeviantArtUrlToSkunkyArt(group.About.BGMeta.Url)
+				group.About.BG = s.ParseMedia(group.About.BGMeta.Media)
 			case "group_admins":
 				var htm strings.Builder
 				for _, z := range x.ModuleData.GroupAdmins.Results {
 					htm.WriteString(`<p><img src="`)
-					htm.WriteString("/media/")
-					htm.WriteString(z.User.Username)
-					htm.WriteString("?type=a")
+					htm.WriteString(UrlBuilder("media", "emojitar", z.User.Username, "?type=a"))
 					htm.WriteString(`"><a href="`)
-					htm.WriteString("/group_user?type=about&q=")
-					htm.WriteString(z.User.Username)
+					htm.WriteString(UrlBuilder("group_user", "?type=about&q=", z.User.Username))
 					htm.WriteString(`">`)
 					htm.WriteString(z.User.Username)
 					htm.WriteString(`</a></p>`)
@@ -175,24 +200,18 @@ func (s skunkyart) GRUser() {
 		s.ReturnHTTPError(400)
 	}
 
-	if !s.atom {
+	if !s.Atom {
 		s.ExecuteTemplate("html/gruser.htm", &s)
 	}
 }
 
 // посты
 func (s skunkyart) Deviation(author, postname string) {
-	// поиск ID
-	re := regexp.MustCompile("[0-9]+").FindAllString(postname, -1)
-	if len(re) >= 1 {
-		var post struct {
-			Post       devianter.Post
-			StringTime string
-			Tags       string
-			Comments   string
-		}
+	id_search := regexp.MustCompile("[0-9]+").FindAllString(postname, -1)
+	if len(id_search) >= 1 {
+		post := &s.Templates.Deviation
 
-		id := re[len(re)-1]
+		id := id_search[len(id_search)-1]
 		post.Post = devianter.DeviationFunc(id, author)
 
 		post.Post.Description = ParseDescription(post.Post.Deviation.TextContent)
@@ -202,9 +221,9 @@ func (s skunkyart) Deviation(author, postname string) {
 		// хештэги
 		for _, x := range post.Post.Deviation.Extended.Tags {
 			var tag strings.Builder
-			tag.WriteString(` <a href="/search?q=`)
-			tag.WriteString(x.Name)
-			tag.WriteString(`&type=tag">#`)
+			tag.WriteString(` <a href="`)
+			tag.WriteString(UrlBuilder("search", "?q=", x.Name, "&type=tag"))
+			tag.WriteString(`">#`)
 			tag.WriteString(x.Name)
 			tag.WriteString("</a>")
 
@@ -213,7 +232,7 @@ func (s skunkyart) Deviation(author, postname string) {
 
 		post.Comments = s.ParseComments(devianter.CommentsFunc(id, post.Post.Comments.Cursor, s.Page, 1))
 
-		s.ExecuteTemplate("html/deviantion.htm", &post)
+		s.ExecuteTemplate("html/deviantion.htm", &s)
 	} else {
 		s.ReturnHTTPError(400)
 	}
@@ -221,17 +240,17 @@ func (s skunkyart) Deviation(author, postname string) {
 
 func (s skunkyart) DD() {
 	dd := devianter.DailyDeviationsFunc(s.Page)
-	ddparsed := s.DeviationList(dd.Deviations, dlist{
+	s.Templates.SomeList = s.DeviationList(dd.Deviations, dlist{
 		Pages: 0,
 		More:  dd.HasMore,
 	})
-	if !s.atom {
-		s.ExecuteTemplate("html/list.htm", &ddparsed)
+	if !s.Atom {
+		s.ExecuteTemplate("html/list.htm", &s)
 	}
 }
 
 func (s skunkyart) Search() {
-	s.atom = false
+	s.Atom = false
 	var e error
 	ss := &s.Templates.Search
 	switch s.Type {
@@ -263,3 +282,9 @@ func (s skunkyart) Emojitar(name string) {
 		s.ReturnHTTPError(400)
 	}
 }
+
+func (s skunkyart) About() {
+	s.Templates.About.Nsfw = CFG.Nsfw
+	s.Templates.About.Proxy = CFG.Proxy
+	s.ExecuteTemplate("html/about.htm", &s)
+}
diff --git a/atom.xml b/atom.xml
deleted file mode 100644
index 784f37d..0000000
--- a/atom.xml
+++ /dev/null
@@ -1,43 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<feed xmlns:yt="http://www.youtube.com/xml/schemas/2015" xmlns:media="http://search.yahoo.com/mrss/" xmlns="http://www.w3.org/2005/Atom" xml:lang="en-US">
-  <link rel="self" href="https://inv.clovius.club/feed/channel/UCuxpxCCevIlF-k-K5YU8XPA"/>
-  <id>yt:channel:UCuxpxCCevIlF-k-K5YU8XPA</id>
-  <yt:channelId>UCuxpxCCevIlF-k-K5YU8XPA</yt:channelId>
-  <icon>https://yt3.googleusercontent.com/ytc/AIdro_n7bKr9iusclFjvYw3U8UR74iTCoMcdGTKI3h55SOG8mSsY=s900-c-k-c0x00ffffff-no-rj</icon>
-  <title>Test</title>
-  <link rel="alternate" href="https://skunky.ebloid.ru"/>
-  <author>
-    <name>lost+skunk</name>
-    <uri>https://skunky.ebloid.ru</uri>
-  </author>
-  <image>
-    <url>https://ebloid.ru/_matrix/media/v3/download/ebloid.ru/enfAtfdjzCQMRNBgLXmiURfJ?allow_redirect=true</url>
-    <title>lost+skunk</title>
-    <link rel="self" href="https://skunky.ebloid.ru"/>
-  </image>
-  <entry>
-    <title>Test</title>
-    <link rel="alternate" href="https://2ip.ua"/>
-    <author>
-      <name>lost+skunk</name>
-      <uri>https://skunky.ebloid.ru</uri>
-    </author>
-    <content type="xhtml">
-      <div xmlns="http://www.w3.org/1999/xhtml">
-        <a href="https://2ip.ua">
-          <img src="https://images-wixmp-ed30a86b8c4ca887773594c2.wixmp.com/f/526e735c-e3da-4542-a339-72ccdf827a00/dh3mfuq-5bd6472c-68f4-428d-bb90-8d461fc21410.jpg/v1/fill/w_1920,h_1080/image.gif?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1cm46YXBwOjdlMGQxODg5ODIyNjQzNzNhNWYwZDQxNWVhMGQyNmUwIiwiaXNzIjoidXJuOmFwcDo3ZTBkMTg4OTgyMjY0MzczYTVmMGQ0MTVlYTBkMjZlMCIsIm9iaiI6W1t7ImhlaWdodCI6Ijw9MTA4MCIsInBhdGgiOiJcL2ZcLzUyNmU3MzVjLWUzZGEtNDU0Mi1hMzM5LTcyY2NkZjgyN2EwMFwvZGgzbWZ1cS01YmQ2NDcyYy02OGY0LTQyOGQtYmI5MC04ZDQ2MWZjMjE0MTAuanBnIiwid2lkdGgiOiI8PTE5MjAifV1dLCJhdWQiOlsidXJuOnNlcnZpY2U6aW1hZ2Uub3BlcmF0aW9ucyJdfQ.S_1DGdd29F5l6Y46Q_bumtGlEEUf_WawZsmF8kBygBs"/>
-        </a>
-        <p>test</p>
-      </div>
-    </content>
-    <published>2024-07-04T00:37:55+00:00</published>
-    <media:group>
-      <media:title>GM Just Announced Their Shutting Down Production in America and Firing Their Workers</media:title>
-      <media:thumbnail url="https://ebloid.ru/_matrix/media/v3/download/ebloid.ru/enfAtfdjzCQMRNBgLXmiURfJ?allow_redirect=true" width="320" height="180"/>
-      <media:description>test</media:description>
-    </media:group>
-    <media:community>
-      <media:statistics views="91247"/>
-    </media:community>
-  </entry>
-</feed>
diff --git a/config.example.json b/config.example.json
index 1d979bd..56b5218 100644
--- a/config.example.json
+++ b/config.example.json
@@ -1,12 +1,13 @@
 {
     "listen": "0.0.0.0:3003",
-    "base_uri": null,
+    "base-path": "/skunky/",
     "cache": {
         "enabled": true,
         "path": "cache",
         "lifetime": null,
-        "max_size": 10000
+        "max-size": 100000
     },
-    "proxy": false,
+    "proxy": true,
+    "wixmp-proxy": "http://127.0.0.1:8080",
     "nsfw": false
 }
\ No newline at end of file
diff --git a/css/skunky.css b/css/skunky.css
index 9e38325..b440f5b 100644
--- a/css/skunky.css
+++ b/css/skunky.css
@@ -28,9 +28,12 @@ form input, button, select {
     border: 0px;
     border-radius: 1px;
 }
-.nsfw, .true {
+.nsfw, .about-false {
     color: red;
 }
+.about-true {
+    color: green;
+}
 .author {
     color: seagreen;
 }
diff --git a/html/about.htm b/html/about.htm
index 2c43bbe..4a1b316 100644
--- a/html/about.htm
+++ b/html/about.htm
@@ -2,12 +2,12 @@
 <html>
     <head>
         <title>SkunkyArt</title>
-        <link rel="stylesheet" href="gui/css/skunky.css">
+        <link rel="stylesheet" href="{{.BasePath}}gui/css/skunky.css">
     </head>
     <main>
         <header>
-            <h1><a href="/">HOME</a> | <a href="/dd">DD</a></h1>
-            <form method="get" action="/search">
+            <h1><a href="{{.BasePath}}">HOME</a> | <a href="{{.BasePath}}dd">DD</a></h1>
+            <form method="get" action="{{.BasePath}}search">
                 <input type="text" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false">
                 <select name="type">
                     <option value="all">All</option>
@@ -19,5 +19,11 @@
         <p>
             SkunkyArt is an alternative frontend for deviantart.com, written in Go.
         </p>
+        <h2>Instance settings:</h2>
+        <ul>
+            <li><b>NSFW</b>: <span class="about-{{.Templates.About.Nsfw}}">{{if .Templates.About.Nsfw}}YES{{else}}NO{{end}}</span></li>
+            <li><b>Proxyfing</b>: <span class="about-{{.Templates.About.Proxy}}">{{if .Templates.About.Proxy}}YES{{else}}NO{{end}}</span></li>
+        </ul>
+        <p>Copyright <a href="https://go.kde.org/matrix/#/@softpigeones:ebloid.ru" target="_blank">lost+skunk</a>, X11. <a href="https://git.macaw.me/skunky/skunkyart/src/tag/v1.3" target="_blank">SkunkyArt v1.3</a></p>
     </main>
 </html>
\ No newline at end of file
diff --git a/html/deviantion.htm b/html/deviantion.htm
index 6308c91..7a7f282 100644
--- a/html/deviantion.htm
+++ b/html/deviantion.htm
@@ -1,13 +1,13 @@
 <!DOCTYPE html>
 <html>
     <head>
-        <title>SkunkyArt | {{.Post.Deviation.Author.Username}} - {{.Post.Deviation.Title}}</title>
-        <link rel="stylesheet" href="/gui/css/skunky.css">
+        <title>SkunkyArt | {{.Templates.Deviation.Post.Deviation.Author.Username}} - {{.Templates.Deviation.Post.Deviation.Title}}</title>
+        <link rel="stylesheet" href="{{.BasePath}}gui/css/skunky.css">
     </head>
     <main>
         <header>
-            <h1><a href="/">HOME</a> | <a href="/dd">DD</a></h1>
-            <form method="get" action="/search">
+            <h1><a href="{{.BasePath}}">HOME</a> | <a href="{{.BasePath}}dd">DD</a></h1>
+            <form method="get" action="{{.BasePath}}search">
                 <input type="text" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false">
                 <select name="type">
                     <option value="all">All</option>
@@ -17,29 +17,29 @@
             </form>
         </header>
         <figure>
-            <img src="/media/{{.Post.Deviation.Author.Username}}?type=a" width="30px">
-            <span><strong><a href="/group_user?type=about&q={{.Post.Deviation.Author.Username}}">{{.Post.Deviation.Author.Username}}</a></strong> — {{if (.Post.Deviation.DD)}}
-                <span class="dd" title="Daily Deviation!"><b>{{.Post.Deviation.Title}}</b></span>
-                {{else}}{{.Post.Deviation.Title}}{{end}}
-                 {{if (ne .Post.Deviation.License "none")}}<mark title="License">{{.Post.Deviation.License}}</mark>{{end}} {{if (.Post.Deviation.AI)}}[🤖]{{end}} 
-                 {{if (.Post.Deviation.NSFW)}}[<span class="nsfw">NSFW</span>]{{end}}
+            <img src="{{.BasePath}}media/emojitar/{{.Templates.Deviation.Post.Deviation.Author.Username}}?type=a" width="30px">
+            <span><strong><a href="{{.BasePath}}group_user?type=about&q={{.Templates.Deviation.Post.Deviation.Author.Username}}">{{.Templates.Deviation.Post.Deviation.Author.Username}}</a></strong> — {{if (.Templates.Deviation.Post.Deviation.DD)}}
+                <span class="dd" title="Daily Deviation!"><b>{{.Templates.Deviation.Post.Deviation.Title}}</b></span>
+                {{else}}{{.Templates.Deviation.Post.Deviation.Title}}{{end}}
+                 {{if (ne .Templates.Deviation.Post.Deviation.License "none")}}<mark title="License">{{.Templates.Deviation.Post.Deviation.License}}</mark>{{end}} {{if (.Templates.Deviation.Post.Deviation.AI)}}[🤖]{{end}} 
+                 {{if (.Templates.Deviation.Post.Deviation.NSFW)}}[<span class="nsfw">NSFW</span>]{{end}}
             </span>
             <br>
-            {{if (ne .Post.IMG "")}}
-            <a href="{{.Post.IMG}}" title="open/download image"><img src="{{.Post.IMG}}" width="50%"></a>
+            {{if (ne .Templates.Deviation.Post.IMG "")}}
+            <a href="{{.Templates.Deviation.Post.IMG}}" title="open/download image"><img src="{{.Templates.Deviation.Post.IMG}}" width="50%"></a>
             <br>
             {{end}}
-            {{if (ne .Tags "")}}
-            {{.Tags}}<br>
+            {{if (ne .Templates.Deviation.Tags "")}}
+            {{.Templates.Deviation.Tags}}<br>
             {{end}}
-            <span>Published: <strong>{{.StringTime}}</strong>; Views: <strong>{{.Post.Deviation.Stats.Views}}</strong>; Favourites: <strong>{{.Post.Deviation.Stats.Favourites}}</strong>; Downloads: <strong>{{.Post.Deviation.Stats.Downloads}}</strong></span>
-            {{if (ne .Post.Description "")}}
+            <span>Published: <strong>{{.Templates.Deviation.StringTime}}</strong>; Views: <strong>{{.Templates.Deviation.Post.Deviation.Stats.Views}}</strong>; Favourites: <strong>{{.Templates.Deviation.Post.Deviation.Stats.Favourites}}</strong>; Downloads: <strong>{{.Templates.Deviation.Post.Deviation.Stats.Downloads}}</strong></span>
+            {{if (ne .Templates.Deviation.Post.Description "")}}
             <figcaption>
-            {{.Post.Description}}
+            {{.Templates.Deviation.Post.Description}}
             </figcaption>
             {{end}}
-            {{if (ne .Comments "")}}
-            {{.Comments}}
+            {{if (ne .Templates.Deviation.Comments "")}}
+            {{.Templates.Deviation.Comments}}
             {{end}}
         </figure>
     </main>
diff --git a/html/gruser.htm b/html/gruser.htm
index 89347aa..4280628 100644
--- a/html/gruser.htm
+++ b/html/gruser.htm
@@ -8,14 +8,14 @@
         gallery of {{.Templates.GroupUser.GR.Owner.Username}}
         {{end}}
         </title>
-        <link rel="stylesheet" href="/gui/css/skunky.css">
+        <link rel="stylesheet" href="{{.BasePath}}gui/css/skunky.css">
     </head>
     <main>
         <header>
-            <h1><a href="/">HOME</a> | <a href="/dd">DD</a>
+            <h1><a href="{{.BasePath}}">HOME</a> | <a href="{{.BasePath}}dd">DD</a>
             | <a href="?q={{.Templates.GroupUser.GR.Owner.Username}}&type={{if eq .Type 'a'}}gallery">Gallery{{else}}about">About{{end}}</a>
              | <a href="?q={{.Templates.GroupUser.GR.Owner.Username}}&type=gallery&atom=true">RSS</a></h1>
-             <form method="get" action="/search">
+             <form method="get" action="{{.BasePath}}search">
                 <input type="gallery" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false">
                 <input type="hidden" name="usr" value="{{.Templates.GroupUser.GR.Owner.Username}}">
                 <select name="type">
@@ -32,7 +32,7 @@
             {{.Templates.GroupUser.About.BGMeta.Author.Username}} - {{end}}{{.Templates.GroupUser.About.BGMeta.Title}}" src="{{.Templates.GroupUser.About.BG}}"></a>
             {{end}}
 
-            <img src="/media/{{.Templates.GroupUser.GR.Owner.Username}}?type=a" width="30px" height="30px"> <b>{{.Templates.GroupUser.GR.Owner.Username}}</b>
+            <img src="{{.BasePath}}media/emojitar/{{.Templates.GroupUser.GR.Owner.Username}}?type=a" width="30px" height="30px"> <b>{{.Templates.GroupUser.GR.Owner.Username}}</b>
             {{if (eq .Templates.GroupUser.About.A.Gender "male")}}
             ♂️
             {{end}}
diff --git a/html/index.htm b/html/index.htm
index e754884..c01fd8c 100644
--- a/html/index.htm
+++ b/html/index.htm
@@ -2,11 +2,11 @@
 <html>
     <head>
         <title>SkunkyArt</title>
-        <link rel="stylesheet" href="gui/css/skunky.css"
+        <link rel="stylesheet" href="{{.}}gui/css/skunky.css"/>
     </head>
     <main>
         <center>
-            <form method="get" action="search">
+            <form method="get" action="{{.}}search">
                 <input type="text" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false">
                 <select name="type">
                     <option value="all">All</option>
@@ -14,7 +14,7 @@
                 </select>
                 <button type="submit">Search!</button>
             </form>
-            <h1><a href="dd">Daily Deviations</a> | <a href="about">About</a> | <a href="https://git.macaw.me/skunky/SkunkyArt" target="_blank">Source Code</a></h1>
+            <h1><a href="{{.}}dd">Daily Deviations</a> | <a href="{{.}}about">About</a> | <a href="https://git.macaw.me/skunky/SkunkyArt" target="_blank">Source Code</a></h1>
         </center>
     </main>
 </html>
\ No newline at end of file
diff --git a/html/list.htm b/html/list.htm
index 23d48e5..57b4659 100644
--- a/html/list.htm
+++ b/html/list.htm
@@ -2,12 +2,12 @@
 <html>
     <head>
         <title>SkunkyArt</title>
-        <link rel="stylesheet" href="gui/css/skunky.css">
+        <link rel="stylesheet" href="{{.BasePath}}gui/css/skunky.css">
     </head>
     <main>
         <header>
-            <h1><a href="/">HOME</a> | <a href="/dd">DD</a> | <a href="?atom=true">RSS</a></h1>
-            <form method="get" action="/search">
+            <h1><a href="{{.BasePath}}">HOME</a> | <a href="{{.BasePath}}dd">DD</a> | <a href="?atom=true">RSS</a></h1>
+            <form method="get" action="{{.BasePath}}search">
                 <input type="text" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false">
                 <select name="type">
                     <option value="all">All</option>
@@ -16,6 +16,6 @@
                 <button type="submit">Search!</button>
             </form>
         </header>
-        {{.}}
+        {{.Templates.SomeList}}
     </main>
 </html>
\ No newline at end of file
diff --git a/html/search.htm b/html/search.htm
index a78feb6..8cdcbc1 100644
--- a/html/search.htm
+++ b/html/search.htm
@@ -1,12 +1,12 @@
 <!DOCTYPE html>
 <html>
     <head>
-        <title>SkunkyArt | Search "{{.Query}}"</title>
-        <link rel="stylesheet" href="/gui/css/skunky.css">
+        <title>SkunkyArt | Search "{{.QueryRaw}}"</title>
+        <link rel="stylesheet" href="{{.BasePath}}gui/css/skunky.css">
     </head>
     <main>
         <header>
-            <h1><a href="/">HOME</a> | <a href="/dd">DD</a></h1>
+            <h1><a href="{{.BasePath}}">HOME</a> | <a href="{{.BasePath}}dd">DD</a></h1>
             <form method="get" action="search">
                 <input type="text" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false">
                 <select name="type">
diff --git a/main.go b/main.go
index 4e3af62..636c358 100644
--- a/main.go
+++ b/main.go
@@ -7,6 +7,7 @@ import (
 )
 
 func main() {
+	app.ExecuteConfig()
 	err := devianter.UpdateCSRF()
 	if err != nil {
 		println(err.Error())