krz/skunky-art

Alternative privacy frontend for DeviantArt.

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

v1.3.1: app/util.go · raw

  1package app
  2
  3import (
  4	"encoding/base64"
  5	"io"
  6	"net/http"
  7	u "net/url"
  8	"os"
  9	"strconv"
 10	"strings"
 11	"syscall"
 12	"text/template"
 13	"time"
 14
 15	"git.macaw.me/skunky/devianter"
 16	"golang.org/x/net/html"
 17)
 18
 19/* INTERNAL */
 20func exit(msg string, code int) {
 21	println(msg)
 22	os.Exit(code)
 23}
 24func try(e error) {
 25	if e != nil {
 26		println(e.Error())
 27	}
 28}
 29func tryWithExitStatus(err error, code int) {
 30	if err != nil {
 31		exit(err.Error(), code)
 32	}
 33}
 34
 35func RefreshInstances() {
 36	for {
 37		func() {
 38			defer func() {
 39				if r := recover(); r != nil {
 40					recover()
 41				}
 42			}()
 43			Templates["instances.json"] = string(Download("https://git.macaw.me/skunky/SkunkyArt/raw/branch/master/instances.json").Body)
 44		}()
 45		time.Sleep(1 * time.Hour)
 46	}
 47}
 48
 49// some crap for frontend
 50func (s skunkyart) ExecuteTemplate(file string, data any) {
 51	var buf strings.Builder
 52	tmp := template.New(file)
 53	tmp, e := tmp.Parse(Templates[file])
 54	try(e)
 55	try(tmp.Execute(&buf, &data))
 56	wr(s.Writer, buf.String())
 57}
 58
 59func UrlBuilder(strs ...string) string {
 60	var str strings.Builder
 61	l := len(strs)
 62	str.WriteString(Host)
 63	str.WriteString(CFG.URI)
 64	for n, x := range strs {
 65		str.WriteString(x)
 66		if n+1 < l && !(strs[n+1][0] == '?' || strs[n+1][0] == '&') && !(x[0] == '?' || x[0] == '&') {
 67			str.WriteString("/")
 68		}
 69	}
 70	return str.String()
 71}
 72
 73func (s skunkyart) ReturnHTTPError(status int) {
 74	s.Writer.WriteHeader(status)
 75
 76	var msg strings.Builder
 77	msg.WriteString(`<html><link rel="stylesheet" href="`)
 78	msg.WriteString(UrlBuilder("stylesheet"))
 79	msg.WriteString(`" /><h1>`)
 80	msg.WriteString(strconv.Itoa(status))
 81	msg.WriteString(" - ")
 82	msg.WriteString(http.StatusText(status))
 83	msg.WriteString("</h1></html>")
 84
 85	wr(s.Writer, msg.String())
 86}
 87
 88type Downloaded struct {
 89	Headers http.Header
 90	Status  int
 91	Body    []byte
 92}
 93
 94func Download(url string) (d Downloaded) {
 95	cli := &http.Client{}
 96	if CFG.DownloadProxy != "" {
 97		u, e := u.Parse(CFG.DownloadProxy)
 98		try(e)
 99		cli.Transport = &http.Transport{Proxy: http.ProxyURL(u)}
100	}
101
102	req, e := http.NewRequest("GET", url, nil)
103	try(e)
104	req.Header.Set("User-Agent", CFG.UserAgent)
105
106	resp, e := cli.Do(req)
107	try(e)
108	defer resp.Body.Close()
109	b, e := io.ReadAll(resp.Body)
110	try(e)
111
112	d.Body = b
113	d.Status = resp.StatusCode
114	d.Headers = resp.Header
115	return
116}
117
118// caching
119func (s skunkyart) DownloadAndSendMedia(subdomain, path string) {
120	var url strings.Builder
121	url.WriteString("https://images-wixmp-")
122	url.WriteString(subdomain)
123	url.WriteString(".wixmp.com/")
124	url.WriteString(path)
125	url.WriteString("?token=")
126	url.WriteString(s.Args.Get("token"))
127
128	if CFG.Cache.Enabled {
129		fname := CFG.Cache.Path + "/" + base64.StdEncoding.EncodeToString([]byte(subdomain+path))
130		file, e := os.Open(fname)
131
132		if e != nil {
133			dwnld := Download(url.String())
134			if dwnld.Status == 200 && dwnld.Headers["Content-Type"][0][:5] == "image" {
135				try(os.WriteFile(fname, dwnld.Body, 0700))
136				s.Writer.Write(dwnld.Body)
137			}
138		} else {
139			file, e := io.ReadAll(file)
140			try(e)
141			s.Writer.Write(file)
142		}
143	} else if CFG.Proxy {
144		dwnld := Download(url.String())
145		s.Writer.Write(dwnld.Body)
146	} else {
147		s.Writer.WriteHeader(403)
148		s.Writer.Write([]byte("Sorry, butt proxy on this instance are disabled."))
149	}
150}
151
152func InitCacheSystem() {
153	c := &CFG.Cache
154	os.Mkdir(CFG.Cache.Path, 0700)
155	for {
156		dir, e := os.Open(c.Path)
157		try(e)
158		stat, e := dir.Stat()
159		try(e)
160
161		dirnames, e := dir.Readdirnames(-1)
162		try(e)
163		for _, a := range dirnames {
164			a = c.Path + "/" + a
165			if c.Lifetime != "" {
166				now := time.Now().UnixMilli()
167
168				f, _ := os.Stat(a)
169				stat := f.Sys().(*syscall.Stat_t)
170				time := time.Unix(stat.Ctim.Unix()).UnixMilli()
171
172				if time+lifetimeParsed <= now {
173					try(os.RemoveAll(a))
174				}
175			}
176			if c.MaxSize != 0 && stat.Size() > c.MaxSize {
177				try(os.RemoveAll(a))
178			}
179		}
180
181		dir.Close()
182		time.Sleep(time.Second * time.Duration(CFG.Cache.UpdateInterval))
183	}
184}
185
186func CopyTemplatesToMemory() {
187	for _, dirname := range CFG.Dirs {
188		dir, e := os.ReadDir(dirname)
189		tryWithExitStatus(e, 1)
190
191		for _, x := range dir {
192			file, e := os.ReadFile(dirname + "/" + x.Name())
193			tryWithExitStatus(e, 1)
194			Templates[x.Name()] = string(file)
195		}
196	}
197}
198
199/* PARSING HELPERS */
200func ParseMedia(media devianter.Media, thumb ...int) string {
201	url := devianter.UrlFromMedia(media, thumb...)
202	if len(url) != 0 && CFG.Proxy {
203		url = url[21:]
204		dot := strings.Index(url, ".")
205
206		return UrlBuilder("media", "file", url[:dot], url[dot+11:])
207	}
208	return url
209}
210
211func ConvertDeviantArtUrlToSkunkyArt(url string) (output string) {
212	if len(url) > 32 && url[27:32] != "stash" {
213		url = url[27:]
214		firstshash := strings.Index(url, "/")
215		lastshash := firstshash + strings.Index(url[firstshash+1:], "/")
216		if lastshash != -1 {
217			output = UrlBuilder("post", url[:firstshash], url[lastshash+2:])
218		}
219	}
220	return
221}
222
223func BuildUserPlate(name string) string {
224	var htm strings.Builder
225	htm.WriteString(`<div class="user-plate"><img src="`)
226	htm.WriteString(UrlBuilder("media", "emojitar", name, "?type=a"))
227	htm.WriteString(`"><a href="`)
228	htm.WriteString(UrlBuilder("group_user", "?type=about&q=", name))
229	htm.WriteString(`">`)
230	htm.WriteString(name)
231	htm.WriteString(`</a></div>`)
232	return htm.String()
233}
234
235func GetValueOfTag(t *html.Tokenizer) string {
236	for tt := t.Next(); ; {
237		if tt == html.TextToken {
238			return string(t.Text())
239		} else {
240			return ""
241		}
242	}
243}
244
245// навигация по страницам
246type DeviationList struct {
247	Pages int
248	More  bool
249}
250
251// FIXME: на некоротрых артах первая страница может вызывать полное отсутствие панели навигации.
252func (s skunkyart) NavBase(c DeviationList) string {
253	var list strings.Builder
254
255	list.WriteString("<br>")
256	prevrev := func(msg string, page int, onpage bool) {
257		if !onpage {
258			list.WriteString(`<a href="?p=`)
259			list.WriteString(strconv.Itoa(page))
260			if s.Type != 0 {
261				list.WriteString("&type=")
262				list.WriteRune(s.Type)
263			}
264			if s.Query != "" {
265				list.WriteString("&q=")
266				list.WriteString(s.Query)
267			}
268			if f := s.Args.Get("folder"); f != "" {
269				list.WriteString("&folder=")
270				list.WriteString(f)
271			}
272			list.WriteString(`">`)
273			list.WriteString(msg)
274			list.WriteString("</a> ")
275		} else {
276			list.WriteString(strconv.Itoa(page))
277			list.WriteString(" ")
278		}
279	}
280
281	p := s.Page
282
283	if p > 1 {
284		prevrev("<= Prev |", p-1, false)
285	} else {
286		p = 1
287	}
288
289	for i, x := p-6, 0; (i <= c.Pages && i <= p+6) && x < 12; i++ {
290		if i > 0 {
291			var onPage bool
292			if i == p {
293				onPage = true
294			}
295
296			prevrev(strconv.Itoa(i), i, onPage)
297			x++
298		}
299	}
300
301	if c.More {
302		prevrev("| Next =>", p+1, false)
303	}
304
305	return list.String()
306}