Commit 667de65e2f

667de65e2fda7cb30ec886ccf6619a4178d5488b

parent: 8391cc34a9

Unsigned

lost+skunk <skunky@ebloid.ru> · 2024-07-13 18:32 UTC

v1.3

Layout: unified · split

LICENSE added +13
@@ -0,0 +1,13 @@
1X11 License
2
3Copyright (C) 1996 X Consortium
4
5Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
7The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
9THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE X CONSORTIUM BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10
11Except as contained in this notice, the name of the X Consortium shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Software without prior written authorization from the X Consortium.
12
13X Window System is a trademark of X Consortium, Inc.
README.md added +76
@@ -0,0 +1,76 @@
1[![Matrix room](https://img.shields.io/badge/matrix-000000?style=for-the-badge&logo=Matrix&logoColor=white)]("https://go.kde.org/matrix/#/#skunkyart:ebloid.ru")
2# Instances
3|Инстанс|Yggdrasil|I2P|Tor|NSFW|Proxifying|Country|
4|:-----:|:-------:|:-:|:-:|:--:|:--------:|:-----:|
5|[skunky.ebloid.ru](https://skunky.ebloid.ru/art)|[Yes](http://[201:eba5:d1fc:bf7b:cfcb:a811:4b8b:7ea3]/art)|No|No| No | No | Russia |
6|[clovius.club](https://skunky.clovius.club)|No|No|No| Yes | Yes | Sweden |
7|[bloat.cat](https://skunky.bloat.cat)|No|No|No| Yes | Yes | Romania |
8|[frontendfriendly.xyz](https://skunkyart.frontendfriendly.xyz)|No|No|No| Yes | Yes | Finland |
9
10# EN 🇺🇸
11## Description
12SkunkyArt 🦨 -- alternative frontend to DeviantArt, which will work without problems even on quite old hardware, due to the lack of JavaScript.
13## Config
14The sample config is in the `config.example.json` file. To specify your own path to the config, use the CLI argument `-c` or `--config`.
15* `listen` -- the address and port on which SkunkyArt will listen
16* `base-path` -- the path to the instance. Example: "`base-path`:"/art/" -> https://skunky.ebloid.ru/art/
17* `cache` -- caching system; default is off.
18* * `path` -- the path to the cache
19* * `lifetime` -- cache file lifetime; measured in Unix milliseconds.
20* * `max-size` -- maximum file size in bytes.
21* `dirs-to-memory` -- this setting determines which directories will be copied to RAM when SkunkyArt is started. Required
22* `download-proxy` -- proxy address for downloading files.
23## Examples of reverse proxies
24Nginx:
25```apache
26server {
27 listen 443 ssl;
28 server_name skunky.example.com;
29
30 location ((BASE URL)) { # if you have a separate subdomain for the frontend, insert '/' instead of '((BASE URL))'.
31 proxy_set_header Scheme $scheme;
32 proxy_http_version 1.1;
33 proxy_pass http://((IP)):((PORT));
34 }
35}
36```
37## How do I add my instance to the list?
38To do this, you must either make a PR by adding your instance to the `instances.json` file, or report it to the room in Matrix. I don't think it needs any description. However, be aware, this list has a couple rules:
391. the instance must not use Cloudflare.
402. If your instance has modified source code, you need to publish it to any free platform. For example, Github and Gitlab are not.
41## Acknowledgements
42* [Лис⚛](https://go.kde.org/matrix/#/@fox:matrix.org) -- helped me understand Go and gave me a lot of useful advice on this language.
43
44# RU 🇷🇺
45## Описание
46SkunkyArt 🦨 -- альтернативный фронтенд к DeviantArt, который будет работать без проблем даже на довольно старом оборудовании, за счёт отсутствия JavaScript.
47## Конфиг
48Пример конфига находится в файле `config.example.json`. Чтобы указать свой путь до конфига, используйте CLI-аргумент `-c` или `--config`.
49* `listen` -- адрес и порт, на котором будет слушать SkunkyArt
50* `base-path` -- путь к инстансу. Пример: "base-path": "/art/" -> https://skunky.ebloid.ru/art/
51* `cache` -- система кеширования; по умолчанию - выкл.
52* * `path` -- путь до кеша
53* * `lifetime` -- время жизни файла в кеше; измеряется в Unix-миллисекундах
54* * `max-size` -- максимальный размер файла в байтах
55* `dirs-to-memory` -- данная настройка определяет какие каталоги будут скопированы в ОЗУ при запуске SkunkyArt. Обязательна
56* `download-proxy` -- адрес прокси для загрузки файлов
57## Примеры reverse-прокси
58Nginx:
59```apache
60server {
61 listen 443 ssl;
62 server_name skunky.example.com;
63
64 location ((BASE URL)) { # если у вас отдельный поддомен для фронтенда, вместо '((BASE URL))' вставляйте '/'
65 proxy_set_header Scheme $scheme;
66 proxy_http_version 1.1;
67 proxy_pass http://((IP)):((PORT));
68 }
69}
70```
71## Как добавить свой инстанс в список?
72Чтобы это сделать, вы должны либо сделать PR, добавив в файл `instances.json` свой инстанс, либо сообщить о нём в комнате в Matrix. Думаю, он не нуждается в описании. Однако учтите, у этого списка есть пара правил:
731. Инстанс не должен использовать Cloudflare.
742. Если ваш инстанс имеет модифицированный исходный код, то вам нужно опубликовать его на любую свободную площадку. Например, Github и Gitlab таковыми не являются.
75## Благодарности
76* [Лис⚛](https://go.kde.org/matrix/#/@fox:matrix.org) -- помог разобраться в Go и много чего полезного посоветовал по этому языку.
\ No newline at end of file
TODO.md added +7
@@ -0,0 +1,7 @@
1# v1.3.x
2* Доделать парсинг описания
3* Реализовать миниатюры и оптимизировать CSS под маленькие экраны
4# v1.4
5* Реализовать темы
6* Реализовать многоязычный интерфейс
7* Реализовать API
\ No newline at end of file
app/config.go +41 −40
@@ -2,8 +2,8 @@ package app
22
33import (
44 "encoding/json"
5 "errors"
65 "os"
6 "time"
77)
88
99type cache_config struct {
@@ -15,13 +15,13 @@ type cache_config struct {
1515}
1616
1717type config struct {
18 cfg string
19 Listen string
20 BasePath string `json:"base-path"`
21 Cache cache_config
22 Proxy, Nsfw bool
23 WixmpProxy string `json:"wixmp-proxy"`
24 TemplatesDir string `json:"templates-dir"`
18 cfg string
19 Listen string
20 BasePath string `json:"base-path"`
21 Cache cache_config
22 Proxy, Nsfw bool
23 DownloadProxy string `json:"download-proxy"`
24 Dirs []string `json:"dirs-to-memory"`
2525}
2626
2727var CFG = config{
@@ -33,51 +33,52 @@ var CFG = config{
3333 Path: "cache",
3434 UpdateInterval: 1,
3535 },
36 TemplatesDir: "html",
37 Proxy: true,
38 Nsfw: true,
36 Dirs: []string{"html", "css"},
37 Proxy: true,
38 Nsfw: true,
3939}
4040
4141func ExecuteConfig() {
42 try := func(err error, exitcode int) {
43 if err != nil {
44 println(err.Error())
45 os.Exit(exitcode)
42 go func() {
43 for {
44 Templates["instances.json"] = string(Download("https://git.macaw.me/skunky/SkunkyArt/raw/branch/master/instances.json").Body)
45 time.Sleep(1 * time.Hour)
4646 }
47 }
47 }()
4848
49 a := os.Args
50 if l := len(a); l > 1 {
51 switch a[1] {
52 case "-c", "--config":
53 if l >= 3 {
54 CFG.cfg = a[2]
55 } else {
56 try(errors.New("Not enought arguments"), 1)
57 }
58 case "-h", "--help":
59 try(errors.New(`SkunkyArt v1.3 [refactoring]
49 const helpmsg = `SkunkyArt v1.3 [refactoring]
6050Usage:
6151 - [-c|--config] - path to config
6252 - [-h|--help] - returns this message
6353Example:
6454 ./skunkyart -c config.json
65Copyright lost+skunk, X11. https://git.macaw.me/skunky/skunkyart/src/tag/v1.3`), 0)
66 default:
67 try(errors.New("Unreconginzed argument: "+a[1]), 1)
68 }
69 if CFG.cfg != "" {
70 f, err := os.ReadFile(CFG.cfg)
71 try(err, 1)
55Copyright lost+skunk, X11. https://git.macaw.me/skunky/skunkyart/src/tag/v1.3`
7256
73 try(json.Unmarshal(f, &CFG), 1)
74 if CFG.Cache.Enabled && !CFG.Proxy {
75 try(errors.New("Incompatible settings detected: cannot use caching media content without proxy"), 1)
57 a := os.Args
58 for n, x := range a {
59 switch x {
60 case "-c", "--config":
61 if len(a) >= 3 {
62 CFG.cfg = a[n+1]
63 } else {
64 exit("Not enought arguments", 1)
7665 }
66 case "-h", "--help":
67 exit(helpmsg, 0)
68 }
69 }
7770
78 if CFG.Cache.MaxSize != 0 || CFG.Cache.Lifetime != 0 {
79 go InitCacheSystem()
80 }
71 if CFG.cfg != "" {
72 f, err := os.ReadFile(CFG.cfg)
73 try_with_exitstatus(err, 1)
74
75 try_with_exitstatus(json.Unmarshal(f, &CFG), 1)
76 if CFG.Cache.Enabled && !CFG.Proxy {
77 exit("Incompatible settings detected: cannot use caching media content without proxy", 1)
78 }
79
80 if CFG.Cache.MaxSize != 0 || CFG.Cache.Lifetime != 0 {
81 go InitCacheSystem()
8182 }
8283 }
8384}
app/parsers.go added +367
@@ -0,0 +1,367 @@
1package app
2
3import (
4 "encoding/json"
5 "strconv"
6 "strings"
7
8 "git.macaw.me/skunky/devianter"
9 "golang.org/x/net/html"
10)
11
12func (s skunkyart) ParseComments(c devianter.Comments) string {
13 var cmmts strings.Builder
14 replied := make(map[int]string)
15
16 cmmts.WriteString("<details><summary>Comments: <b>")
17 cmmts.WriteString(strconv.Itoa(c.Total))
18 cmmts.WriteString("</b></summary>")
19 for _, x := range c.Thread {
20 replied[x.ID] = x.User.Username
21 cmmts.WriteString(`<div class="msg`)
22 if x.Parent > 0 {
23 cmmts.WriteString(` reply`)
24 }
25 cmmts.WriteString(`"><p id="`)
26 cmmts.WriteString(strconv.Itoa(x.ID))
27 cmmts.WriteString(`"><img src="`)
28 cmmts.WriteString(UrlBuilder("media", "emojitar", x.User.Username, "?type=a"))
29 cmmts.WriteString(`" width="30px" height="30px"><a href="`)
30 cmmts.WriteString(UrlBuilder("group_user", "?q=", x.User.Username, "&type=a"))
31 cmmts.WriteString(`"><b`)
32 cmmts.WriteString(` class="`)
33 if x.User.Banned {
34 cmmts.WriteString(`banned`)
35 }
36 if x.Author {
37 cmmts.WriteString(`author`)
38 }
39 cmmts.WriteString(`">`)
40 cmmts.WriteString(x.User.Username)
41 cmmts.WriteString("</b></a> ")
42
43 if x.Parent > 0 {
44 cmmts.WriteString(` In reply to <a href="#`)
45 cmmts.WriteString(strconv.Itoa(x.Parent))
46 cmmts.WriteString(`">`)
47 if replied[x.Parent] == "" {
48 cmmts.WriteString("???")
49 } else {
50 cmmts.WriteString(replied[x.Parent])
51 }
52 cmmts.WriteString("</a>")
53 }
54 cmmts.WriteString(" [")
55 cmmts.WriteString(x.Posted.UTC().String())
56 cmmts.WriteString("]<p>")
57
58 cmmts.WriteString(ParseDescription(x.TextContent))
59 cmmts.WriteString("<p>👍: ")
60 cmmts.WriteString(strconv.Itoa(x.Likes))
61 cmmts.WriteString(" ⏩: ")
62 cmmts.WriteString(strconv.Itoa(x.Replies))
63 cmmts.WriteString("</p></div>\n")
64 }
65 cmmts.WriteString(s.NavBase(DeviationList{
66 Pages: 0,
67 More: c.HasMore,
68 }))
69 cmmts.WriteString("</details>")
70 return cmmts.String()
71}
72
73func (s skunkyart) DeviationList(devs []devianter.Deviation, content ...DeviationList) string {
74 var list strings.Builder
75 if s.Atom && s.Page > 1 {
76 s.ReturnHTTPError(400)
77 return ""
78 } else if s.Atom {
79 list.WriteString(`<?xml version="1.0" encoding="UTF-8"?><feed xmlns:media="http://search.yahoo.com/mrss/" xmlns="http://www.w3.org/2005/Atom">`)
80 list.WriteString(`<title>`)
81 if s.Type == 0 {
82 list.WriteString("Daily Deviations")
83 } else if len(devs) != 0 {
84 list.WriteString(devs[0].Author.Username)
85 } else {
86 list.WriteString("SkunkyArt")
87 }
88 list.WriteString(`</title>`)
89
90 list.WriteString(`<link rel="alternate" href="`)
91 list.WriteString(Host)
92 list.WriteString(`"/>`)
93 } else {
94 list.WriteString(`<div class="content">`)
95 }
96 for _, data := range devs {
97 if !(data.NSFW && !CFG.Nsfw) {
98 url := ParseMedia(data.Media)
99 if s.Atom {
100 id := strconv.Itoa(data.ID)
101 list.WriteString(`<entry><author><name>`)
102 list.WriteString(data.Author.Username)
103 list.WriteString(`</name></author><title>`)
104 list.WriteString(data.Title)
105 list.WriteString(`</title><link rel="alternate" type="text/html" href="`)
106 list.WriteString(UrlBuilder("post", data.Author.Username, "atom-"+id))
107 list.WriteString(`"/><id>`)
108 list.WriteString(id)
109 list.WriteString(`</id><published>`)
110 list.WriteString(data.PublishedTime.UTC().Format("Mon, 02 Jan 2006 15:04:05 -0700"))
111 list.WriteString(`</published>`)
112 list.WriteString(`<media:group><media:title>`)
113 list.WriteString(data.Title)
114 list.WriteString(`</media:title><media:thumbinal url="`)
115 list.WriteString(url)
116 list.WriteString(`"/></media:group><content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><a href="`)
117 list.WriteString(ConvertDeviantArtUrlToSkunkyArt(data.Url))
118 list.WriteString(`"><img src="`)
119 list.WriteString(url)
120 list.WriteString(`"/></a><p>`)
121 list.WriteString(ParseDescription(data.TextContent))
122 list.WriteString(`</p></div></content></entry>`)
123 } else {
124 list.WriteString(`<div class="block">`)
125 if url != "" {
126 list.WriteString(`<a title="open/download" href="`)
127 list.WriteString(url)
128 list.WriteString(`"><img loading="lazy" src="`)
129 list.WriteString(url)
130 list.WriteString(`" width="15%"></a>`)
131 } else {
132 list.WriteString(`<h1>[ TEXT ]</h1>`)
133 }
134 list.WriteString(`<br><a href="`)
135 list.WriteString(ConvertDeviantArtUrlToSkunkyArt(data.Url))
136 list.WriteString(`">`)
137 list.WriteString(data.Author.Username)
138 list.WriteString(" - ")
139 list.WriteString(data.Title)
140
141 // шильдики нсфв, аи и ежедневного поста
142 if data.NSFW {
143 list.WriteString(` [<span class="nsfw">NSFW</span>]`)
144 }
145 if data.AI {
146 list.WriteString(" [🤖]")
147 }
148 if data.DD {
149 list.WriteString(` [<span class="dd">DD</span>]`)
150 }
151
152 list.WriteString("</a></div>")
153 }
154 }
155 }
156
157 if s.Atom {
158 list.WriteString("</feed>")
159 s.Writer.Write([]byte(list.String()))
160 return ""
161 } else {
162 list.WriteString("</div>")
163 if content != nil {
164 list.WriteString(s.NavBase(content[0]))
165 }
166 }
167
168 return list.String()
169}
170
171/* DESCRIPTION/COMMENT PARSER */
172type text struct {
173 TXT string
174 TXT_RAW string
175 From int
176 To int
177}
178
179func ParseDescription(dscr devianter.Text) string {
180 var parseddescription strings.Builder
181 TagBuilder := func(content string, tags ...string) string {
182 l := len(tags)
183 for x := 0; x < l; x++ {
184 var htm strings.Builder
185 htm.WriteString("<")
186 htm.WriteString(tags[x])
187 htm.WriteString(">")
188
189 htm.WriteString(content)
190
191 htm.WriteString("</")
192 htm.WriteString(tags[x])
193 htm.WriteString(">")
194 content = htm.String()
195 }
196 return content
197 }
198 DeleteTrackingFromUrl := func(url string) string {
199 if len(url) > 42 && url[:42] == "https://www.deviantart.com/users/outgoing?" {
200 url = url[42:]
201 }
202 return url
203 }
204
205 if description, dl := dscr.Html.Markup, len(dscr.Html.Markup); dl != 0 &&
206 description[0] == '{' &&
207 description[dl-1] == '}' {
208 var descr struct {
209 Blocks []struct {
210 Text, Type string
211 InlineStyleRanges []struct {
212 Offset, Length int
213 Style string
214 }
215 EntityRanges []struct {
216 Offset, Length int
217 Key int
218 }
219 Data struct {
220 TextAlignment string
221 }
222 }
223 EntityMap map[string]struct {
224 Type string
225 Data struct {
226 Url string
227 Config struct {
228 Aligment string
229 Width int
230 }
231 Data devianter.Deviation
232 }
233 }
234 }
235 e := json.Unmarshal([]byte(description), &descr)
236 try(e)
237
238 entities := make(map[int]devianter.Deviation)
239 urls := make(map[int]string)
240 for n, x := range descr.EntityMap {
241 num, _ := strconv.Atoi(n)
242 if x.Data.Url != "" {
243 urls[num] = DeleteTrackingFromUrl(x.Data.Url)
244 }
245 entities[num] = x.Data.Data
246 }
247
248 for _, x := range descr.Blocks {
249 Styles := make([]text, len(x.InlineStyleRanges))
250
251 if len(x.InlineStyleRanges) != 0 {
252 var tags = make(map[int][]string)
253 for n, rngs := range x.InlineStyleRanges {
254 Styles := &Styles[n]
255 switch rngs.Style {
256 case "BOLD":
257 rngs.Style = "b"
258 case "UNDERLINE":
259 rngs.Style = "u"
260 case "ITALIC":
261 rngs.Style = "i"
262 }
263 Styles.From = rngs.Offset
264 Styles.To = rngs.Offset + rngs.Length
265 FT := Styles.From * Styles.To
266 tags[FT] = append(tags[FT], rngs.Style)
267 }
268 for n := 0; n < len(Styles); n++ {
269 Styles := &Styles[n]
270 Styles.TXT_RAW = x.Text[Styles.From:Styles.To]
271 Styles.TXT = TagBuilder(Styles.TXT_RAW, tags[Styles.From*Styles.To]...)
272 }
273 }
274
275 switch x.Type {
276 case "atomic":
277 d := entities[x.EntityRanges[0].Key]
278 parseddescription.WriteString(`<a href="`)
279 parseddescription.WriteString(ConvertDeviantArtUrlToSkunkyArt(d.Url))
280 parseddescription.WriteString(`"><img width="50%" src="`)
281 parseddescription.WriteString(ParseMedia(d.Media))
282 parseddescription.WriteString(`" title="`)
283 parseddescription.WriteString(d.Author.Username)
284 parseddescription.WriteString(" - ")
285 parseddescription.WriteString(d.Title)
286 parseddescription.WriteString(`"></a>`)
287 case "unstyled":
288 if l := len(Styles); l != 0 {
289 for n, r := range Styles {
290 var tag string
291 if x.Type == "header-two" {
292 tag = "h2"
293 }
294
295 parseddescription.WriteString(x.Text[:r.From])
296 if len(urls) != 0 && len(x.EntityRanges) != 0 {
297 ra := &x.EntityRanges[0]
298
299 parseddescription.WriteString(`<a target="_blank" href="`)
300 parseddescription.WriteString(urls[ra.Key])
301 parseddescription.WriteString(`">`)
302 parseddescription.WriteString(r.TXT)
303 parseddescription.WriteString(`</a>`)
304 } else if l > n+1 {
305 parseddescription.WriteString(r.TXT)
306 }
307 parseddescription.WriteString(TagBuilder(tag, x.Text[r.To:]))
308 }
309 } else {
310 parseddescription.WriteString(x.Text)
311 }
312 }
313 parseddescription.WriteString("<br>")
314 }
315 } else if dl != 0 {
316 for tt := html.NewTokenizer(strings.NewReader(dscr.Html.Markup)); ; {
317 switch tt.Next() {
318 case html.ErrorToken:
319 return parseddescription.String()
320 case html.StartTagToken, html.EndTagToken, html.SelfClosingTagToken:
321 token := tt.Token()
322 switch token.Data {
323 case "a":
324 for _, a := range token.Attr {
325 if a.Key == "href" {
326 url := DeleteTrackingFromUrl(a.Val)
327 parseddescription.WriteString(`<a target="_blank" href="`)
328 parseddescription.WriteString(url)
329 parseddescription.WriteString(`">`)
330 parseddescription.WriteString(GetValueOfTag(tt))
331 parseddescription.WriteString("</a> ")
332 }
333 }
334 case "img":
335 var uri, title string
336 for b, a := range token.Attr {
337 switch a.Key {
338 case "src":
339 if len(a.Val) > 9 && a.Val[8:9] == "e" {
340 uri = UrlBuilder("media", "emojitar", a.Val[37:len(a.Val)-4], "?type=e")
341 }
342 case "title":
343 title = a.Val
344 }
345 if title != "" {
346 for x := -1; x < b; x++ {
347 parseddescription.WriteString(`<img src="`)
348 parseddescription.WriteString(uri)
349 parseddescription.WriteString(`" title="`)
350 parseddescription.WriteString(title)
351 parseddescription.WriteString(`">`)
352 }
353 }
354 }
355 case "br", "li", "ul", "p", "b":
356 parseddescription.WriteString(token.String())
357 case "div":
358 parseddescription.WriteString("<p> ")
359 }
360 case html.TextToken:
361 parseddescription.Write(tt.Text())
362 }
363 }
364 }
365
366 return parseddescription.String()
367}
app/router.go +11 −16
@@ -4,11 +4,12 @@ import (
44 "io"
55 "net/http"
66 u "net/url"
7 "os"
87 "strconv"
98 "strings"
109)
1110
11var Host string
12
1213func Router() {
1314 parsepath := func(path string) map[int]string {
1415 if l := len(CFG.BasePath); len(path) > l {
@@ -43,18 +44,18 @@ func Router() {
4344
4445 // функция, что управляет всем
4546 handle := func(w http.ResponseWriter, r *http.Request) {
46 path := parsepath(r.URL.Path)
47 var wr = io.WriteString
48 open_n_send := func(name string) {
49 f, e := os.ReadFile(name)
50 err(e)
51 wr(w, string(f))
47 if h := r.Header["Scheme"]; len(h) != 0 && h[0] == "https" {
48 Host = h[0] + "://" + r.Host
49 } else {
50 Host = "http://" + r.Host
5251 }
5352
53 path := parsepath(r.URL.Path)
54
5455 // структура с функциями
5556 var skunky skunkyart
56 skunky.Args = r.URL.Query()
5757 skunky.Writer = w
58 skunky.Args = r.URL.Query()
5859 skunky.BasePath = CFG.BasePath
5960
6061 arg := skunky.Args.Get
@@ -95,18 +96,12 @@ func Router() {
9596 }
9697 case "about":
9798 skunky.About()
98 case "gui":
99 case "stylesheet":
99100 w.Header().Add("content-type", "text/css")
100 open_n_send(next(path, 2))
101 io.WriteString(w, Templates["css/skunky.css"])
101102 }
102103 }
103104
104105 http.HandleFunc("/", handle)
105106 http.ListenAndServe(CFG.Listen, nil)
106107}
107
108func err(e error) {
109 if e != nil {
110 println(e.Error())
111 }
112}
app/util.go +151 −469
@@ -2,7 +2,6 @@ package app
22
33import (
44 "encoding/base64"
5 "encoding/json"
65 "io"
76 "net/http"
87 u "net/url"
@@ -17,19 +16,36 @@ import (
1716 "golang.org/x/net/html"
1817)
1918
20// парсинг темплейтов
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
2136func (s skunkyart) ExecuteTemplate(file string, data any) {
2237 var buf strings.Builder
2338 tmp := template.New(file)
2439 tmp, e := tmp.Parse(Templates[file])
25 err(e)
26 err(tmp.Execute(&buf, &data))
40 try(e)
41 try(tmp.Execute(&buf, &data))
2742 wr(s.Writer, buf.String())
2843}
2944
3045func UrlBuilder(strs ...string) string {
3146 var str strings.Builder
3247 l := len(strs)
48 str.WriteString(Host)
3349 str.WriteString(CFG.BasePath)
3450 for n, x := range strs {
3551 str.WriteString(x)
@@ -45,7 +61,7 @@ func (s skunkyart) ReturnHTTPError(status int) {
4561
4662 var msg strings.Builder
4763 msg.WriteString(`<html><link rel="stylesheet" href="`)
48 msg.WriteString(UrlBuilder("gui", "css", "skunky.css"))
64 msg.WriteString(UrlBuilder("stylesheet"))
4965 msg.WriteString(`" /><h1>`)
5066 msg.WriteString(strconv.Itoa(status))
5167 msg.WriteString(" - ")
@@ -55,7 +71,131 @@ func (s skunkyart) ReturnHTTPError(status int) {
5571 wr(s.Writer, msg.String())
5672}
5773
58func (s skunkyart) ConvertDeviantArtUrlToSkunkyArt(url string) (output string) {
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 os.Mkdir(CFG.Cache.Path, 0700)
116 fname := CFG.Cache.Path + "/" + base64.StdEncoding.EncodeToString([]byte(subdomain+path))
117 file, e := os.Open(fname)
118
119 if e != nil {
120 dwnld := Download(url.String())
121 if dwnld.Status == 200 && dwnld.Headers["Content-Type"][0][:5] == "image" {
122 try(os.WriteFile(fname, dwnld.Body, 0700))
123 s.Writer.Write(dwnld.Body)
124 }
125 } else {
126 file, e := io.ReadAll(file)
127 try(e)
128 s.Writer.Write(file)
129 }
130 } else if CFG.Proxy {
131 dwnld := Download(url.String())
132 s.Writer.Write(dwnld.Body)
133 } else {
134 s.Writer.WriteHeader(403)
135 s.Writer.Write([]byte("Sorry, butt proxy on this instance are disabled."))
136 }
137}
138
139func InitCacheSystem() {
140 c := &CFG.Cache
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 n := dirname + "/" + x.Name()
179 file, e := os.ReadFile(n)
180 try_with_exitstatus(e, 1)
181 Templates[n] = string(file)
182 }
183 }
184}
185
186/* PARSING HELPERS */
187func ParseMedia(media devianter.Media) string {
188 url := devianter.UrlFromMedia(media)
189 if len(url) != 0 && CFG.Proxy {
190 url = url[21:]
191 dot := strings.Index(url, ".")
192
193 return UrlBuilder("media", "file", url[:dot], url[dot+11:])
194 }
195 return url
196}
197
198func ConvertDeviantArtUrlToSkunkyArt(url string) (output string) {
59199 if len(url) > 32 && url[27:32] != "stash" {
60200 url = url[27:]
61201 toart := strings.Index(url, "/art/")
@@ -78,13 +218,7 @@ func BuildUserPlate(name string) string {
78218 return htm.String()
79219}
80220
81type text struct {
82 TXT string
83 from int
84 to int
85}
86
87func tagval(t *html.Tokenizer) string {
221func GetValueOfTag(t *html.Tokenizer) string {
88222 for tt := t.Next(); ; {
89223 switch tt {
90224 default:
@@ -95,198 +229,14 @@ func tagval(t *html.Tokenizer) string {
95229 }
96230}
97231
98func ParseDescription(dscr devianter.Text) string {
99 var parseddescription strings.Builder
100 TagBuilder := func(tag string, content string) string {
101 if tag != "" {
102 var htm strings.Builder
103 htm.WriteString("<")
104 htm.WriteString(tag)
105 htm.WriteString(">")
106
107 htm.WriteString(content)
108
109 htm.WriteString("</")
110 htm.WriteString(tag)
111 htm.WriteString(">")
112 return htm.String()
113 }
114 return content
115 }
116 DeleteSpywareFromUrl := func(url string) string {
117 if len(url) > 42 && url[:42] == "https://www.deviantart.com/users/outgoing?" {
118 url = url[42:]
119 }
120 return url
121 }
122
123 if description, dl := dscr.Html.Markup, len(dscr.Html.Markup); dl != 0 &&
124 description[0] == '{' &&
125 description[dl-1] == '}' {
126 var descr struct {
127 Blocks []struct {
128 Text, Type string
129 InlineStyleRanges []struct {
130 Offset, Length int
131 Style string
132 }
133 EntityRanges []struct {
134 Offset, Length int
135 Key int
136 }
137 Data struct {
138 TextAlignment string
139 }
140 }
141 EntityMap map[string]struct {
142 Type string
143 Data struct {
144 Url string
145 Config struct {
146 Aligment string
147 Width int
148 }
149 Data devianter.Deviation
150 }
151 }
152 }
153 e := json.Unmarshal([]byte(description), &descr)
154 err(e)
155
156 entities := make(map[int]devianter.Deviation)
157 urls := make(map[int]string)
158 for n, x := range descr.EntityMap {
159 num, _ := strconv.Atoi(n)
160 if x.Data.Url != "" {
161 urls[num] = DeleteSpywareFromUrl(x.Data.Url)
162 }
163 entities[num] = x.Data.Data
164 }
165
166 for _, x := range descr.Blocks {
167 ranges := make(map[int]text)
168
169 for i, rngs := range x.InlineStyleRanges {
170 var tag string
171
172 switch rngs.Style {
173 case "BOLD":
174 tag = "b"
175 case "UNDERLINE":
176 tag = "u"
177 case "ITALIC":
178 tag = "i"
179 }
180
181 fromto := rngs.Offset + rngs.Length
182 ranges[i] = text{
183 TXT: TagBuilder(tag, x.Text[rngs.Offset:fromto]),
184 from: rngs.Offset,
185 to: fromto,
186 }
187 }
188
189 switch x.Type {
190 case "atomic":
191 d := entities[x.EntityRanges[0].Key]
192 parseddescription.WriteString(`<img width="50%" src="`)
193 parseddescription.WriteString(ParseMedia(d.Media))
194 parseddescription.WriteString(`" title="`)
195 parseddescription.WriteString(d.Author.Username)
196 parseddescription.WriteString(" - ")
197 parseddescription.WriteString(d.Title)
198 parseddescription.WriteString(`">`)
199 case "unstyled":
200 if len(ranges) != 0 {
201 for _, r := range ranges {
202 var tag string
203 switch x.Type {
204 case "header-two":
205 tag = "h2"
206 }
207
208 parseddescription.WriteString(x.Text[:r.from])
209 if len(urls) != 0 && len(x.EntityRanges) != 0 {
210 ra := &x.EntityRanges[0]
211
212 parseddescription.WriteString(`<a target="_blank" href="`)
213 parseddescription.WriteString(urls[ra.Key])
214 parseddescription.WriteString(`">`)
215 parseddescription.WriteString(r.TXT)
216 parseddescription.WriteString(`</a>`)
217 } else {
218 parseddescription.WriteString(r.TXT)
219 }
220 parseddescription.WriteString(TagBuilder(tag, x.Text[r.to:]))
221 }
222 } else {
223 parseddescription.WriteString(x.Text)
224 }
225 }
226 parseddescription.WriteString("<br>")
227 }
228 } else if dl != 0 {
229 for tt := html.NewTokenizer(strings.NewReader(dscr.Html.Markup)); ; {
230 switch tt.Next() {
231 case html.ErrorToken:
232 return parseddescription.String()
233 case html.StartTagToken, html.EndTagToken, html.SelfClosingTagToken:
234 token := tt.Token()
235 switch token.Data {
236 case "a":
237 for _, a := range token.Attr {
238 if a.Key == "href" {
239 url := DeleteSpywareFromUrl(a.Val)
240 parseddescription.WriteString(`<a target="_blank" href="`)
241 parseddescription.WriteString(url)
242 parseddescription.WriteString(`">`)
243 parseddescription.WriteString(tagval(tt))
244 parseddescription.WriteString("</a> ")
245 }
246 }
247 case "img":
248 var uri, title string
249 for b, a := range token.Attr {
250 switch a.Key {
251 case "src":
252 if len(a.Val) > 9 && a.Val[8:9] == "e" {
253 uri = UrlBuilder("media", "emojitar", a.Val[37:len(a.Val)-4], "?type=e")
254 }
255 case "title":
256 title = a.Val
257 }
258 if title != "" {
259 for x := -1; x < b; x++ {
260 parseddescription.WriteString(`<img src="`)
261 parseddescription.WriteString(uri)
262 parseddescription.WriteString(`" title="`)
263 parseddescription.WriteString(title)
264 parseddescription.WriteString(`">`)
265 }
266 }
267 }
268 case "br", "li", "ul", "p", "b":
269 parseddescription.WriteString(token.String())
270 case "div":
271 parseddescription.WriteString("<p> ")
272 }
273 case html.TextToken:
274 parseddescription.Write(tt.Text())
275 }
276 }
277 }
278
279 return parseddescription.String()
280}
281
282232// навигация по страницам
283type dlist struct {
233type DeviationList struct {
284234 Pages int
285235 More bool
286236}
287237
288238// FIXME: на некоротрых артах первая страница может вызывать полное отсутствие панели навигации.
289func (s skunkyart) NavBase(c dlist) string {
239func (s skunkyart) NavBase(c DeviationList) string {
290240 // TODO: сделать понятнее
291241 // навигация по страницам
292242 var list strings.Builder
@@ -333,7 +283,7 @@ func (s skunkyart) NavBase(c dlist) string {
333283 }
334284
335285 // вперёд
336 for x := p; x <= p+6; x++ {
286 for x := p; x <= p+6 && c.Pages > p+6; x++ {
337287 if x == p {
338288 prevrev("", x, true)
339289 x++
@@ -346,277 +296,9 @@ func (s skunkyart) NavBase(c dlist) string {
346296 }
347297
348298 // вперёд-назад
349 if p != 417 || c.More {
299 if c.More {
350300 prevrev("| Next =>", p+1, false)
351301 }
352302
353303 return list.String()
354304}
355
356func (s skunkyart) DeviationList(devs []devianter.Deviation, content ...dlist) string {
357 var list strings.Builder
358 if s.Atom && s.Page > 1 {
359 s.ReturnHTTPError(400)
360 return ""
361 } else if s.Atom {
362 list.WriteString(`<?xml version="1.0" encoding="UTF-8"?><feed xmlns:media="http://search.yahoo.com/mrss/" xmlns="http://www.w3.org/2005/Atom">`)
363 list.WriteString(`<title>SkunkyArt</title>`)
364 // list.WriteString(`<link rel="alternate" href="HOMEPAGE_URL"/><link href="FEED_URL" rel="self"/>`)
365 } else {
366 list.WriteString(`<div class="content">`)
367 }
368 for _, data := range devs {
369 if !(data.NSFW && !CFG.Nsfw) {
370 url := ParseMedia(data.Media)
371 if s.Atom {
372 id := strconv.Itoa(data.ID)
373 list.WriteString(`<entry><author><name>`)
374 list.WriteString(data.Author.Username)
375 list.WriteString(`</name></author><title>`)
376 list.WriteString(data.Title)
377 list.WriteString(`</title><link rel="alternate" type="text/html" href="`)
378 list.WriteString(UrlBuilder("post", data.Author.Username, "atom-"+id))
379 list.WriteString(`"/><id>`)
380 list.WriteString(id)
381 list.WriteString(`</id><published>`)
382 list.WriteString(data.PublishedTime.UTC().Format("Mon, 02 Jan 2006 15:04:05 -0700"))
383 list.WriteString(`</published>`)
384 list.WriteString(`<media:group><media:title>`)
385 list.WriteString(data.Title)
386 list.WriteString(`</media:title><media:thumbinal url="`)
387 list.WriteString(url)
388 list.WriteString(`"/></media:group><content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><a href="`)
389 list.WriteString(data.Url)
390 list.WriteString(`"><img src="`)
391 list.WriteString(url)
392 list.WriteString(`"/></a><p>`)
393 list.WriteString(ParseDescription(data.TextContent))
394 list.WriteString(`</p></div></content></entry>`)
395 } else {
396 list.WriteString(`<div class="block">`)
397 if url != "" {
398 list.WriteString(`<a title="open/download" href="`)
399 list.WriteString(url)
400 list.WriteString(`"><img loading="lazy" src="`)
401 list.WriteString(url)
402 list.WriteString(`" width="15%"></a>`)
403 } else {
404 list.WriteString(`<h1>[ TEXT ]</h1>`)
405 }
406 list.WriteString(`<br><a href="`)
407 list.WriteString(s.ConvertDeviantArtUrlToSkunkyArt(data.Url))
408 list.WriteString(`">`)
409 list.WriteString(data.Author.Username)
410 list.WriteString(" - ")
411 list.WriteString(data.Title)
412
413 // шильдики нсфв, аи и ежедневного поста
414 if data.NSFW {
415 list.WriteString(` [<span class="nsfw">NSFW</span>]`)
416 }
417 if data.AI {
418 list.WriteString(" [🤖]")
419 }
420 if data.DD {
421 list.WriteString(` [<span class="dd">DD</span>]`)
422 }
423
424 list.WriteString("</a></div>")
425 }
426 }
427 }
428
429 if s.Atom {
430 list.WriteString("</feed>")
431 s.Writer.Write([]byte(list.String()))
432 return ""
433 } else {
434 list.WriteString("</div>")
435 if content != nil {
436 list.WriteString(s.NavBase(content[0]))
437 }
438 }
439
440 return list.String()
441}
442
443func (s skunkyart) ParseComments(c devianter.Comments) string {
444 var cmmts strings.Builder
445 replied := make(map[int]string)
446
447 cmmts.WriteString("<details><summary>Comments: <b>")
448 cmmts.WriteString(strconv.Itoa(c.Total))
449 cmmts.WriteString("</b></summary>")
450 for _, x := range c.Thread {
451 replied[x.ID] = x.User.Username
452 cmmts.WriteString(`<div class="msg`)
453 if x.Parent > 0 {
454 cmmts.WriteString(` reply`)
455 }
456 cmmts.WriteString(`"><p id="`)
457 cmmts.WriteString(strconv.Itoa(x.ID))
458 cmmts.WriteString(`"><img src="`)
459 cmmts.WriteString(UrlBuilder("media", "emojitar", x.User.Username, "?type=a"))
460 cmmts.WriteString(`" width="30px" height="30px"><a href="`)
461 cmmts.WriteString(UrlBuilder("group_user", "?q=", x.User.Username, "&type=a"))
462 cmmts.WriteString(`"><b`)
463 cmmts.WriteString(` class="`)
464 if x.User.Banned {
465 cmmts.WriteString(`banned`)
466 }
467 if x.Author {
468 cmmts.WriteString(`author`)
469 }
470 cmmts.WriteString(`">`)
471 cmmts.WriteString(x.User.Username)
472 cmmts.WriteString("</b></a> ")
473
474 if x.Parent > 0 {
475 cmmts.WriteString(` In reply to <a href="#`)
476 cmmts.WriteString(strconv.Itoa(x.Parent))
477 cmmts.WriteString(`">`)
478 if replied[x.Parent] == "" {
479 cmmts.WriteString("???")
480 } else {
481 cmmts.WriteString(replied[x.Parent])
482 }
483 cmmts.WriteString("</a>")
484 }
485 cmmts.WriteString(" [")
486 cmmts.WriteString(x.Posted.UTC().String())
487 cmmts.WriteString("]<p>")
488
489 cmmts.WriteString(ParseDescription(x.TextContent))
490 cmmts.WriteString("<p>👍: ")
491 cmmts.WriteString(strconv.Itoa(x.Likes))
492 cmmts.WriteString(" ⏩: ")
493 cmmts.WriteString(strconv.Itoa(x.Replies))
494 cmmts.WriteString("</p></div>\n")
495 }
496 cmmts.WriteString(s.NavBase(dlist{
497 Pages: 0,
498 More: c.HasMore,
499 }))
500 cmmts.WriteString("</details>")
501 return cmmts.String()
502}
503
504func ParseMedia(media devianter.Media) string {
505 url := devianter.UrlFromMedia(media)
506 if len(url) != 0 {
507 url = url[21:]
508 dot := strings.Index(url, ".")
509
510 return UrlBuilder("media", "file", url[:dot], "/", url[dot+10:])
511 }
512 return ""
513}
514
515func (s skunkyart) DownloadAndSendMedia(subdomain, path string) {
516 var url strings.Builder
517 url.WriteString("https://images-wixmp-")
518 url.WriteString(subdomain)
519 url.WriteString(".wixmp.com/")
520 url.WriteString(path)
521 url.WriteString("?token=")
522 url.WriteString(s.Args.Get("token"))
523
524 download := func() (body []byte, status int, headers http.Header) {
525 cli := &http.Client{}
526 if CFG.WixmpProxy != "" {
527 u, e := u.Parse(CFG.WixmpProxy)
528 err(e)
529 cli.Transport = &http.Transport{Proxy: http.ProxyURL(u)}
530 }
531
532 req, e := http.NewRequest("GET", url.String(), nil)
533 err(e)
534 req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0.0")
535
536 resp, e := cli.Do(req)
537 err(e)
538 defer resp.Body.Close()
539
540 b, e := io.ReadAll(resp.Body)
541 err(e)
542 return b, resp.StatusCode, resp.Header
543 }
544
545 if CFG.Cache.Enabled {
546 os.Mkdir(CFG.Cache.Path, 0700)
547 fname := CFG.Cache.Path + "/" + base64.StdEncoding.EncodeToString([]byte(subdomain+path))
548 file, e := os.Open(fname)
549
550 if e != nil {
551 b, status, headers := download()
552 if status == 200 && headers["Content-Type"][0][:5] == "image" {
553 err(os.WriteFile(fname, b, 0700))
554 s.Writer.Write(b)
555 }
556 } else {
557 file, e := io.ReadAll(file)
558 err(e)
559 s.Writer.Write(file)
560 }
561 } else if CFG.Proxy {
562 b, _, _ := download()
563 s.Writer.Write(b)
564 } else {
565 s.Writer.WriteHeader(403)
566 s.Writer.Write([]byte("Sorry, butt proxy on this instance disabled."))
567 }
568}
569
570func InitCacheSystem() {
571 c := &CFG.Cache
572 for {
573 dir, e := os.Open(c.Path)
574 err(e)
575 stat, e := dir.Stat()
576 err(e)
577
578 dirnames, e := dir.Readdirnames(-1)
579 err(e)
580 for _, a := range dirnames {
581 a = c.Path + "/" + a
582 rm := func() {
583 err(os.RemoveAll(a))
584 }
585 if c.Lifetime != 0 {
586 now := time.Now().UnixMilli()
587
588 f, _ := os.Stat(a)
589 stat := f.Sys().(*syscall.Stat_t)
590 time := time.Unix(stat.Ctim.Unix()).UnixMilli()
591
592 if time+c.Lifetime <= now {
593 rm()
594 }
595 }
596 if c.MaxSize != 0 && stat.Size() > c.MaxSize {
597 rm()
598 }
599 }
600
601 dir.Close()
602 time.Sleep(time.Second * time.Duration(CFG.Cache.UpdateInterval))
603 }
604}
605
606func CopyTemplatesToMemory() {
607 try := func(e error) {
608 if e != nil {
609 panic(e.Error())
610 }
611 }
612
613 dir, e := os.ReadDir(CFG.TemplatesDir)
614 try(e)
615
616 for _, x := range dir {
617 n := CFG.TemplatesDir + "/" + x.Name()
618 file, e := os.ReadFile(n)
619 try(e)
620 Templates[n] = string(file)
621 }
622}
app/wrapper.go +47 −26
@@ -1,6 +1,7 @@
11package app
22
33import (
4 "encoding/json"
45 "io"
56 "net/http"
67 "net/url"
@@ -17,17 +18,33 @@ var wr = io.WriteString
1718var Templates = make(map[string]string)
1819
1920type skunkyart struct {
20 Writer http.ResponseWriter
21 Writer http.ResponseWriter
22
2123 Args url.Values
2224 BasePath string
2325 Type rune
2426 Query, QueryRaw string
2527 Page int
2628 Atom bool
27 Templates struct {
29
30 Templates struct {
2831 About struct {
29 Proxy bool
30 Nsfw bool
32 Proxy bool
33 Nsfw bool
34 Instances []struct {
35 Title string
36 Country string
37 Urls []struct {
38 I2P string `json:"i2p"`
39 Ygg string
40 Tor string
41 Clearnet string
42 }
43 Settings struct {
44 Nsfw bool
45 Proxy bool
46 }
47 }
3148 }
3249
3350 SomeList string
@@ -127,7 +144,7 @@ func (s skunkyart) GRUser() {
127144
128145 case "cover_deviation":
129146 group.About.BGMeta = x.ModuleData.CoverDeviation.Deviation
130 group.About.BGMeta.Url = s.ConvertDeviantArtUrlToSkunkyArt(group.About.BGMeta.Url)
147 group.About.BGMeta.Url = ConvertDeviantArtUrlToSkunkyArt(group.About.BGMeta.Url)
131148 group.About.BG = ParseMedia(group.About.BGMeta.Media)
132149 case "group_admins":
133150 var htm strings.Builder
@@ -146,7 +163,7 @@ func (s skunkyart) GRUser() {
146163
147164 gallery := g.Gallery(s.Page, folderid)
148165 if folderid > 0 {
149 group.Gallery.List = s.DeviationList(gallery.Content.Results, dlist{
166 group.Gallery.List = s.DeviationList(gallery.Content.Results, DeviationList{
150167 More: gallery.Content.HasMore,
151168 })
152169 } else {
@@ -157,13 +174,18 @@ func (s skunkyart) GRUser() {
157174 for _, x := range x.ModuleData.Folders.Results {
158175 folders.WriteString(`<div class="block folder-item">`)
159176
160 folders.WriteString(`<a href="`)
161 folders.WriteString(s.ConvertDeviantArtUrlToSkunkyArt(x.Thumb.Url))
162 folders.WriteString(`"><img loading="lazy" src="`)
163 folders.WriteString(ParseMedia(x.Thumb.Media))
164 folders.WriteString(`" title="`)
165 folders.WriteString(x.Thumb.Title)
166 folders.WriteString(`"></a><br>`)
177 if !(x.Thumb.NSFW && !CFG.Nsfw) {
178 folders.WriteString(`<a href="`)
179 folders.WriteString(ConvertDeviantArtUrlToSkunkyArt(x.Thumb.Url))
180 folders.WriteString(`"><img loading="lazy" src="`)
181 folders.WriteString(ParseMedia(x.Thumb.Media))
182 folders.WriteString(`" title="`)
183 folders.WriteString(x.Thumb.Title)
184 folders.WriteString(`"></a>`)
185 } else {
186 folders.WriteString(`<h1>[ <span class="nsfw">NSFW</span> ]</h1>`)
187 }
188 folders.WriteString("<br>")
167189
168190 folders.WriteString(`<a href="?folder=`)
169191 folders.WriteString(strconv.Itoa(x.FolderId))
@@ -182,7 +204,7 @@ func (s skunkyart) GRUser() {
182204 }
183205
184206 if x.Name == "folder_deviations" {
185 group.Gallery.List = s.DeviationList(x.ModuleData.Folder.Deviations, dlist{
207 group.Gallery.List = s.DeviationList(x.ModuleData.Folder.Deviations, DeviationList{
186208 Pages: x.ModuleData.Folder.Pages,
187209 More: x.ModuleData.Folder.HasMore,
188210 })
@@ -217,7 +239,7 @@ func (s skunkyart) Deviation(author, postname string) {
217239 post.Post.IMG = ParseMedia(post.Post.Deviation.Media)
218240 for _, x := range post.Post.Deviation.Extended.RelatedContent {
219241 if len(x.Deviations) != 0 {
220 post.Related = s.DeviationList(x.Deviations)
242 post.Related += s.DeviationList(x.Deviations)
221243 }
222244 }
223245
@@ -247,7 +269,7 @@ func (s skunkyart) Deviation(author, postname string) {
247269
248270func (s skunkyart) DD() {
249271 dd := devianter.DailyDeviationsFunc(s.Page)
250 s.Templates.SomeList = s.DeviationList(dd.Deviations, dlist{
272 s.Templates.SomeList = s.DeviationList(dd.Deviations, DeviationList{
251273 Pages: 0,
252274 More: dd.HasMore,
253275 })
@@ -281,17 +303,13 @@ func (s skunkyart) Search() {
281303 url.WriteString(strconv.Itoa(10 * s.Page))
282304 }
283305
284 r, err := http.Get(url.String())
285 if err != nil {
286 s.ReturnHTTPError(502)
287 }
288 defer r.Body.Close()
306 dwnld := Download(url.String())
289307
290 for z := html.NewTokenizer(r.Body); ; {
308 for z := html.NewTokenizer(strings.NewReader(string(dwnld.Body))); ; {
291309 if n, token := z.Next(), z.Token(); n == html.StartTagToken && token.Data == "a" {
292310 for _, x := range token.Attr {
293311 if x.Key == "class" && x.Val == "u regular username" {
294 usernames[num] = tagval(z)
312 usernames[num] = GetValueOfTag(z)
295313 num++
296314 }
297315 }
@@ -308,15 +326,17 @@ func (s skunkyart) Search() {
308326 ss.List += BuildUserPlate(usernames[x])
309327 }
310328 ss.List += `</div>`
311 ss.List += s.NavBase(dlist{})
329 ss.List += s.NavBase(DeviationList{
330 More: true,
331 })
312332 }
313333 default:
314334 s.ReturnHTTPError(400)
315335 }
316 err(e)
336 try(e)
317337
318338 if s.Type != 'r' {
319 ss.List = s.DeviationList(ss.Content.Results, dlist{
339 ss.List = s.DeviationList(ss.Content.Results, DeviationList{
320340 Pages: ss.Content.Pages,
321341 More: ss.Content.HasMore,
322342 })
@@ -340,5 +360,6 @@ func (s skunkyart) Emojitar(name string) {
340360func (s skunkyart) About() {
341361 s.Templates.About.Nsfw = CFG.Nsfw
342362 s.Templates.About.Proxy = CFG.Proxy
363 try(json.Unmarshal([]byte(Templates["instances.json"]), &s.Templates.About))
343364 s.ExecuteTemplate("html/about.htm", &s)
344365}
config.example.json +6 −3
@@ -8,8 +8,11 @@
88 "max-size": 100000,
99 "update-interval": 5
1010 },
11 "templates-dir": "html",
12 "wixmp-proxy": "http://127.0.0.1:8080",
11 "dirs-to-memory": [
12 "html",
13 "css"
14 ],
15 "download-proxy": "",
1316 "proxy": true,
14 "nsfw": true
17 "nsfw": false
1518}
\ No newline at end of file
css/skunky.css +57 −45
@@ -1,5 +1,6 @@
1/* TAGS */
12html {
2 font-family: ubuntu;
3 font-family: Ubuntu;
34 background-color:black;
45 color: rgb(234, 216, 216);
56}
@@ -8,7 +9,7 @@ a {
89 color: cadetblue;
910}
1011a:hover {
11 color: yellow;
12 color: #d0ff00;
1213 transition: 400ms;
1314}
1415header h1 {
@@ -28,37 +29,8 @@ form input, button, select {
2829 border: 0px;
2930 border-radius: 1px;
3031}
31.nsfw, .about-false {
32 color: red;
33}
34.about-true {
35 color: green;
36}
37.author {
38 color: seagreen;
39}
40.msg {
41 background-color: #091f19;
42 color: whitesmoke;
43 width: fit-content;
44 max-width: 90%;
45 padding: 4px;
46 border-radius: 2px;
47 margin-top: 6px;
48 text-wrap: pretty;
49 transition: 350ms;
50}
51.msg:hover {
52 background-color: #134134;
53}
54.reply {
55 border-radius: 0px 2px 2px 0px;
56 border-left: #258268 solid;
57 margin-left: 40px;
58}
59.dd {
60 color: rgb(160, 0, 147);
61}
32
33/* BLOCKS */
6234.content {
6335 align-items: center;
6436 border-radius: 3px;
@@ -82,29 +54,43 @@ form input, button, select {
8254 border: 3px solid #4d27d6;
8355 transition: 400ms;
8456}
85.block img, .plates .user-plate img{
57.block img, .plates .user-plate img {
8658 width: 100%;
8759}
8860.block p {
8961 word-break: break-all;
9062}
91.ubg {
92 display: flex;
93 flex-wrap: wrap;
94 justify-content: center;
63
64/* MESSAGES */
65.msg {
66 background-color: #091f19;
67 color: whitesmoke;
68 width: fit-content;
69 max-width: 90%;
70 padding: 4px;
71 border-radius: 2px;
72 margin-top: 6px;
73 text-wrap: pretty;
74 transition: 350ms;
9575}
96.ubg img {
97 width: 20%;
76.msg:hover {
77 background-color: #134134;
9878}
99.folders {
100 display: flex
79.reply {
80 border-radius: 0px 2px 2px 0px;
81 border-left: #258268 solid;
82 margin-left: 40px;
83}
84
85/* FOLDER & PLATES */
86.folders, .plates {
87 display: flex;
88 flex-wrap: wrap;
10189}
10290.folder-item {
10391 background-color: #060820;
10492 border: 3px solid #060820;
105}
106.plates {
107 display: flex
93 width: 10%
10894}
10995.admins .user-plate {
11096 width: 5%;
@@ -112,6 +98,7 @@ form input, button, select {
11298}
11399.plates .user-plate {
114100 margin-left: 1%;
101 margin-bottom: 1%;
115102 background-color: #091f19;
116103 padding: 3px;
117104 word-break: break-all;
@@ -119,6 +106,31 @@ form input, button, select {
119106 max-width: 15%;
120107}
121108
109/* USER/GROUP BACKGROUNDS */
110.ubg {
111 display: flex;
112 flex-wrap: wrap;
113 justify-content: center;
114}
115.ubg img {
116 width: 20%;
117}
118
119/* COLORS */
120.nsfw, .about-false {
121 color: red;
122}
123.about-true {
124 color: green;
125}
126.author {
127 color: seagreen;
128}
129.dd {
130 color: rgb(160, 0, 147);
131}
132
133/* SCREEN OPTIMISATIONS */
122134@media screen and (orientation: portrait) {
123135 header {
124136 scale: 155%;
gowna deleted
html/about.htm +34 −1
@@ -2,7 +2,7 @@
22<html>
33 <head>
44 <title>SkunkyArt</title>
5 <link rel="stylesheet" href="{{.BasePath}}gui/css/skunky.css">
5 <link rel="stylesheet" href="{{.BasePath}}stylesheet">
66 </head>
77 <main>
88 <header>
@@ -20,11 +20,44 @@
2020 <p>
2121 SkunkyArt is an alternative frontend for deviantart.com, written in Go.
2222 </p>
23 <h3><a href="https://go.kde.org/matrix/#/#skunkyart:ebloid.ru" target="_blank">Room in Matrix</a></h3>
2324 <h2>Instance settings:</h2>
2425 <ul>
2526 <li><b>NSFW</b>: <span class="about-{{.Templates.About.Nsfw}}">{{if .Templates.About.Nsfw}}YES{{else}}NO{{end}}</span></li>
2627 <li><b>Proxyfing</b>: <span class="about-{{.Templates.About.Proxy}}">{{if .Templates.About.Proxy}}YES{{else}}NO{{end}}</span></li>
2728 </ul>
29 <h2>Instances:</h2>
30 <ul>
31 {{range .Templates.About.Instances}}
32 <li><u><b>{{.Title}}</b></u>:
33 <ul>
34 <li><b>Country</b>: {{.Country}}</li>
35 <li><b>URLs</b>: </li>
36 <ul>
37 {{range .Urls}}
38 {{if ne .I2P ""}}
39 <li><b>I2P</b>: <a href="{{.I2P}}">Yes</a></li>
40 {{end}}
41 {{if ne .Ygg ""}}
42 <li><b>Ygg</b>: <a href="{{.Ygg}}">Yes</a></li>
43 {{end}}
44 {{if ne .Tor ""}}
45 <li><b>Tor</b>: <a href="{{.Tor}}">Yes</a></li>
46 {{end}}
47 {{if ne .Clearnet ""}}
48 <li><b>Clearnet</b>: <a href="{{.Clearnet}}">{{.Clearnet}}</a></li>
49 {{end}}
50 {{end}}
51 </ul>
52 <li><b>Settings</b>: </li>
53 <ul>
54 <li><b>NSFW</b>: <span class="about-{{.Settings.Nsfw}}">{{if .Settings.Nsfw}}YES{{else}}NO{{end}}</span></li>
55 <li><b>Proxyfing</b>: <span class="about-{{.Settings.Proxy}}">{{if .Settings.Proxy}}YES{{else}}NO{{end}}</span></li>
56 </ul>
57 </ul>
58 </li>
59 {{end}}
60 </ul>
2861 <p>Copyright <a href="https://go.kde.org/matrix/#/@softpigeones:ebloid.ru" target="_blank">lost+skunk</a>, X11. <a href="https://git.macaw.me/skunky/skunkyart/src/tag/v1.3" target="_blank">SkunkyArt v1.3</a></p>
2962 </main>
3063</html>
\ No newline at end of file
html/deviantion.htm +2 −2
@@ -2,7 +2,7 @@
22<html>
33 <head>
44 <title>SkunkyArt | {{.Templates.Deviation.Post.Deviation.Author.Username}} - {{.Templates.Deviation.Post.Deviation.Title}}</title>
5 <link rel="stylesheet" href="{{.BasePath}}gui/css/skunky.css">
5 <link rel="stylesheet" href="{{.BasePath}}stylesheet">
66 <meta name="referrer" content="no-referrer" />
77 </head>
88 <main>
@@ -34,7 +34,7 @@
3434 {{if (ne .Templates.Deviation.Tags "")}}
3535 {{.Templates.Deviation.Tags}}<br>
3636 {{end}}
37 <span>Published: <strong>{{.Templates.Deviation.StringTime}}</strong>; Views: <strong>{{.Templates.Deviation.Post.Deviation.Stats.Views}}</strong>; Favourites: <strong>{{.Templates.Deviation.Post.Deviation.Stats.Favourites}}</strong>; Downloads: <strong>{{.Templates.Deviation.Post.Deviation.Stats.Downloads}}</strong><span>
37 <span>Published: <strong>{{.Templates.Deviation.StringTime}}</strong>; Views: <strong>{{.Templates.Deviation.Post.Deviation.Stats.Views}}</strong>; Favourites: <strong>{{.Templates.Deviation.Post.Deviation.Stats.Favourites}}</strong>; Downloads: <strong>{{.Templates.Deviation.Post.Deviation.Stats.Downloads}}</strong>
3838 <br><a target="_blank" href="https://www.deviantart.com/{{.Templates.Deviation.Post.Deviation.Author.Username}}/art/art-{{.Templates.Deviation.Post.Deviation.ID}}">Redirect to original</a>
3939 </span>
4040 {{if (ne .Templates.Deviation.Post.Description "")}}
html/gruser.htm +1 −1
@@ -8,7 +8,7 @@
88 gallery of {{.Templates.GroupUser.GR.Owner.Username}}
99 {{end}}
1010 </title>
11 <link rel="stylesheet" href="{{.BasePath}}gui/css/skunky.css">
11 <link rel="stylesheet" href="{{.BasePath}}stylesheet">
1212 </head>
1313 <main>
1414 <header>
html/index.htm +1 −1
@@ -2,7 +2,7 @@
22<html>
33 <head>
44 <title>SkunkyArt</title>
5 <link rel="stylesheet" href="{{.}}gui/css/skunky.css"/>
5 <link rel="stylesheet" href="{{.}}stylesheet"/>
66 </head>
77 <main>
88 <center>
html/list.htm +1 −1
@@ -2,7 +2,7 @@
22<html>
33 <head>
44 <title>SkunkyArt</title>
5 <link rel="stylesheet" href="{{.BasePath}}gui/css/skunky.css">
5 <link rel="stylesheet" href="{{.BasePath}}stylesheet">
66 </head>
77 <main>
88 <header>
html/search.htm +1 −1
@@ -2,7 +2,7 @@
22<html>
33 <head>
44 <title>SkunkyArt | Search "{{.QueryRaw}}"</title>
5 <link rel="stylesheet" href="{{.BasePath}}gui/css/skunky.css">
5 <link rel="stylesheet" href="{{.BasePath}}stylesheet">
66 </head>
77 <main>
88 <header>
instances.json +46 −11
@@ -1,14 +1,49 @@
11{
2 "instances": [{
3 "urls": [{
4 "i2p": "http://skunky.i2p/art",
5 "tor": "http://skunky0wjkf8j8ajofgh98aOZjhu8f.onion/art",
6 "ygg": "http://[324:71e:281a:9ed3::fa11]/skunkyart",
7 "clearnet": "https://skunky.net/art"
8 }],
9 "settings": {
10 "nsfw": true,
11 "proxy": true
2 "instances": [
3 {
4 "title": "skunky.ebloid.ru",
5 "country": "Russia",
6 "urls": [{
7 "ygg": "http://[201:eba5:d1fc:bf7b:cfcb:a811:4b8b:7ea3]/art",
8 "clearnet": "https://skunky.ebloid.ru/art"
9 }],
10 "settings": {
11 "nsfw": false,
12 "proxy": false
13 }
14 },
15 {
16 "title": "clovius.club",
17 "country": "Sweden",
18 "urls": [{
19 "clearnet": "https://skunky.clovius.club"
20 }],
21 "settings": {
22 "nsfw": true,
23 "proxy": true
24 }
25 },
26 {
27 "title": "bloat.cat",
28 "country": "Romania",
29 "urls": [{
30 "clearnet": "https://skunky.bloat.cat"
31 }],
32 "settings": {
33 "nsfw": true,
34 "proxy": true
35 }
36 },
37 {
38 "title": "frontendfriendly.xyz",
39 "country": "Finland",
40 "urls": [{
41 "clearnet": "https://skunkyart.frontendfriendly.xyz"
42 }],
43 "settings": {
44 "nsfw": true,
45 "proxy": true
46 }
1247 }
13 }]
48 ]
1449}
\ No newline at end of file