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 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}