krz/skunky-art

Alternative privacy frontend for DeviantArt.

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

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