krz/skunky-art
Alternative privacy frontend for DeviantArt.
clone: git clone https://gitbay.org/krz/skunky-art.git
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}