Commit 6f872fe2f7
Unsigned
Layout: unified · split
app/config.go added +48
| @@ -0,0 +1,48 @@ | ||
| 1 | package app | |
| 2 | ||
| 3 | import ( | |
| 4 | "os" | |
| 5 | ) | |
| 6 | ||
| 7 | type cache_config struct { | |
| 8 | Enabled bool | |
| 9 | Path string | |
| 10 | Max_size, Lifetime int64 | |
| 11 | } | |
| 12 | ||
| 13 | type config struct { | |
| 14 | cfg string | |
| 15 | Listen, Base_uri string | |
| 16 | Cache cache_config | |
| 17 | Proxy, Nsfw bool | |
| 18 | } | |
| 19 | ||
| 20 | var CFG = config{ | |
| 21 | cfg: "config.json", | |
| 22 | Listen: "127.0.0.1:3003", | |
| 23 | Base_uri: "/", | |
| 24 | Cache: cache_config{ | |
| 25 | Enabled: true, | |
| 26 | Path: "cache", | |
| 27 | }, | |
| 28 | Proxy: true, | |
| 29 | Nsfw: true, | |
| 30 | } | |
| 31 | ||
| 32 | func execcfg() { | |
| 33 | a := os.Args | |
| 34 | for num, val := range a { | |
| 35 | switch val { | |
| 36 | case "-conf": | |
| 37 | CFG.cfg = a[num] | |
| 38 | case "-help": | |
| 39 | println(`SkunkyArt v 1.3 [refactoring] | |
| 40 | Usage: | |
| 41 | - -conf - path to config | |
| 42 | - -help this message | |
| 43 | Example: | |
| 44 | ./skunkyart -conf config.json | |
| 45 | Copyright lost+skunk, X11. https://git.macaw.me/skunky/skunkyart/src/tag/v1.3`) | |
| 46 | } | |
| 47 | } | |
| 48 | } | |
app/router.go added +81
| @@ -0,0 +1,81 @@ | ||
| 1 | package app | |
| 2 | ||
| 3 | import ( | |
| 4 | "io" | |
| 5 | "net/http" | |
| 6 | u "net/url" | |
| 7 | "os" | |
| 8 | "strconv" | |
| 9 | "strings" | |
| 10 | ) | |
| 11 | ||
| 12 | const addr string = "0.0.0.0:3003" | |
| 13 | ||
| 14 | // роутер | |
| 15 | func Router() { | |
| 16 | // расшифровка эндпоинта из урл | |
| 17 | endpoint := func(url string) (string, string) { | |
| 18 | if CFG.Base_uri != "" { | |
| 19 | url = strings.Replace(url, CFG.Base_uri, "/", 1) | |
| 20 | } | |
| 21 | ||
| 22 | end := strings.Index(url[1:], "/") | |
| 23 | if end == -1 { | |
| 24 | return url[1:], "" | |
| 25 | } | |
| 26 | return url[1 : end+1], url[end+2:] | |
| 27 | } | |
| 28 | ||
| 29 | // функция, что управляет всем | |
| 30 | handle := func(w http.ResponseWriter, r *http.Request) { | |
| 31 | e, url := endpoint(r.URL.Path) | |
| 32 | var wr = io.WriteString | |
| 33 | open_n_send := func(name string) { | |
| 34 | f, e := os.ReadFile(name) | |
| 35 | err(e) | |
| 36 | wr(w, string(f)) | |
| 37 | } | |
| 38 | ||
| 39 | // структура с функциями | |
| 40 | var skunky skunkyart | |
| 41 | skunky.Args = r.URL.Query() | |
| 42 | skunky.Writer = w | |
| 43 | ||
| 44 | arg := skunky.Args.Get | |
| 45 | skunky.Query = u.QueryEscape(arg("q")) | |
| 46 | if t := arg("type"); len(t) > 0 { | |
| 47 | skunky.Type = rune(t[0]) | |
| 48 | } | |
| 49 | p, _ := strconv.Atoi(arg("p")) | |
| 50 | skunky.Page = p | |
| 51 | ||
| 52 | // пути | |
| 53 | switch e { | |
| 54 | default: | |
| 55 | skunky.httperr(404) | |
| 56 | case "/", "": | |
| 57 | open_n_send("html/index.htm") | |
| 58 | case "post": | |
| 59 | slash := strings.Index(url, "/") | |
| 60 | skunky.Deviation(url[:slash], url[slash+1:]) | |
| 61 | case "search": | |
| 62 | skunky.Search() | |
| 63 | case "media": | |
| 64 | skunky.Emojitar(url) | |
| 65 | case "about": | |
| 66 | open_n_send("html/about.htm") | |
| 67 | case "gui": | |
| 68 | w.Header().Add("content-type", "text/css") | |
| 69 | open_n_send(url) | |
| 70 | } | |
| 71 | } | |
| 72 | ||
| 73 | http.HandleFunc("/", handle) | |
| 74 | http.ListenAndServe(addr, nil) | |
| 75 | } | |
| 76 | ||
| 77 | func err(e error) { | |
| 78 | if e != nil { | |
| 79 | println(e.Error()) | |
| 80 | } | |
| 81 | } | |
app/wraper.go added +192
| @@ -0,0 +1,192 @@ | ||
| 1 | package app | |
| 2 | ||
| 3 | import ( | |
| 4 | "bytes" | |
| 5 | "io" | |
| 6 | "net/http" | |
| 7 | "net/url" | |
| 8 | "regexp" | |
| 9 | "strconv" | |
| 10 | "strings" | |
| 11 | "text/template" | |
| 12 | ||
| 13 | "git.macaw.me/skunky/devianter" | |
| 14 | ) | |
| 15 | ||
| 16 | var wr = io.WriteString | |
| 17 | ||
| 18 | type skunkyart struct { | |
| 19 | Writer http.ResponseWriter | |
| 20 | Args url.Values | |
| 21 | Type rune | |
| 22 | Query string | |
| 23 | Page int | |
| 24 | } | |
| 25 | ||
| 26 | // парсинг темплейтов | |
| 27 | func (s skunkyart) exe(file string, data any) { | |
| 28 | var buf bytes.Buffer | |
| 29 | tmp, e := template.ParseFiles(file) | |
| 30 | err(e) | |
| 31 | tmp.Execute(&buf, &data) | |
| 32 | wr(s.Writer, buf.String()) | |
| 33 | } | |
| 34 | ||
| 35 | func (s skunkyart) httperr(status int) { | |
| 36 | s.Writer.WriteHeader(status) | |
| 37 | ||
| 38 | // пострйока с помощью strings.Builder, потому что такой метод быстрее обычного сложения | |
| 39 | var msg strings.Builder | |
| 40 | msg.WriteString(`<html><link rel="stylesheet" href="/gui/css/skunky.css" />`) | |
| 41 | msg.WriteString("<h1>") | |
| 42 | msg.WriteString(strconv.Itoa(status)) | |
| 43 | msg.WriteString(" - ") | |
| 44 | msg.WriteString(http.StatusText(status)) | |
| 45 | msg.WriteString("</h1></html>") | |
| 46 | ||
| 47 | wr(s.Writer, msg.String()) | |
| 48 | } | |
| 49 | ||
| 50 | func (s skunkyart) DeviationList(devs []devianter.Deviation) string { | |
| 51 | var list strings.Builder | |
| 52 | list.WriteString(`<div class="content">`) | |
| 53 | for _, data := range devs { | |
| 54 | url := devianter.UrlFromMedia(data.Media) | |
| 55 | ||
| 56 | list.WriteString(`<a title="open/download" href="`) | |
| 57 | list.WriteString(url) | |
| 58 | list.WriteString(`"><div class="block"><img src="`) | |
| 59 | list.WriteString(url) | |
| 60 | list.WriteString(`" width="15%"></a><br><a href="`) | |
| 61 | list.WriteString("/post/") | |
| 62 | list.WriteString(data.Author.Username) | |
| 63 | list.WriteString("/") | |
| 64 | list.WriteString(data.Url[27:][strings.Index(data.Url[27:], "/art/")+5:]) | |
| 65 | list.WriteString(`">`) | |
| 66 | list.WriteString(data.Author.Username) | |
| 67 | list.WriteString(" - ") | |
| 68 | list.WriteString(data.Title) | |
| 69 | ||
| 70 | // шильдики нсфв, аи и ежедневного поста | |
| 71 | if data.NSFW { | |
| 72 | list.WriteString(` [<span class="nsfw">NSFW</span>]`) | |
| 73 | } | |
| 74 | if data.AI { | |
| 75 | list.WriteString(" [🤖]") | |
| 76 | } | |
| 77 | if data.DD { | |
| 78 | list.WriteString(` [<span class="dd">DD</span>]`) | |
| 79 | } | |
| 80 | ||
| 81 | list.WriteString("</a></div>") | |
| 82 | } | |
| 83 | list.WriteString("</div>") | |
| 84 | return list.String() | |
| 85 | } | |
| 86 | ||
| 87 | // посты | |
| 88 | func (s skunkyart) Deviation(author, postname string) { | |
| 89 | // поиск ID | |
| 90 | re := regexp.MustCompile("[0-9]+").FindAllString(postname, -1) | |
| 91 | if len(re) >= 1 { | |
| 92 | var post struct { | |
| 93 | Post devianter.Post | |
| 94 | StringTime string | |
| 95 | Tags string | |
| 96 | Comments string | |
| 97 | } | |
| 98 | ||
| 99 | id := re[len(re)-1] | |
| 100 | post.Post = devianter.DeviationFunc(id, author) | |
| 101 | ||
| 102 | // время публикации | |
| 103 | post.StringTime = post.Post.Deviation.PublishedTime.UTC().String() | |
| 104 | ||
| 105 | println(post.Post.Description) | |
| 106 | // хештэги | |
| 107 | for _, x := range post.Post.Deviation.Extended.Tags { | |
| 108 | var tag strings.Builder | |
| 109 | tag.WriteString(` <a href="/search?q=`) | |
| 110 | tag.WriteString(x.Name) | |
| 111 | tag.WriteString(`&type=tag">#`) | |
| 112 | tag.WriteString(x.Name) | |
| 113 | tag.WriteString("</a>") | |
| 114 | ||
| 115 | post.Tags += tag.String() | |
| 116 | } | |
| 117 | ||
| 118 | // генерация комментов | |
| 119 | var cmmts strings.Builder | |
| 120 | var replied map[int]string | |
| 121 | _ = replied | |
| 122 | c := devianter.CommentsFunc(id, post.Post.Comments.Cursor, s.Page, 1) | |
| 123 | ||
| 124 | cmmts.WriteString("<details><summary>Comments: <b>") | |
| 125 | cmmts.WriteString(strconv.Itoa(c.Total)) | |
| 126 | cmmts.WriteString("</b></summary>") | |
| 127 | for _, x := range c.Thread { | |
| 128 | cmmts.WriteString(`<div class="msg"><p id="`) | |
| 129 | cmmts.WriteString(strconv.Itoa(x.ID)) | |
| 130 | cmmts.WriteString(`"><img src="/media/`) | |
| 131 | cmmts.WriteString(x.User.Username) | |
| 132 | cmmts.WriteString(`?type=a" width="30px" height="30px"><a href="/user/`) | |
| 133 | cmmts.WriteString(x.User.Username) | |
| 134 | cmmts.WriteString(`"><b`) | |
| 135 | if x.User.Banned { | |
| 136 | cmmts.WriteString(` class="banned"`) | |
| 137 | } | |
| 138 | if x.Author { | |
| 139 | cmmts.WriteString(` class="author"`) | |
| 140 | } | |
| 141 | cmmts.WriteString(">") | |
| 142 | cmmts.WriteString(x.User.Username) | |
| 143 | cmmts.WriteString("</b></a> ") | |
| 144 | cmmts.WriteString(x.Posted.UTC().String()) | |
| 145 | cmmts.WriteString("<p>") | |
| 146 | cmmts.WriteString(x.Comment) | |
| 147 | cmmts.WriteString("<p>👍: ") | |
| 148 | cmmts.WriteString(strconv.Itoa(x.Likes)) | |
| 149 | cmmts.WriteString(" ⏩: ") | |
| 150 | cmmts.WriteString(strconv.Itoa(x.Replies)) | |
| 151 | cmmts.WriteString("</p></div>\n") | |
| 152 | } | |
| 153 | cmmts.WriteString("</details>") | |
| 154 | post.Comments = cmmts.String() | |
| 155 | ||
| 156 | s.exe("html/deviantion.htm", &post) | |
| 157 | } else { | |
| 158 | s.httperr(400) | |
| 159 | } | |
| 160 | } | |
| 161 | ||
| 162 | func (s skunkyart) Search() { | |
| 163 | // тут всё и так понятно | |
| 164 | if s.Type == 'a' || s.Type == 't' || s.Type == 'g' { | |
| 165 | var srch struct { | |
| 166 | Search devianter.Search | |
| 167 | List string | |
| 168 | } | |
| 169 | ||
| 170 | var e error | |
| 171 | srch.Search, e = devianter.SearchFunc(s.Query, s.Page, s.Type) | |
| 172 | err(e) | |
| 173 | srch.List = s.DeviationList(srch.Search.Results) | |
| 174 | ||
| 175 | s.exe("html/search.htm", &srch) | |
| 176 | } else { | |
| 177 | s.httperr(400) | |
| 178 | } | |
| 179 | } | |
| 180 | ||
| 181 | func (s skunkyart) Emojitar(name string) { | |
| 182 | if name != "" && (s.Type == 'a' || s.Type == 'e') { | |
| 183 | ae, e := devianter.AEmedia(name, s.Type) | |
| 184 | if e != nil { | |
| 185 | s.httperr(404) | |
| 186 | println(e.Error()) | |
| 187 | } | |
| 188 | wr(s.Writer, ae) | |
| 189 | } else { | |
| 190 | s.httperr(400) | |
| 191 | } | |
| 192 | } | |
config.json added +12
| @@ -0,0 +1,12 @@ | ||
| 1 | { | |
| 2 | "listen": "0.0.0.0:3003", | |
| 3 | "base_uri": null, | |
| 4 | "cache": { | |
| 5 | "enabled": true, | |
| 6 | "path": "cache", | |
| 7 | "lifetime": null, | |
| 8 | "max_size": 10000 | |
| 9 | }, | |
| 10 | "proxy": false, | |
| 11 | "nsfw": false | |
| 12 | } | |
| \ No newline at end of file | ||
css/skunky.css added +81
| @@ -0,0 +1,81 @@ | ||
| 1 | html { | |
| 2 | font-family: ubuntu; | |
| 3 | background-color:black; | |
| 4 | color: rgb(234, 216, 216); | |
| 5 | } | |
| 6 | a { | |
| 7 | text-decoration: none; | |
| 8 | color: cadetblue; | |
| 9 | } | |
| 10 | a:hover { | |
| 11 | color: red; | |
| 12 | transition: 400ms; | |
| 13 | } | |
| 14 | header h1 { | |
| 15 | padding-right: 0.2%; | |
| 16 | } | |
| 17 | header form { | |
| 18 | align-self: center; | |
| 19 | } | |
| 20 | header { | |
| 21 | display: flex; | |
| 22 | } | |
| 23 | form input, button, select { | |
| 24 | background-color: #134134; | |
| 25 | padding: 5px; | |
| 26 | color: whitesmoke; | |
| 27 | border: 0px; | |
| 28 | border-radius: 1px; | |
| 29 | } | |
| 30 | .nsfw, .true { | |
| 31 | color: red; | |
| 32 | } | |
| 33 | .author { | |
| 34 | color: seagreen; | |
| 35 | } | |
| 36 | .msg { | |
| 37 | background-color: #091f19; | |
| 38 | color: whitesmoke; | |
| 39 | width: fit-content; | |
| 40 | max-width: 90%; | |
| 41 | padding: 4px; | |
| 42 | border-radius: 2px; | |
| 43 | margin-top: 6px; | |
| 44 | text-wrap: pretty; | |
| 45 | transition: 350ms; | |
| 46 | } | |
| 47 | .msg:hover { | |
| 48 | background-color: #134134; | |
| 49 | } | |
| 50 | .dd { | |
| 51 | color: rgb(160, 0, 147); | |
| 52 | } | |
| 53 | .content { | |
| 54 | border-radius: 3px; | |
| 55 | display: flex; | |
| 56 | flex-wrap: wrap; | |
| 57 | justify-content: center; | |
| 58 | } | |
| 59 | .block { | |
| 60 | max-width: 20%; | |
| 61 | height: 0%; | |
| 62 | padding: 4px; | |
| 63 | border-radius: 2px; | |
| 64 | border: 3px solid #091f19; | |
| 65 | word-break: break-all; | |
| 66 | background-color: #091f19; | |
| 67 | margin-left: 5px; | |
| 68 | margin-top: 5px; | |
| 69 | text-align: center; | |
| 70 | } | |
| 71 | .block:hover { | |
| 72 | border: 3px solid #4d27d6; | |
| 73 | transition: 400ms; | |
| 74 | } | |
| 75 | .block img { | |
| 76 | width: 100%; | |
| 77 | } | |
| 78 | .block p { | |
| 79 | word-break: break-all; | |
| 80 | } | |
| 81 | ||
go.mod added +7
| @@ -0,0 +1,7 @@ | ||
| 1 | module skunkyart | |
| 2 | ||
| 3 | go 1.22.3 | |
| 4 | ||
| 5 | replace git.macaw.me/skunky/devianter v0.1.0 => /home/skunk/projects/devianter | |
| 6 | ||
| 7 | require git.macaw.me/skunky/devianter v0.1.0 | |
go.sum added
html/about.htm added +23
| @@ -0,0 +1,23 @@ | ||
| 1 | <!DOCTYPE html> | |
| 2 | <html> | |
| 3 | <head> | |
| 4 | <title>SkunkyArt</title> | |
| 5 | <link rel="stylesheet" href="gui/css/skunky.css"> | |
| 6 | </head> | |
| 7 | <main> | |
| 8 | <header> | |
| 9 | <h1><a href="/">HOME</a> | <a href="/dd">DD</a></h1> | |
| 10 | <form method="get" action="/search"> | |
| 11 | <input type="text" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false"> | |
| 12 | <select name="type"> | |
| 13 | <option value="all">All</option> | |
| 14 | <option value="tag">Tag</option> | |
| 15 | </select> | |
| 16 | <button type="submit">Search!</button> | |
| 17 | </form> | |
| 18 | </header> | |
| 19 | <p> | |
| 20 | SkunkyArt is an alternative frontend for deviantart.com, written in Go. | |
| 21 | </p> | |
| 22 | </main> | |
| 23 | </html> | |
| \ No newline at end of file | ||
html/deviantion.htm added +44
| @@ -0,0 +1,44 @@ | ||
| 1 | <!DOCTYPE html> | |
| 2 | <html> | |
| 3 | <head> | |
| 4 | <title>SkunkyArt | {{.Post.Deviation.Author.Username}} - {{.Post.Deviation.Title}}</title> | |
| 5 | <link rel="stylesheet" href="/gui/css/skunky.css"> | |
| 6 | </head> | |
| 7 | <main> | |
| 8 | <header> | |
| 9 | <h1><a href="/">HOME</a> | <a href="/dd">DD</a></h1> | |
| 10 | <form method="get" action="/search"> | |
| 11 | <input type="text" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false"> | |
| 12 | <select name="type"> | |
| 13 | <option value="all">All</option> | |
| 14 | <option value="tag">Tag</option> | |
| 15 | </select> | |
| 16 | <button type="submit">Search!</button> | |
| 17 | </form> | |
| 18 | </header> | |
| 19 | <figure> | |
| 20 | <img src="/media/{{.Post.Deviation.Author.Username}}?type=a" width="30px"> | |
| 21 | <span><strong><a href="/user/{{.Post.Deviation.Author.Username}}">{{.Post.Deviation.Author.Username}}</a></strong> — {{if (.Post.Deviation.DD)}} | |
| 22 | <span class="dd" title="Daily Deviation!"><b>{{.Post.Deviation.Title}}</b></span> | |
| 23 | {{else}}{{.Post.Deviation.Title}}{{end}} | |
| 24 | {{if (ne .Post.Deviation.License "none")}}<mark title="License">{{.Post.Deviation.License}}</mark>{{end}} {{if (.Post.Deviation.AI)}}[🤖]{{end}} | |
| 25 | {{if (.Post.Deviation.NSFW)}}[<span class="nsfw">NSFW</span>]{{end}} | |
| 26 | </span> | |
| 27 | <br> | |
| 28 | {{if (ne .Post.IMG "")}} | |
| 29 | <a href="{{.Post.IMG}}" title="open/download image"><img src="{{.Post.IMG}}" width="50%"></a> | |
| 30 | <br> | |
| 31 | {{end}} | |
| 32 | {{if (ne .Tags "")}} | |
| 33 | {{.Tags}}<br> | |
| 34 | {{end}} | |
| 35 | <span>Published: <strong>{{.StringTime}}</strong>; Views: <strong>{{.Post.Deviation.Stats.Views}}</strong>; Favourites: <strong>{{.Post.Deviation.Stats.Favourites}}</strong>; Downloads: <strong>{{.Post.Deviation.Stats.Downloads}}</strong></span> | |
| 36 | {{if (ne .Post.Description "")}} | |
| 37 | {{.Post.Description}} | |
| 38 | {{end}} | |
| 39 | {{if (ne .Comments "")}} | |
| 40 | {{.Comments}} | |
| 41 | {{end}} | |
| 42 | </figure> | |
| 43 | </main> | |
| 44 | </html> | |
| \ No newline at end of file | ||
html/index.htm added +20
| @@ -0,0 +1,20 @@ | ||
| 1 | <!DOCTYPE html> | |
| 2 | <html> | |
| 3 | <head> | |
| 4 | <title>SkunkyArt</title> | |
| 5 | <link rel="stylesheet" href="gui/css/skunky.css" | |
| 6 | </head> | |
| 7 | <main> | |
| 8 | <center> | |
| 9 | <form method="get" action="search"> | |
| 10 | <input type="text" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false"> | |
| 11 | <select name="type"> | |
| 12 | <option value="all">All</option> | |
| 13 | <option value="tag">Tag</option> | |
| 14 | </select> | |
| 15 | <button type="submit">Search!</button> | |
| 16 | </form> | |
| 17 | <h1><a href="dd">Daily Deviations</a> | <a href="about">About</a> | <a href="https://git.macaw.me/skunky/SkunkyArt" target="_blank">Source Code</a></h1> | |
| 18 | </center> | |
| 19 | </main> | |
| 20 | </html> | |
| \ No newline at end of file | ||
html/search.htm added +29
| @@ -0,0 +1,29 @@ | ||
| 1 | <!DOCTYPE html> | |
| 2 | <html> | |
| 3 | <head> | |
| 4 | <title>SkunkyArt | Search</title> | |
| 5 | <link rel="stylesheet" href="/gui/css/skunky.css"> | |
| 6 | </head> | |
| 7 | <main> | |
| 8 | <header> | |
| 9 | <h1><a href="/">HOME</a> | <a href="/dd">DD</a></h1> | |
| 10 | <form method="get" action="search"> | |
| 11 | <input type="text" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false"> | |
| 12 | <select name="type"> | |
| 13 | <option value="all">All</option> | |
| 14 | <option value="tag">Tag</option> | |
| 15 | </select> | |
| 16 | <button type="submit">Search!</button> | |
| 17 | </form> | |
| 18 | </header> | |
| 19 | {{if (ne .List "")}} | |
| 20 | {{if (ne .Search.Total 0)}} | |
| 21 | <h1>Total resuls: {{.Search.Total}}</h1> | |
| 22 | {{end}} | |
| 23 | {{.List}} | |
| 24 | {{.Search.Pages}} | |
| 25 | {{else}} | |
| 26 | <p>No results :(</p> | |
| 27 | {{end}} | |
| 28 | </main> | |
| 29 | </html> | |
| \ No newline at end of file | ||
main.go added +7
| @@ -0,0 +1,7 @@ | ||
| 1 | package main | |
| 2 | ||
| 3 | import "skunkyart/app" | |
| 4 | ||
| 5 | func main() { | |
| 6 | app.Router() | |
| 7 | } | |