krz/skunky-art
Alternative privacy frontend for DeviantArt.
clone: git clone https://gitbay.org/krz/skunky-art.git
1package app
2
3import (
4 "encoding/json"
5 "io"
6 "net/http"
7 "net/url"
8 "os"
9 "skunkyart/static"
10 "strconv"
11 "strings"
12 "text/template"
13 "time"
14
15 "git.macaw.me/skunky/devianter"
16 "golang.org/x/net/html"
17)
18
19/* INTERNAL */
20var wr = io.WriteString
21
22func exit(msg string, code int) {
23 println(msg)
24 os.Exit(code)
25}
26func try(e error) {
27 if e != nil {
28 println(e.Error())
29 }
30}
31func tryWithExitStatus(err error, code int) {
32 if err != nil {
33 exit(err.Error(), code)
34 }
35}
36
37func restore() {
38 if r := recover(); r != nil {
39 recover()
40 }
41}
42
43var instances []byte
44var About instanceAbout
45
46func RefreshInstances() {
47 for {
48 func() {
49 defer restore()
50 instances = Download("https://git.macaw.me/skunky/SkunkyArt/raw/branch/master/instances.json").Body
51 try(json.Unmarshal(instances, &About))
52 }()
53 time.Sleep(1 * time.Hour)
54 }
55}
56
57// some crap for frontend
58type instanceAbout struct {
59 Proxy bool
60 Nsfw bool
61 Instances []settings
62}
63
64type skunkyart struct {
65 Writer http.ResponseWriter
66
67 Args url.Values
68 Page int
69 Type rune
70 Atom bool
71
72 BasePath, Endpoint string
73 Query, QueryRaw string
74
75 API API
76 Version string
77
78 Templates struct {
79 About instanceAbout
80
81 SomeList string
82 DDStrips string
83 Deviation struct {
84 Post devianter.Post
85 Related string
86 StringTime string
87 Tags string
88 Comments string
89 }
90
91 GroupUser struct {
92 GR devianter.GRuser
93 Admins string
94 Group bool
95 CreationDate string
96
97 About struct {
98 A devianter.About
99
100 DescriptionFormatted string
101 Interests, Social string
102 Comments string
103 BG string
104 BGMeta devianter.Deviation
105 }
106
107 Gallery struct {
108 Folders string
109 Pages int
110 List string
111 }
112 }
113 Search struct {
114 Content devianter.Search
115 List string
116 }
117 }
118}
119
120func (s skunkyart) ExecuteTemplate(file, dir string, data any) {
121 var buf strings.Builder
122 tmp := template.New(file)
123 tmp, err := tmp.ParseFS(static.Templates, dir+"/*")
124 if err != nil {
125 s.Writer.WriteHeader(500)
126 wr(s.Writer, err.Error())
127 return
128 }
129 try(tmp.Execute(&buf, &data))
130 wr(s.Writer, buf.String())
131}
132
133func UrlBuilder(strs ...string) string {
134 var str strings.Builder
135 l := len(strs)
136 str.WriteString(Host)
137 str.WriteString(CFG.URI)
138 for n, x := range strs {
139 str.WriteString(x)
140 if n := n + 1; n < l && len(strs[n]) != 0 && !(strs[n][0] == '?' || strs[n][0] == '&') && !(x[0] == '?' || x[0] == '&') {
141 str.WriteString("/")
142 }
143 }
144 return str.String()
145}
146
147func (s skunkyart) Error(dAerr devianter.Error) {
148 s.Writer.WriteHeader(502)
149
150 var msg strings.Builder
151 msg.WriteString(`<html><link rel="stylesheet" href="`)
152 msg.WriteString(UrlBuilder("stylesheet"))
153 msg.WriteString(`" /><h3>DeviantArt error — '`)
154 msg.WriteString(dAerr.Error)
155 msg.WriteString("'</h3></html>")
156
157 wr(s.Writer, msg.String())
158}
159
160func (s skunkyart) ReturnHTTPError(status int) {
161 s.Writer.WriteHeader(status)
162
163 var msg strings.Builder
164 msg.WriteString(`<html><link rel="stylesheet" href="`)
165 msg.WriteString(UrlBuilder("stylesheet"))
166 msg.WriteString(`" /><h1>`)
167 msg.WriteString(strconv.Itoa(status))
168 msg.WriteString(" - ")
169 msg.WriteString(http.StatusText(status))
170 msg.WriteString("</h1></html>")
171
172 wr(s.Writer, msg.String())
173}
174
175func (s skunkyart) SetFilename(name string) {
176 var filename strings.Builder
177 filename.WriteString(`filename="`)
178 filename.WriteString(name)
179 filename.WriteString(`"`)
180 s.Writer.Header().Add("Content-Disposition", filename.String())
181}
182
183type Downloaded struct {
184 Headers http.Header
185 Status int
186 Body []byte
187}
188
189func Download(urlString string) (d Downloaded) {
190 cli := &http.Client{}
191 if CFG.DownloadProxy != "" {
192 u, e := url.Parse(CFG.DownloadProxy)
193 try(e)
194 cli.Transport = &http.Transport{Proxy: http.ProxyURL(u)}
195 }
196
197 req, e := http.NewRequest("GET", urlString, nil)
198 try(e)
199 req.Header.Set("User-Agent", CFG.UserAgent)
200
201 resp, e := cli.Do(req)
202 try(e)
203 defer resp.Body.Close()
204 b, e := io.ReadAll(resp.Body)
205 try(e)
206
207 d.Body = b
208 d.Status = resp.StatusCode
209 d.Headers = resp.Header
210 return
211}
212
213/* PARSING HELPERS */
214func ParseMedia(media devianter.Media, thumb ...int) string {
215 mediaUrl, filename := devianter.UrlFromMedia(media, thumb...)
216 if len(mediaUrl) != 0 && CFG.Proxy {
217 mediaUrl = mediaUrl[21:]
218 dot := strings.Index(mediaUrl, ".")
219 if filename == "" {
220 filename = "image.gif"
221 }
222 return UrlBuilder("media", "file", mediaUrl[:dot], mediaUrl[dot+11:], "&filename=", filename)
223 } else if !CFG.Proxy {
224 return mediaUrl
225 }
226 return ""
227}
228
229func ConvertDeviantArtUrlToSkunkyArt(url string) (output string) {
230 if len(url) > 32 && url[27:32] != "stash" {
231 url = url[27:]
232 firstshash := strings.Index(url, "/")
233 lastshash := firstshash + strings.Index(url[firstshash+1:], "/")
234 if lastshash != -1 {
235 output = UrlBuilder("post", url[:firstshash], url[lastshash+2:])
236 }
237 }
238 return
239}
240
241func BuildUserPlate(name string) string {
242 var htm strings.Builder
243 htm.WriteString(`<div class="user-plate"><img src="`)
244 htm.WriteString(UrlBuilder("media", "emojitar", name, "?type=a"))
245 htm.WriteString(`"><a href="`)
246 htm.WriteString(UrlBuilder("group_user", "?type=about&q=", name))
247 htm.WriteString(`">`)
248 htm.WriteString(name)
249 htm.WriteString(`</a></div>`)
250 return htm.String()
251}
252
253func GetValueOfTag(t *html.Tokenizer) string {
254 for tt := t.Next(); ; {
255 if tt == html.TextToken {
256 return string(t.Text())
257 } else {
258 return ""
259 }
260 }
261}
262
263// навигация по страницам
264type DeviationList struct {
265 Pages int
266 More bool
267}
268
269// FIXME: на некоротрых артах первая страница может вызывать полное отсутствие панели навигации.
270func (s skunkyart) NavBase(c DeviationList) string {
271 var list strings.Builder
272
273 list.WriteString("<br>")
274 prevrev := func(msg string, page int, onpage bool) {
275 if !onpage {
276 list.WriteString(`<a href="`)
277 list.WriteString(Path)
278 list.WriteString(`?p=`)
279 list.WriteString(strconv.Itoa(page))
280 if s.Type != 0 {
281 list.WriteString("&type=")
282 list.WriteRune(s.Type)
283 }
284 if s.Query != "" {
285 list.WriteString("&q=")
286 list.WriteString(s.Query)
287 }
288 if f := s.Args.Get("folder"); f != "" {
289 list.WriteString("&folder=")
290 list.WriteString(f)
291 }
292 list.WriteString(`">`)
293 list.WriteString(msg)
294 list.WriteString("</a> ")
295 } else {
296 list.WriteString(strconv.Itoa(page))
297 list.WriteString(" ")
298 }
299 }
300
301 p := s.Page
302
303 if p > 1 {
304 prevrev("<= Prev |", p-1, false)
305 } else {
306 p = 1
307 }
308
309 for i, x := p-6, 0; (i <= c.Pages && i <= p+6) && x < 12; i++ {
310 if i > 0 {
311 var onPage bool
312 if i == p {
313 onPage = true
314 }
315
316 prevrev(strconv.Itoa(i), i, onPage)
317 x++
318 }
319 }
320
321 if c.More {
322 prevrev("| Next =>", p+1, false)
323 }
324
325 return list.String()
326}