v1.3
This commit is contained in:
parent
8391cc34a9
commit
667de65e2f
13
LICENSE
Normal file
13
LICENSE
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
X11 License
|
||||||
|
|
||||||
|
Copyright (C) 1996 X Consortium
|
||||||
|
|
||||||
|
Permission 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:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE 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.
|
||||||
|
|
||||||
|
Except 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.
|
||||||
|
|
||||||
|
X Window System is a trademark of X Consortium, Inc.
|
76
README.md
Normal file
76
README.md
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
[![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")
|
||||||
|
# Instances
|
||||||
|
|Инстанс|Yggdrasil|I2P|Tor|NSFW|Proxifying|Country|
|
||||||
|
|:-----:|:-------:|:-:|:-:|:--:|:--------:|:-----:|
|
||||||
|
|[skunky.ebloid.ru](https://skunky.ebloid.ru/art)|[Yes](http://[201:eba5:d1fc:bf7b:cfcb:a811:4b8b:7ea3]/art)|No|No| No | No | Russia |
|
||||||
|
|[clovius.club](https://skunky.clovius.club)|No|No|No| Yes | Yes | Sweden |
|
||||||
|
|[bloat.cat](https://skunky.bloat.cat)|No|No|No| Yes | Yes | Romania |
|
||||||
|
|[frontendfriendly.xyz](https://skunkyart.frontendfriendly.xyz)|No|No|No| Yes | Yes | Finland |
|
||||||
|
|
||||||
|
# EN 🇺🇸
|
||||||
|
## Description
|
||||||
|
SkunkyArt 🦨 -- alternative frontend to DeviantArt, which will work without problems even on quite old hardware, due to the lack of JavaScript.
|
||||||
|
## Config
|
||||||
|
The sample config is in the `config.example.json` file. To specify your own path to the config, use the CLI argument `-c` or `--config`.
|
||||||
|
* `listen` -- the address and port on which SkunkyArt will listen
|
||||||
|
* `base-path` -- the path to the instance. Example: "`base-path`:"/art/" -> https://skunky.ebloid.ru/art/
|
||||||
|
* `cache` -- caching system; default is off.
|
||||||
|
* * `path` -- the path to the cache
|
||||||
|
* * `lifetime` -- cache file lifetime; measured in Unix milliseconds.
|
||||||
|
* * `max-size` -- maximum file size in bytes.
|
||||||
|
* `dirs-to-memory` -- this setting determines which directories will be copied to RAM when SkunkyArt is started. Required
|
||||||
|
* `download-proxy` -- proxy address for downloading files.
|
||||||
|
## Examples of reverse proxies
|
||||||
|
Nginx:
|
||||||
|
```apache
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name skunky.example.com;
|
||||||
|
|
||||||
|
location ((BASE URL)) { # if you have a separate subdomain for the frontend, insert '/' instead of '((BASE URL))'.
|
||||||
|
proxy_set_header Scheme $scheme;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_pass http://((IP)):((PORT));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
## How do I add my instance to the list?
|
||||||
|
To 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:
|
||||||
|
1. the instance must not use Cloudflare.
|
||||||
|
2. If your instance has modified source code, you need to publish it to any free platform. For example, Github and Gitlab are not.
|
||||||
|
## Acknowledgements
|
||||||
|
* [Лис⚛](https://go.kde.org/matrix/#/@fox:matrix.org) -- helped me understand Go and gave me a lot of useful advice on this language.
|
||||||
|
|
||||||
|
# RU 🇷🇺
|
||||||
|
## Описание
|
||||||
|
SkunkyArt 🦨 -- альтернативный фронтенд к DeviantArt, который будет работать без проблем даже на довольно старом оборудовании, за счёт отсутствия JavaScript.
|
||||||
|
## Конфиг
|
||||||
|
Пример конфига находится в файле `config.example.json`. Чтобы указать свой путь до конфига, используйте CLI-аргумент `-c` или `--config`.
|
||||||
|
* `listen` -- адрес и порт, на котором будет слушать SkunkyArt
|
||||||
|
* `base-path` -- путь к инстансу. Пример: "base-path": "/art/" -> https://skunky.ebloid.ru/art/
|
||||||
|
* `cache` -- система кеширования; по умолчанию - выкл.
|
||||||
|
* * `path` -- путь до кеша
|
||||||
|
* * `lifetime` -- время жизни файла в кеше; измеряется в Unix-миллисекундах
|
||||||
|
* * `max-size` -- максимальный размер файла в байтах
|
||||||
|
* `dirs-to-memory` -- данная настройка определяет какие каталоги будут скопированы в ОЗУ при запуске SkunkyArt. Обязательна
|
||||||
|
* `download-proxy` -- адрес прокси для загрузки файлов
|
||||||
|
## Примеры reverse-прокси
|
||||||
|
Nginx:
|
||||||
|
```apache
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name skunky.example.com;
|
||||||
|
|
||||||
|
location ((BASE URL)) { # если у вас отдельный поддомен для фронтенда, вместо '((BASE URL))' вставляйте '/'
|
||||||
|
proxy_set_header Scheme $scheme;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_pass http://((IP)):((PORT));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
## Как добавить свой инстанс в список?
|
||||||
|
Чтобы это сделать, вы должны либо сделать PR, добавив в файл `instances.json` свой инстанс, либо сообщить о нём в комнате в Matrix. Думаю, он не нуждается в описании. Однако учтите, у этого списка есть пара правил:
|
||||||
|
1. Инстанс не должен использовать Cloudflare.
|
||||||
|
2. Если ваш инстанс имеет модифицированный исходный код, то вам нужно опубликовать его на любую свободную площадку. Например, Github и Gitlab таковыми не являются.
|
||||||
|
## Благодарности
|
||||||
|
* [Лис⚛](https://go.kde.org/matrix/#/@fox:matrix.org) -- помог разобраться в Go и много чего полезного посоветовал по этому языку.
|
7
TODO.md
Normal file
7
TODO.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# v1.3.x
|
||||||
|
* Доделать парсинг описания
|
||||||
|
* Реализовать миниатюры и оптимизировать CSS под маленькие экраны
|
||||||
|
# v1.4
|
||||||
|
* Реализовать темы
|
||||||
|
* Реализовать многоязычный интерфейс
|
||||||
|
* Реализовать API
|
@ -2,8 +2,8 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type cache_config struct {
|
type cache_config struct {
|
||||||
@ -15,13 +15,13 @@ type cache_config struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type config struct {
|
type config struct {
|
||||||
cfg string
|
cfg string
|
||||||
Listen string
|
Listen string
|
||||||
BasePath string `json:"base-path"`
|
BasePath string `json:"base-path"`
|
||||||
Cache cache_config
|
Cache cache_config
|
||||||
Proxy, Nsfw bool
|
Proxy, Nsfw bool
|
||||||
WixmpProxy string `json:"wixmp-proxy"`
|
DownloadProxy string `json:"download-proxy"`
|
||||||
TemplatesDir string `json:"templates-dir"`
|
Dirs []string `json:"dirs-to-memory"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var CFG = config{
|
var CFG = config{
|
||||||
@ -33,51 +33,52 @@ var CFG = config{
|
|||||||
Path: "cache",
|
Path: "cache",
|
||||||
UpdateInterval: 1,
|
UpdateInterval: 1,
|
||||||
},
|
},
|
||||||
TemplatesDir: "html",
|
Dirs: []string{"html", "css"},
|
||||||
Proxy: true,
|
Proxy: true,
|
||||||
Nsfw: true,
|
Nsfw: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExecuteConfig() {
|
func ExecuteConfig() {
|
||||||
try := func(err error, exitcode int) {
|
go func() {
|
||||||
if err != nil {
|
for {
|
||||||
println(err.Error())
|
Templates["instances.json"] = string(Download("https://git.macaw.me/skunky/SkunkyArt/raw/branch/master/instances.json").Body)
|
||||||
os.Exit(exitcode)
|
time.Sleep(1 * time.Hour)
|
||||||
}
|
}
|
||||||
}
|
}()
|
||||||
|
|
||||||
a := os.Args
|
const helpmsg = `SkunkyArt v1.3 [refactoring]
|
||||||
if l := len(a); l > 1 {
|
|
||||||
switch a[1] {
|
|
||||||
case "-c", "--config":
|
|
||||||
if l >= 3 {
|
|
||||||
CFG.cfg = a[2]
|
|
||||||
} else {
|
|
||||||
try(errors.New("Not enought arguments"), 1)
|
|
||||||
}
|
|
||||||
case "-h", "--help":
|
|
||||||
try(errors.New(`SkunkyArt v1.3 [refactoring]
|
|
||||||
Usage:
|
Usage:
|
||||||
- [-c|--config] - path to config
|
- [-c|--config] - path to config
|
||||||
- [-h|--help] - returns this message
|
- [-h|--help] - returns this message
|
||||||
Example:
|
Example:
|
||||||
./skunkyart -c config.json
|
./skunkyart -c config.json
|
||||||
Copyright lost+skunk, X11. https://git.macaw.me/skunky/skunkyart/src/tag/v1.3`), 0)
|
Copyright lost+skunk, X11. https://git.macaw.me/skunky/skunkyart/src/tag/v1.3`
|
||||||
default:
|
|
||||||
try(errors.New("Unreconginzed argument: "+a[1]), 1)
|
a := os.Args
|
||||||
|
for n, x := range a {
|
||||||
|
switch x {
|
||||||
|
case "-c", "--config":
|
||||||
|
if len(a) >= 3 {
|
||||||
|
CFG.cfg = a[n+1]
|
||||||
|
} else {
|
||||||
|
exit("Not enought arguments", 1)
|
||||||
|
}
|
||||||
|
case "-h", "--help":
|
||||||
|
exit(helpmsg, 0)
|
||||||
}
|
}
|
||||||
if CFG.cfg != "" {
|
}
|
||||||
f, err := os.ReadFile(CFG.cfg)
|
|
||||||
try(err, 1)
|
|
||||||
|
|
||||||
try(json.Unmarshal(f, &CFG), 1)
|
if CFG.cfg != "" {
|
||||||
if CFG.Cache.Enabled && !CFG.Proxy {
|
f, err := os.ReadFile(CFG.cfg)
|
||||||
try(errors.New("Incompatible settings detected: cannot use caching media content without proxy"), 1)
|
try_with_exitstatus(err, 1)
|
||||||
}
|
|
||||||
|
|
||||||
if CFG.Cache.MaxSize != 0 || CFG.Cache.Lifetime != 0 {
|
try_with_exitstatus(json.Unmarshal(f, &CFG), 1)
|
||||||
go InitCacheSystem()
|
if CFG.Cache.Enabled && !CFG.Proxy {
|
||||||
}
|
exit("Incompatible settings detected: cannot use caching media content without proxy", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if CFG.Cache.MaxSize != 0 || CFG.Cache.Lifetime != 0 {
|
||||||
|
go InitCacheSystem()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
367
app/parsers.go
Normal file
367
app/parsers.go
Normal file
@ -0,0 +1,367 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.macaw.me/skunky/devianter"
|
||||||
|
"golang.org/x/net/html"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s skunkyart) ParseComments(c devianter.Comments) string {
|
||||||
|
var cmmts strings.Builder
|
||||||
|
replied := make(map[int]string)
|
||||||
|
|
||||||
|
cmmts.WriteString("<details><summary>Comments: <b>")
|
||||||
|
cmmts.WriteString(strconv.Itoa(c.Total))
|
||||||
|
cmmts.WriteString("</b></summary>")
|
||||||
|
for _, x := range c.Thread {
|
||||||
|
replied[x.ID] = x.User.Username
|
||||||
|
cmmts.WriteString(`<div class="msg`)
|
||||||
|
if x.Parent > 0 {
|
||||||
|
cmmts.WriteString(` reply`)
|
||||||
|
}
|
||||||
|
cmmts.WriteString(`"><p id="`)
|
||||||
|
cmmts.WriteString(strconv.Itoa(x.ID))
|
||||||
|
cmmts.WriteString(`"><img src="`)
|
||||||
|
cmmts.WriteString(UrlBuilder("media", "emojitar", x.User.Username, "?type=a"))
|
||||||
|
cmmts.WriteString(`" width="30px" height="30px"><a href="`)
|
||||||
|
cmmts.WriteString(UrlBuilder("group_user", "?q=", x.User.Username, "&type=a"))
|
||||||
|
cmmts.WriteString(`"><b`)
|
||||||
|
cmmts.WriteString(` class="`)
|
||||||
|
if x.User.Banned {
|
||||||
|
cmmts.WriteString(`banned`)
|
||||||
|
}
|
||||||
|
if x.Author {
|
||||||
|
cmmts.WriteString(`author`)
|
||||||
|
}
|
||||||
|
cmmts.WriteString(`">`)
|
||||||
|
cmmts.WriteString(x.User.Username)
|
||||||
|
cmmts.WriteString("</b></a> ")
|
||||||
|
|
||||||
|
if x.Parent > 0 {
|
||||||
|
cmmts.WriteString(` In reply to <a href="#`)
|
||||||
|
cmmts.WriteString(strconv.Itoa(x.Parent))
|
||||||
|
cmmts.WriteString(`">`)
|
||||||
|
if replied[x.Parent] == "" {
|
||||||
|
cmmts.WriteString("???")
|
||||||
|
} else {
|
||||||
|
cmmts.WriteString(replied[x.Parent])
|
||||||
|
}
|
||||||
|
cmmts.WriteString("</a>")
|
||||||
|
}
|
||||||
|
cmmts.WriteString(" [")
|
||||||
|
cmmts.WriteString(x.Posted.UTC().String())
|
||||||
|
cmmts.WriteString("]<p>")
|
||||||
|
|
||||||
|
cmmts.WriteString(ParseDescription(x.TextContent))
|
||||||
|
cmmts.WriteString("<p>👍: ")
|
||||||
|
cmmts.WriteString(strconv.Itoa(x.Likes))
|
||||||
|
cmmts.WriteString(" ⏩: ")
|
||||||
|
cmmts.WriteString(strconv.Itoa(x.Replies))
|
||||||
|
cmmts.WriteString("</p></div>\n")
|
||||||
|
}
|
||||||
|
cmmts.WriteString(s.NavBase(DeviationList{
|
||||||
|
Pages: 0,
|
||||||
|
More: c.HasMore,
|
||||||
|
}))
|
||||||
|
cmmts.WriteString("</details>")
|
||||||
|
return cmmts.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s skunkyart) DeviationList(devs []devianter.Deviation, content ...DeviationList) string {
|
||||||
|
var list strings.Builder
|
||||||
|
if s.Atom && s.Page > 1 {
|
||||||
|
s.ReturnHTTPError(400)
|
||||||
|
return ""
|
||||||
|
} else if s.Atom {
|
||||||
|
list.WriteString(`<?xml version="1.0" encoding="UTF-8"?><feed xmlns:media="http://search.yahoo.com/mrss/" xmlns="http://www.w3.org/2005/Atom">`)
|
||||||
|
list.WriteString(`<title>`)
|
||||||
|
if s.Type == 0 {
|
||||||
|
list.WriteString("Daily Deviations")
|
||||||
|
} else if len(devs) != 0 {
|
||||||
|
list.WriteString(devs[0].Author.Username)
|
||||||
|
} else {
|
||||||
|
list.WriteString("SkunkyArt")
|
||||||
|
}
|
||||||
|
list.WriteString(`</title>`)
|
||||||
|
|
||||||
|
list.WriteString(`<link rel="alternate" href="`)
|
||||||
|
list.WriteString(Host)
|
||||||
|
list.WriteString(`"/>`)
|
||||||
|
} else {
|
||||||
|
list.WriteString(`<div class="content">`)
|
||||||
|
}
|
||||||
|
for _, data := range devs {
|
||||||
|
if !(data.NSFW && !CFG.Nsfw) {
|
||||||
|
url := ParseMedia(data.Media)
|
||||||
|
if s.Atom {
|
||||||
|
id := strconv.Itoa(data.ID)
|
||||||
|
list.WriteString(`<entry><author><name>`)
|
||||||
|
list.WriteString(data.Author.Username)
|
||||||
|
list.WriteString(`</name></author><title>`)
|
||||||
|
list.WriteString(data.Title)
|
||||||
|
list.WriteString(`</title><link rel="alternate" type="text/html" href="`)
|
||||||
|
list.WriteString(UrlBuilder("post", data.Author.Username, "atom-"+id))
|
||||||
|
list.WriteString(`"/><id>`)
|
||||||
|
list.WriteString(id)
|
||||||
|
list.WriteString(`</id><published>`)
|
||||||
|
list.WriteString(data.PublishedTime.UTC().Format("Mon, 02 Jan 2006 15:04:05 -0700"))
|
||||||
|
list.WriteString(`</published>`)
|
||||||
|
list.WriteString(`<media:group><media:title>`)
|
||||||
|
list.WriteString(data.Title)
|
||||||
|
list.WriteString(`</media:title><media:thumbinal url="`)
|
||||||
|
list.WriteString(url)
|
||||||
|
list.WriteString(`"/></media:group><content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><a href="`)
|
||||||
|
list.WriteString(ConvertDeviantArtUrlToSkunkyArt(data.Url))
|
||||||
|
list.WriteString(`"><img src="`)
|
||||||
|
list.WriteString(url)
|
||||||
|
list.WriteString(`"/></a><p>`)
|
||||||
|
list.WriteString(ParseDescription(data.TextContent))
|
||||||
|
list.WriteString(`</p></div></content></entry>`)
|
||||||
|
} else {
|
||||||
|
list.WriteString(`<div class="block">`)
|
||||||
|
if url != "" {
|
||||||
|
list.WriteString(`<a title="open/download" href="`)
|
||||||
|
list.WriteString(url)
|
||||||
|
list.WriteString(`"><img loading="lazy" src="`)
|
||||||
|
list.WriteString(url)
|
||||||
|
list.WriteString(`" width="15%"></a>`)
|
||||||
|
} else {
|
||||||
|
list.WriteString(`<h1>[ TEXT ]</h1>`)
|
||||||
|
}
|
||||||
|
list.WriteString(`<br><a href="`)
|
||||||
|
list.WriteString(ConvertDeviantArtUrlToSkunkyArt(data.Url))
|
||||||
|
list.WriteString(`">`)
|
||||||
|
list.WriteString(data.Author.Username)
|
||||||
|
list.WriteString(" - ")
|
||||||
|
list.WriteString(data.Title)
|
||||||
|
|
||||||
|
// шильдики нсфв, аи и ежедневного поста
|
||||||
|
if data.NSFW {
|
||||||
|
list.WriteString(` [<span class="nsfw">NSFW</span>]`)
|
||||||
|
}
|
||||||
|
if data.AI {
|
||||||
|
list.WriteString(" [🤖]")
|
||||||
|
}
|
||||||
|
if data.DD {
|
||||||
|
list.WriteString(` [<span class="dd">DD</span>]`)
|
||||||
|
}
|
||||||
|
|
||||||
|
list.WriteString("</a></div>")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Atom {
|
||||||
|
list.WriteString("</feed>")
|
||||||
|
s.Writer.Write([]byte(list.String()))
|
||||||
|
return ""
|
||||||
|
} else {
|
||||||
|
list.WriteString("</div>")
|
||||||
|
if content != nil {
|
||||||
|
list.WriteString(s.NavBase(content[0]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return list.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DESCRIPTION/COMMENT PARSER */
|
||||||
|
type text struct {
|
||||||
|
TXT string
|
||||||
|
TXT_RAW string
|
||||||
|
From int
|
||||||
|
To int
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseDescription(dscr devianter.Text) string {
|
||||||
|
var parseddescription strings.Builder
|
||||||
|
TagBuilder := func(content string, tags ...string) string {
|
||||||
|
l := len(tags)
|
||||||
|
for x := 0; x < l; x++ {
|
||||||
|
var htm strings.Builder
|
||||||
|
htm.WriteString("<")
|
||||||
|
htm.WriteString(tags[x])
|
||||||
|
htm.WriteString(">")
|
||||||
|
|
||||||
|
htm.WriteString(content)
|
||||||
|
|
||||||
|
htm.WriteString("</")
|
||||||
|
htm.WriteString(tags[x])
|
||||||
|
htm.WriteString(">")
|
||||||
|
content = htm.String()
|
||||||
|
}
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
DeleteTrackingFromUrl := func(url string) string {
|
||||||
|
if len(url) > 42 && url[:42] == "https://www.deviantart.com/users/outgoing?" {
|
||||||
|
url = url[42:]
|
||||||
|
}
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
if description, dl := dscr.Html.Markup, len(dscr.Html.Markup); dl != 0 &&
|
||||||
|
description[0] == '{' &&
|
||||||
|
description[dl-1] == '}' {
|
||||||
|
var descr struct {
|
||||||
|
Blocks []struct {
|
||||||
|
Text, Type string
|
||||||
|
InlineStyleRanges []struct {
|
||||||
|
Offset, Length int
|
||||||
|
Style string
|
||||||
|
}
|
||||||
|
EntityRanges []struct {
|
||||||
|
Offset, Length int
|
||||||
|
Key int
|
||||||
|
}
|
||||||
|
Data struct {
|
||||||
|
TextAlignment string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EntityMap map[string]struct {
|
||||||
|
Type string
|
||||||
|
Data struct {
|
||||||
|
Url string
|
||||||
|
Config struct {
|
||||||
|
Aligment string
|
||||||
|
Width int
|
||||||
|
}
|
||||||
|
Data devianter.Deviation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e := json.Unmarshal([]byte(description), &descr)
|
||||||
|
try(e)
|
||||||
|
|
||||||
|
entities := make(map[int]devianter.Deviation)
|
||||||
|
urls := make(map[int]string)
|
||||||
|
for n, x := range descr.EntityMap {
|
||||||
|
num, _ := strconv.Atoi(n)
|
||||||
|
if x.Data.Url != "" {
|
||||||
|
urls[num] = DeleteTrackingFromUrl(x.Data.Url)
|
||||||
|
}
|
||||||
|
entities[num] = x.Data.Data
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, x := range descr.Blocks {
|
||||||
|
Styles := make([]text, len(x.InlineStyleRanges))
|
||||||
|
|
||||||
|
if len(x.InlineStyleRanges) != 0 {
|
||||||
|
var tags = make(map[int][]string)
|
||||||
|
for n, rngs := range x.InlineStyleRanges {
|
||||||
|
Styles := &Styles[n]
|
||||||
|
switch rngs.Style {
|
||||||
|
case "BOLD":
|
||||||
|
rngs.Style = "b"
|
||||||
|
case "UNDERLINE":
|
||||||
|
rngs.Style = "u"
|
||||||
|
case "ITALIC":
|
||||||
|
rngs.Style = "i"
|
||||||
|
}
|
||||||
|
Styles.From = rngs.Offset
|
||||||
|
Styles.To = rngs.Offset + rngs.Length
|
||||||
|
FT := Styles.From * Styles.To
|
||||||
|
tags[FT] = append(tags[FT], rngs.Style)
|
||||||
|
}
|
||||||
|
for n := 0; n < len(Styles); n++ {
|
||||||
|
Styles := &Styles[n]
|
||||||
|
Styles.TXT_RAW = x.Text[Styles.From:Styles.To]
|
||||||
|
Styles.TXT = TagBuilder(Styles.TXT_RAW, tags[Styles.From*Styles.To]...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch x.Type {
|
||||||
|
case "atomic":
|
||||||
|
d := entities[x.EntityRanges[0].Key]
|
||||||
|
parseddescription.WriteString(`<a href="`)
|
||||||
|
parseddescription.WriteString(ConvertDeviantArtUrlToSkunkyArt(d.Url))
|
||||||
|
parseddescription.WriteString(`"><img width="50%" src="`)
|
||||||
|
parseddescription.WriteString(ParseMedia(d.Media))
|
||||||
|
parseddescription.WriteString(`" title="`)
|
||||||
|
parseddescription.WriteString(d.Author.Username)
|
||||||
|
parseddescription.WriteString(" - ")
|
||||||
|
parseddescription.WriteString(d.Title)
|
||||||
|
parseddescription.WriteString(`"></a>`)
|
||||||
|
case "unstyled":
|
||||||
|
if l := len(Styles); l != 0 {
|
||||||
|
for n, r := range Styles {
|
||||||
|
var tag string
|
||||||
|
if x.Type == "header-two" {
|
||||||
|
tag = "h2"
|
||||||
|
}
|
||||||
|
|
||||||
|
parseddescription.WriteString(x.Text[:r.From])
|
||||||
|
if len(urls) != 0 && len(x.EntityRanges) != 0 {
|
||||||
|
ra := &x.EntityRanges[0]
|
||||||
|
|
||||||
|
parseddescription.WriteString(`<a target="_blank" href="`)
|
||||||
|
parseddescription.WriteString(urls[ra.Key])
|
||||||
|
parseddescription.WriteString(`">`)
|
||||||
|
parseddescription.WriteString(r.TXT)
|
||||||
|
parseddescription.WriteString(`</a>`)
|
||||||
|
} else if l > n+1 {
|
||||||
|
parseddescription.WriteString(r.TXT)
|
||||||
|
}
|
||||||
|
parseddescription.WriteString(TagBuilder(tag, x.Text[r.To:]))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
parseddescription.WriteString(x.Text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parseddescription.WriteString("<br>")
|
||||||
|
}
|
||||||
|
} else if dl != 0 {
|
||||||
|
for tt := html.NewTokenizer(strings.NewReader(dscr.Html.Markup)); ; {
|
||||||
|
switch tt.Next() {
|
||||||
|
case html.ErrorToken:
|
||||||
|
return parseddescription.String()
|
||||||
|
case html.StartTagToken, html.EndTagToken, html.SelfClosingTagToken:
|
||||||
|
token := tt.Token()
|
||||||
|
switch token.Data {
|
||||||
|
case "a":
|
||||||
|
for _, a := range token.Attr {
|
||||||
|
if a.Key == "href" {
|
||||||
|
url := DeleteTrackingFromUrl(a.Val)
|
||||||
|
parseddescription.WriteString(`<a target="_blank" href="`)
|
||||||
|
parseddescription.WriteString(url)
|
||||||
|
parseddescription.WriteString(`">`)
|
||||||
|
parseddescription.WriteString(GetValueOfTag(tt))
|
||||||
|
parseddescription.WriteString("</a> ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "img":
|
||||||
|
var uri, title string
|
||||||
|
for b, a := range token.Attr {
|
||||||
|
switch a.Key {
|
||||||
|
case "src":
|
||||||
|
if len(a.Val) > 9 && a.Val[8:9] == "e" {
|
||||||
|
uri = UrlBuilder("media", "emojitar", a.Val[37:len(a.Val)-4], "?type=e")
|
||||||
|
}
|
||||||
|
case "title":
|
||||||
|
title = a.Val
|
||||||
|
}
|
||||||
|
if title != "" {
|
||||||
|
for x := -1; x < b; x++ {
|
||||||
|
parseddescription.WriteString(`<img src="`)
|
||||||
|
parseddescription.WriteString(uri)
|
||||||
|
parseddescription.WriteString(`" title="`)
|
||||||
|
parseddescription.WriteString(title)
|
||||||
|
parseddescription.WriteString(`">`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "br", "li", "ul", "p", "b":
|
||||||
|
parseddescription.WriteString(token.String())
|
||||||
|
case "div":
|
||||||
|
parseddescription.WriteString("<p> ")
|
||||||
|
}
|
||||||
|
case html.TextToken:
|
||||||
|
parseddescription.Write(tt.Text())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseddescription.String()
|
||||||
|
}
|
@ -4,11 +4,12 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
u "net/url"
|
u "net/url"
|
||||||
"os"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var Host string
|
||||||
|
|
||||||
func Router() {
|
func Router() {
|
||||||
parsepath := func(path string) map[int]string {
|
parsepath := func(path string) map[int]string {
|
||||||
if l := len(CFG.BasePath); len(path) > l {
|
if l := len(CFG.BasePath); len(path) > l {
|
||||||
@ -43,18 +44,18 @@ func Router() {
|
|||||||
|
|
||||||
// функция, что управляет всем
|
// функция, что управляет всем
|
||||||
handle := func(w http.ResponseWriter, r *http.Request) {
|
handle := func(w http.ResponseWriter, r *http.Request) {
|
||||||
path := parsepath(r.URL.Path)
|
if h := r.Header["Scheme"]; len(h) != 0 && h[0] == "https" {
|
||||||
var wr = io.WriteString
|
Host = h[0] + "://" + r.Host
|
||||||
open_n_send := func(name string) {
|
} else {
|
||||||
f, e := os.ReadFile(name)
|
Host = "http://" + r.Host
|
||||||
err(e)
|
|
||||||
wr(w, string(f))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
path := parsepath(r.URL.Path)
|
||||||
|
|
||||||
// структура с функциями
|
// структура с функциями
|
||||||
var skunky skunkyart
|
var skunky skunkyart
|
||||||
skunky.Args = r.URL.Query()
|
|
||||||
skunky.Writer = w
|
skunky.Writer = w
|
||||||
|
skunky.Args = r.URL.Query()
|
||||||
skunky.BasePath = CFG.BasePath
|
skunky.BasePath = CFG.BasePath
|
||||||
|
|
||||||
arg := skunky.Args.Get
|
arg := skunky.Args.Get
|
||||||
@ -95,18 +96,12 @@ func Router() {
|
|||||||
}
|
}
|
||||||
case "about":
|
case "about":
|
||||||
skunky.About()
|
skunky.About()
|
||||||
case "gui":
|
case "stylesheet":
|
||||||
w.Header().Add("content-type", "text/css")
|
w.Header().Add("content-type", "text/css")
|
||||||
open_n_send(next(path, 2))
|
io.WriteString(w, Templates["css/skunky.css"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
http.HandleFunc("/", handle)
|
http.HandleFunc("/", handle)
|
||||||
http.ListenAndServe(CFG.Listen, nil)
|
http.ListenAndServe(CFG.Listen, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func err(e error) {
|
|
||||||
if e != nil {
|
|
||||||
println(e.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
620
app/util.go
620
app/util.go
@ -2,7 +2,6 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
u "net/url"
|
u "net/url"
|
||||||
@ -17,19 +16,36 @@ import (
|
|||||||
"golang.org/x/net/html"
|
"golang.org/x/net/html"
|
||||||
)
|
)
|
||||||
|
|
||||||
// парсинг темплейтов
|
/* INTERNAL */
|
||||||
|
func exit(msg string, code int) {
|
||||||
|
println(msg)
|
||||||
|
os.Exit(code)
|
||||||
|
}
|
||||||
|
func try(e error) {
|
||||||
|
if e != nil {
|
||||||
|
println(e.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func try_with_exitstatus(err error, code int) {
|
||||||
|
if err != nil {
|
||||||
|
exit(err.Error(), code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// some crap for frontend
|
||||||
func (s skunkyart) ExecuteTemplate(file string, data any) {
|
func (s skunkyart) ExecuteTemplate(file string, data any) {
|
||||||
var buf strings.Builder
|
var buf strings.Builder
|
||||||
tmp := template.New(file)
|
tmp := template.New(file)
|
||||||
tmp, e := tmp.Parse(Templates[file])
|
tmp, e := tmp.Parse(Templates[file])
|
||||||
err(e)
|
try(e)
|
||||||
err(tmp.Execute(&buf, &data))
|
try(tmp.Execute(&buf, &data))
|
||||||
wr(s.Writer, buf.String())
|
wr(s.Writer, buf.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func UrlBuilder(strs ...string) string {
|
func UrlBuilder(strs ...string) string {
|
||||||
var str strings.Builder
|
var str strings.Builder
|
||||||
l := len(strs)
|
l := len(strs)
|
||||||
|
str.WriteString(Host)
|
||||||
str.WriteString(CFG.BasePath)
|
str.WriteString(CFG.BasePath)
|
||||||
for n, x := range strs {
|
for n, x := range strs {
|
||||||
str.WriteString(x)
|
str.WriteString(x)
|
||||||
@ -45,7 +61,7 @@ func (s skunkyart) ReturnHTTPError(status int) {
|
|||||||
|
|
||||||
var msg strings.Builder
|
var msg strings.Builder
|
||||||
msg.WriteString(`<html><link rel="stylesheet" href="`)
|
msg.WriteString(`<html><link rel="stylesheet" href="`)
|
||||||
msg.WriteString(UrlBuilder("gui", "css", "skunky.css"))
|
msg.WriteString(UrlBuilder("stylesheet"))
|
||||||
msg.WriteString(`" /><h1>`)
|
msg.WriteString(`" /><h1>`)
|
||||||
msg.WriteString(strconv.Itoa(status))
|
msg.WriteString(strconv.Itoa(status))
|
||||||
msg.WriteString(" - ")
|
msg.WriteString(" - ")
|
||||||
@ -55,7 +71,131 @@ func (s skunkyart) ReturnHTTPError(status int) {
|
|||||||
wr(s.Writer, msg.String())
|
wr(s.Writer, msg.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s skunkyart) ConvertDeviantArtUrlToSkunkyArt(url string) (output string) {
|
type Downloaded struct {
|
||||||
|
Headers http.Header
|
||||||
|
Status int
|
||||||
|
Body []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func Download(url string) (d Downloaded) {
|
||||||
|
cli := &http.Client{}
|
||||||
|
if CFG.DownloadProxy != "" {
|
||||||
|
u, e := u.Parse(CFG.DownloadProxy)
|
||||||
|
try(e)
|
||||||
|
cli.Transport = &http.Transport{Proxy: http.ProxyURL(u)}
|
||||||
|
}
|
||||||
|
|
||||||
|
req, e := http.NewRequest("GET", url, nil)
|
||||||
|
try(e)
|
||||||
|
req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0.0")
|
||||||
|
|
||||||
|
resp, e := cli.Do(req)
|
||||||
|
try(e)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
b, e := io.ReadAll(resp.Body)
|
||||||
|
try(e)
|
||||||
|
|
||||||
|
d.Body = b
|
||||||
|
d.Status = resp.StatusCode
|
||||||
|
d.Headers = resp.Header
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// caching
|
||||||
|
func (s skunkyart) DownloadAndSendMedia(subdomain, path string) {
|
||||||
|
var url strings.Builder
|
||||||
|
url.WriteString("https://images-wixmp-")
|
||||||
|
url.WriteString(subdomain)
|
||||||
|
url.WriteString(".wixmp.com/")
|
||||||
|
url.WriteString(path)
|
||||||
|
url.WriteString("?token=")
|
||||||
|
url.WriteString(s.Args.Get("token"))
|
||||||
|
|
||||||
|
if CFG.Cache.Enabled {
|
||||||
|
os.Mkdir(CFG.Cache.Path, 0700)
|
||||||
|
fname := CFG.Cache.Path + "/" + base64.StdEncoding.EncodeToString([]byte(subdomain+path))
|
||||||
|
file, e := os.Open(fname)
|
||||||
|
|
||||||
|
if e != nil {
|
||||||
|
dwnld := Download(url.String())
|
||||||
|
if dwnld.Status == 200 && dwnld.Headers["Content-Type"][0][:5] == "image" {
|
||||||
|
try(os.WriteFile(fname, dwnld.Body, 0700))
|
||||||
|
s.Writer.Write(dwnld.Body)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
file, e := io.ReadAll(file)
|
||||||
|
try(e)
|
||||||
|
s.Writer.Write(file)
|
||||||
|
}
|
||||||
|
} else if CFG.Proxy {
|
||||||
|
dwnld := Download(url.String())
|
||||||
|
s.Writer.Write(dwnld.Body)
|
||||||
|
} else {
|
||||||
|
s.Writer.WriteHeader(403)
|
||||||
|
s.Writer.Write([]byte("Sorry, butt proxy on this instance are disabled."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitCacheSystem() {
|
||||||
|
c := &CFG.Cache
|
||||||
|
for {
|
||||||
|
dir, e := os.Open(c.Path)
|
||||||
|
try(e)
|
||||||
|
stat, e := dir.Stat()
|
||||||
|
try(e)
|
||||||
|
|
||||||
|
dirnames, e := dir.Readdirnames(-1)
|
||||||
|
try(e)
|
||||||
|
for _, a := range dirnames {
|
||||||
|
a = c.Path + "/" + a
|
||||||
|
if c.Lifetime != 0 {
|
||||||
|
now := time.Now().UnixMilli()
|
||||||
|
|
||||||
|
f, _ := os.Stat(a)
|
||||||
|
stat := f.Sys().(*syscall.Stat_t)
|
||||||
|
time := time.Unix(stat.Ctim.Unix()).UnixMilli()
|
||||||
|
|
||||||
|
if time+c.Lifetime <= now {
|
||||||
|
try(os.RemoveAll(a))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c.MaxSize != 0 && stat.Size() > c.MaxSize {
|
||||||
|
try(os.RemoveAll(a))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dir.Close()
|
||||||
|
time.Sleep(time.Second * time.Duration(CFG.Cache.UpdateInterval))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CopyTemplatesToMemory() {
|
||||||
|
for _, dirname := range CFG.Dirs {
|
||||||
|
dir, e := os.ReadDir(dirname)
|
||||||
|
try_with_exitstatus(e, 1)
|
||||||
|
|
||||||
|
for _, x := range dir {
|
||||||
|
n := dirname + "/" + x.Name()
|
||||||
|
file, e := os.ReadFile(n)
|
||||||
|
try_with_exitstatus(e, 1)
|
||||||
|
Templates[n] = string(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* PARSING HELPERS */
|
||||||
|
func ParseMedia(media devianter.Media) string {
|
||||||
|
url := devianter.UrlFromMedia(media)
|
||||||
|
if len(url) != 0 && CFG.Proxy {
|
||||||
|
url = url[21:]
|
||||||
|
dot := strings.Index(url, ".")
|
||||||
|
|
||||||
|
return UrlBuilder("media", "file", url[:dot], url[dot+11:])
|
||||||
|
}
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConvertDeviantArtUrlToSkunkyArt(url string) (output string) {
|
||||||
if len(url) > 32 && url[27:32] != "stash" {
|
if len(url) > 32 && url[27:32] != "stash" {
|
||||||
url = url[27:]
|
url = url[27:]
|
||||||
toart := strings.Index(url, "/art/")
|
toart := strings.Index(url, "/art/")
|
||||||
@ -78,13 +218,7 @@ func BuildUserPlate(name string) string {
|
|||||||
return htm.String()
|
return htm.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
type text struct {
|
func GetValueOfTag(t *html.Tokenizer) string {
|
||||||
TXT string
|
|
||||||
from int
|
|
||||||
to int
|
|
||||||
}
|
|
||||||
|
|
||||||
func tagval(t *html.Tokenizer) string {
|
|
||||||
for tt := t.Next(); ; {
|
for tt := t.Next(); ; {
|
||||||
switch tt {
|
switch tt {
|
||||||
default:
|
default:
|
||||||
@ -95,198 +229,14 @@ func tagval(t *html.Tokenizer) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseDescription(dscr devianter.Text) string {
|
|
||||||
var parseddescription strings.Builder
|
|
||||||
TagBuilder := func(tag string, content string) string {
|
|
||||||
if tag != "" {
|
|
||||||
var htm strings.Builder
|
|
||||||
htm.WriteString("<")
|
|
||||||
htm.WriteString(tag)
|
|
||||||
htm.WriteString(">")
|
|
||||||
|
|
||||||
htm.WriteString(content)
|
|
||||||
|
|
||||||
htm.WriteString("</")
|
|
||||||
htm.WriteString(tag)
|
|
||||||
htm.WriteString(">")
|
|
||||||
return htm.String()
|
|
||||||
}
|
|
||||||
return content
|
|
||||||
}
|
|
||||||
DeleteSpywareFromUrl := func(url string) string {
|
|
||||||
if len(url) > 42 && url[:42] == "https://www.deviantart.com/users/outgoing?" {
|
|
||||||
url = url[42:]
|
|
||||||
}
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
|
|
||||||
if description, dl := dscr.Html.Markup, len(dscr.Html.Markup); dl != 0 &&
|
|
||||||
description[0] == '{' &&
|
|
||||||
description[dl-1] == '}' {
|
|
||||||
var descr struct {
|
|
||||||
Blocks []struct {
|
|
||||||
Text, Type string
|
|
||||||
InlineStyleRanges []struct {
|
|
||||||
Offset, Length int
|
|
||||||
Style string
|
|
||||||
}
|
|
||||||
EntityRanges []struct {
|
|
||||||
Offset, Length int
|
|
||||||
Key int
|
|
||||||
}
|
|
||||||
Data struct {
|
|
||||||
TextAlignment string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EntityMap map[string]struct {
|
|
||||||
Type string
|
|
||||||
Data struct {
|
|
||||||
Url string
|
|
||||||
Config struct {
|
|
||||||
Aligment string
|
|
||||||
Width int
|
|
||||||
}
|
|
||||||
Data devianter.Deviation
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
e := json.Unmarshal([]byte(description), &descr)
|
|
||||||
err(e)
|
|
||||||
|
|
||||||
entities := make(map[int]devianter.Deviation)
|
|
||||||
urls := make(map[int]string)
|
|
||||||
for n, x := range descr.EntityMap {
|
|
||||||
num, _ := strconv.Atoi(n)
|
|
||||||
if x.Data.Url != "" {
|
|
||||||
urls[num] = DeleteSpywareFromUrl(x.Data.Url)
|
|
||||||
}
|
|
||||||
entities[num] = x.Data.Data
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, x := range descr.Blocks {
|
|
||||||
ranges := make(map[int]text)
|
|
||||||
|
|
||||||
for i, rngs := range x.InlineStyleRanges {
|
|
||||||
var tag string
|
|
||||||
|
|
||||||
switch rngs.Style {
|
|
||||||
case "BOLD":
|
|
||||||
tag = "b"
|
|
||||||
case "UNDERLINE":
|
|
||||||
tag = "u"
|
|
||||||
case "ITALIC":
|
|
||||||
tag = "i"
|
|
||||||
}
|
|
||||||
|
|
||||||
fromto := rngs.Offset + rngs.Length
|
|
||||||
ranges[i] = text{
|
|
||||||
TXT: TagBuilder(tag, x.Text[rngs.Offset:fromto]),
|
|
||||||
from: rngs.Offset,
|
|
||||||
to: fromto,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch x.Type {
|
|
||||||
case "atomic":
|
|
||||||
d := entities[x.EntityRanges[0].Key]
|
|
||||||
parseddescription.WriteString(`<img width="50%" src="`)
|
|
||||||
parseddescription.WriteString(ParseMedia(d.Media))
|
|
||||||
parseddescription.WriteString(`" title="`)
|
|
||||||
parseddescription.WriteString(d.Author.Username)
|
|
||||||
parseddescription.WriteString(" - ")
|
|
||||||
parseddescription.WriteString(d.Title)
|
|
||||||
parseddescription.WriteString(`">`)
|
|
||||||
case "unstyled":
|
|
||||||
if len(ranges) != 0 {
|
|
||||||
for _, r := range ranges {
|
|
||||||
var tag string
|
|
||||||
switch x.Type {
|
|
||||||
case "header-two":
|
|
||||||
tag = "h2"
|
|
||||||
}
|
|
||||||
|
|
||||||
parseddescription.WriteString(x.Text[:r.from])
|
|
||||||
if len(urls) != 0 && len(x.EntityRanges) != 0 {
|
|
||||||
ra := &x.EntityRanges[0]
|
|
||||||
|
|
||||||
parseddescription.WriteString(`<a target="_blank" href="`)
|
|
||||||
parseddescription.WriteString(urls[ra.Key])
|
|
||||||
parseddescription.WriteString(`">`)
|
|
||||||
parseddescription.WriteString(r.TXT)
|
|
||||||
parseddescription.WriteString(`</a>`)
|
|
||||||
} else {
|
|
||||||
parseddescription.WriteString(r.TXT)
|
|
||||||
}
|
|
||||||
parseddescription.WriteString(TagBuilder(tag, x.Text[r.to:]))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
parseddescription.WriteString(x.Text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
parseddescription.WriteString("<br>")
|
|
||||||
}
|
|
||||||
} else if dl != 0 {
|
|
||||||
for tt := html.NewTokenizer(strings.NewReader(dscr.Html.Markup)); ; {
|
|
||||||
switch tt.Next() {
|
|
||||||
case html.ErrorToken:
|
|
||||||
return parseddescription.String()
|
|
||||||
case html.StartTagToken, html.EndTagToken, html.SelfClosingTagToken:
|
|
||||||
token := tt.Token()
|
|
||||||
switch token.Data {
|
|
||||||
case "a":
|
|
||||||
for _, a := range token.Attr {
|
|
||||||
if a.Key == "href" {
|
|
||||||
url := DeleteSpywareFromUrl(a.Val)
|
|
||||||
parseddescription.WriteString(`<a target="_blank" href="`)
|
|
||||||
parseddescription.WriteString(url)
|
|
||||||
parseddescription.WriteString(`">`)
|
|
||||||
parseddescription.WriteString(tagval(tt))
|
|
||||||
parseddescription.WriteString("</a> ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "img":
|
|
||||||
var uri, title string
|
|
||||||
for b, a := range token.Attr {
|
|
||||||
switch a.Key {
|
|
||||||
case "src":
|
|
||||||
if len(a.Val) > 9 && a.Val[8:9] == "e" {
|
|
||||||
uri = UrlBuilder("media", "emojitar", a.Val[37:len(a.Val)-4], "?type=e")
|
|
||||||
}
|
|
||||||
case "title":
|
|
||||||
title = a.Val
|
|
||||||
}
|
|
||||||
if title != "" {
|
|
||||||
for x := -1; x < b; x++ {
|
|
||||||
parseddescription.WriteString(`<img src="`)
|
|
||||||
parseddescription.WriteString(uri)
|
|
||||||
parseddescription.WriteString(`" title="`)
|
|
||||||
parseddescription.WriteString(title)
|
|
||||||
parseddescription.WriteString(`">`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "br", "li", "ul", "p", "b":
|
|
||||||
parseddescription.WriteString(token.String())
|
|
||||||
case "div":
|
|
||||||
parseddescription.WriteString("<p> ")
|
|
||||||
}
|
|
||||||
case html.TextToken:
|
|
||||||
parseddescription.Write(tt.Text())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return parseddescription.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// навигация по страницам
|
// навигация по страницам
|
||||||
type dlist struct {
|
type DeviationList struct {
|
||||||
Pages int
|
Pages int
|
||||||
More bool
|
More bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: на некоротрых артах первая страница может вызывать полное отсутствие панели навигации.
|
// FIXME: на некоротрых артах первая страница может вызывать полное отсутствие панели навигации.
|
||||||
func (s skunkyart) NavBase(c dlist) string {
|
func (s skunkyart) NavBase(c DeviationList) string {
|
||||||
// TODO: сделать понятнее
|
// TODO: сделать понятнее
|
||||||
// навигация по страницам
|
// навигация по страницам
|
||||||
var list strings.Builder
|
var list strings.Builder
|
||||||
@ -333,7 +283,7 @@ func (s skunkyart) NavBase(c dlist) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// вперёд
|
// вперёд
|
||||||
for x := p; x <= p+6; x++ {
|
for x := p; x <= p+6 && c.Pages > p+6; x++ {
|
||||||
if x == p {
|
if x == p {
|
||||||
prevrev("", x, true)
|
prevrev("", x, true)
|
||||||
x++
|
x++
|
||||||
@ -346,277 +296,9 @@ func (s skunkyart) NavBase(c dlist) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// вперёд-назад
|
// вперёд-назад
|
||||||
if p != 417 || c.More {
|
if c.More {
|
||||||
prevrev("| Next =>", p+1, false)
|
prevrev("| Next =>", p+1, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
return list.String()
|
return list.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s skunkyart) DeviationList(devs []devianter.Deviation, content ...dlist) string {
|
|
||||||
var list strings.Builder
|
|
||||||
if s.Atom && s.Page > 1 {
|
|
||||||
s.ReturnHTTPError(400)
|
|
||||||
return ""
|
|
||||||
} else if s.Atom {
|
|
||||||
list.WriteString(`<?xml version="1.0" encoding="UTF-8"?><feed xmlns:media="http://search.yahoo.com/mrss/" xmlns="http://www.w3.org/2005/Atom">`)
|
|
||||||
list.WriteString(`<title>SkunkyArt</title>`)
|
|
||||||
// list.WriteString(`<link rel="alternate" href="HOMEPAGE_URL"/><link href="FEED_URL" rel="self"/>`)
|
|
||||||
} else {
|
|
||||||
list.WriteString(`<div class="content">`)
|
|
||||||
}
|
|
||||||
for _, data := range devs {
|
|
||||||
if !(data.NSFW && !CFG.Nsfw) {
|
|
||||||
url := ParseMedia(data.Media)
|
|
||||||
if s.Atom {
|
|
||||||
id := strconv.Itoa(data.ID)
|
|
||||||
list.WriteString(`<entry><author><name>`)
|
|
||||||
list.WriteString(data.Author.Username)
|
|
||||||
list.WriteString(`</name></author><title>`)
|
|
||||||
list.WriteString(data.Title)
|
|
||||||
list.WriteString(`</title><link rel="alternate" type="text/html" href="`)
|
|
||||||
list.WriteString(UrlBuilder("post", data.Author.Username, "atom-"+id))
|
|
||||||
list.WriteString(`"/><id>`)
|
|
||||||
list.WriteString(id)
|
|
||||||
list.WriteString(`</id><published>`)
|
|
||||||
list.WriteString(data.PublishedTime.UTC().Format("Mon, 02 Jan 2006 15:04:05 -0700"))
|
|
||||||
list.WriteString(`</published>`)
|
|
||||||
list.WriteString(`<media:group><media:title>`)
|
|
||||||
list.WriteString(data.Title)
|
|
||||||
list.WriteString(`</media:title><media:thumbinal url="`)
|
|
||||||
list.WriteString(url)
|
|
||||||
list.WriteString(`"/></media:group><content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><a href="`)
|
|
||||||
list.WriteString(data.Url)
|
|
||||||
list.WriteString(`"><img src="`)
|
|
||||||
list.WriteString(url)
|
|
||||||
list.WriteString(`"/></a><p>`)
|
|
||||||
list.WriteString(ParseDescription(data.TextContent))
|
|
||||||
list.WriteString(`</p></div></content></entry>`)
|
|
||||||
} else {
|
|
||||||
list.WriteString(`<div class="block">`)
|
|
||||||
if url != "" {
|
|
||||||
list.WriteString(`<a title="open/download" href="`)
|
|
||||||
list.WriteString(url)
|
|
||||||
list.WriteString(`"><img loading="lazy" src="`)
|
|
||||||
list.WriteString(url)
|
|
||||||
list.WriteString(`" width="15%"></a>`)
|
|
||||||
} else {
|
|
||||||
list.WriteString(`<h1>[ TEXT ]</h1>`)
|
|
||||||
}
|
|
||||||
list.WriteString(`<br><a href="`)
|
|
||||||
list.WriteString(s.ConvertDeviantArtUrlToSkunkyArt(data.Url))
|
|
||||||
list.WriteString(`">`)
|
|
||||||
list.WriteString(data.Author.Username)
|
|
||||||
list.WriteString(" - ")
|
|
||||||
list.WriteString(data.Title)
|
|
||||||
|
|
||||||
// шильдики нсфв, аи и ежедневного поста
|
|
||||||
if data.NSFW {
|
|
||||||
list.WriteString(` [<span class="nsfw">NSFW</span>]`)
|
|
||||||
}
|
|
||||||
if data.AI {
|
|
||||||
list.WriteString(" [🤖]")
|
|
||||||
}
|
|
||||||
if data.DD {
|
|
||||||
list.WriteString(` [<span class="dd">DD</span>]`)
|
|
||||||
}
|
|
||||||
|
|
||||||
list.WriteString("</a></div>")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.Atom {
|
|
||||||
list.WriteString("</feed>")
|
|
||||||
s.Writer.Write([]byte(list.String()))
|
|
||||||
return ""
|
|
||||||
} else {
|
|
||||||
list.WriteString("</div>")
|
|
||||||
if content != nil {
|
|
||||||
list.WriteString(s.NavBase(content[0]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return list.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s skunkyart) ParseComments(c devianter.Comments) string {
|
|
||||||
var cmmts strings.Builder
|
|
||||||
replied := make(map[int]string)
|
|
||||||
|
|
||||||
cmmts.WriteString("<details><summary>Comments: <b>")
|
|
||||||
cmmts.WriteString(strconv.Itoa(c.Total))
|
|
||||||
cmmts.WriteString("</b></summary>")
|
|
||||||
for _, x := range c.Thread {
|
|
||||||
replied[x.ID] = x.User.Username
|
|
||||||
cmmts.WriteString(`<div class="msg`)
|
|
||||||
if x.Parent > 0 {
|
|
||||||
cmmts.WriteString(` reply`)
|
|
||||||
}
|
|
||||||
cmmts.WriteString(`"><p id="`)
|
|
||||||
cmmts.WriteString(strconv.Itoa(x.ID))
|
|
||||||
cmmts.WriteString(`"><img src="`)
|
|
||||||
cmmts.WriteString(UrlBuilder("media", "emojitar", x.User.Username, "?type=a"))
|
|
||||||
cmmts.WriteString(`" width="30px" height="30px"><a href="`)
|
|
||||||
cmmts.WriteString(UrlBuilder("group_user", "?q=", x.User.Username, "&type=a"))
|
|
||||||
cmmts.WriteString(`"><b`)
|
|
||||||
cmmts.WriteString(` class="`)
|
|
||||||
if x.User.Banned {
|
|
||||||
cmmts.WriteString(`banned`)
|
|
||||||
}
|
|
||||||
if x.Author {
|
|
||||||
cmmts.WriteString(`author`)
|
|
||||||
}
|
|
||||||
cmmts.WriteString(`">`)
|
|
||||||
cmmts.WriteString(x.User.Username)
|
|
||||||
cmmts.WriteString("</b></a> ")
|
|
||||||
|
|
||||||
if x.Parent > 0 {
|
|
||||||
cmmts.WriteString(` In reply to <a href="#`)
|
|
||||||
cmmts.WriteString(strconv.Itoa(x.Parent))
|
|
||||||
cmmts.WriteString(`">`)
|
|
||||||
if replied[x.Parent] == "" {
|
|
||||||
cmmts.WriteString("???")
|
|
||||||
} else {
|
|
||||||
cmmts.WriteString(replied[x.Parent])
|
|
||||||
}
|
|
||||||
cmmts.WriteString("</a>")
|
|
||||||
}
|
|
||||||
cmmts.WriteString(" [")
|
|
||||||
cmmts.WriteString(x.Posted.UTC().String())
|
|
||||||
cmmts.WriteString("]<p>")
|
|
||||||
|
|
||||||
cmmts.WriteString(ParseDescription(x.TextContent))
|
|
||||||
cmmts.WriteString("<p>👍: ")
|
|
||||||
cmmts.WriteString(strconv.Itoa(x.Likes))
|
|
||||||
cmmts.WriteString(" ⏩: ")
|
|
||||||
cmmts.WriteString(strconv.Itoa(x.Replies))
|
|
||||||
cmmts.WriteString("</p></div>\n")
|
|
||||||
}
|
|
||||||
cmmts.WriteString(s.NavBase(dlist{
|
|
||||||
Pages: 0,
|
|
||||||
More: c.HasMore,
|
|
||||||
}))
|
|
||||||
cmmts.WriteString("</details>")
|
|
||||||
return cmmts.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseMedia(media devianter.Media) string {
|
|
||||||
url := devianter.UrlFromMedia(media)
|
|
||||||
if len(url) != 0 {
|
|
||||||
url = url[21:]
|
|
||||||
dot := strings.Index(url, ".")
|
|
||||||
|
|
||||||
return UrlBuilder("media", "file", url[:dot], "/", url[dot+10:])
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s skunkyart) DownloadAndSendMedia(subdomain, path string) {
|
|
||||||
var url strings.Builder
|
|
||||||
url.WriteString("https://images-wixmp-")
|
|
||||||
url.WriteString(subdomain)
|
|
||||||
url.WriteString(".wixmp.com/")
|
|
||||||
url.WriteString(path)
|
|
||||||
url.WriteString("?token=")
|
|
||||||
url.WriteString(s.Args.Get("token"))
|
|
||||||
|
|
||||||
download := func() (body []byte, status int, headers http.Header) {
|
|
||||||
cli := &http.Client{}
|
|
||||||
if CFG.WixmpProxy != "" {
|
|
||||||
u, e := u.Parse(CFG.WixmpProxy)
|
|
||||||
err(e)
|
|
||||||
cli.Transport = &http.Transport{Proxy: http.ProxyURL(u)}
|
|
||||||
}
|
|
||||||
|
|
||||||
req, e := http.NewRequest("GET", url.String(), nil)
|
|
||||||
err(e)
|
|
||||||
req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0.0")
|
|
||||||
|
|
||||||
resp, e := cli.Do(req)
|
|
||||||
err(e)
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
b, e := io.ReadAll(resp.Body)
|
|
||||||
err(e)
|
|
||||||
return b, resp.StatusCode, resp.Header
|
|
||||||
}
|
|
||||||
|
|
||||||
if CFG.Cache.Enabled {
|
|
||||||
os.Mkdir(CFG.Cache.Path, 0700)
|
|
||||||
fname := CFG.Cache.Path + "/" + base64.StdEncoding.EncodeToString([]byte(subdomain+path))
|
|
||||||
file, e := os.Open(fname)
|
|
||||||
|
|
||||||
if e != nil {
|
|
||||||
b, status, headers := download()
|
|
||||||
if status == 200 && headers["Content-Type"][0][:5] == "image" {
|
|
||||||
err(os.WriteFile(fname, b, 0700))
|
|
||||||
s.Writer.Write(b)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
file, e := io.ReadAll(file)
|
|
||||||
err(e)
|
|
||||||
s.Writer.Write(file)
|
|
||||||
}
|
|
||||||
} else if CFG.Proxy {
|
|
||||||
b, _, _ := download()
|
|
||||||
s.Writer.Write(b)
|
|
||||||
} else {
|
|
||||||
s.Writer.WriteHeader(403)
|
|
||||||
s.Writer.Write([]byte("Sorry, butt proxy on this instance disabled."))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func InitCacheSystem() {
|
|
||||||
c := &CFG.Cache
|
|
||||||
for {
|
|
||||||
dir, e := os.Open(c.Path)
|
|
||||||
err(e)
|
|
||||||
stat, e := dir.Stat()
|
|
||||||
err(e)
|
|
||||||
|
|
||||||
dirnames, e := dir.Readdirnames(-1)
|
|
||||||
err(e)
|
|
||||||
for _, a := range dirnames {
|
|
||||||
a = c.Path + "/" + a
|
|
||||||
rm := func() {
|
|
||||||
err(os.RemoveAll(a))
|
|
||||||
}
|
|
||||||
if c.Lifetime != 0 {
|
|
||||||
now := time.Now().UnixMilli()
|
|
||||||
|
|
||||||
f, _ := os.Stat(a)
|
|
||||||
stat := f.Sys().(*syscall.Stat_t)
|
|
||||||
time := time.Unix(stat.Ctim.Unix()).UnixMilli()
|
|
||||||
|
|
||||||
if time+c.Lifetime <= now {
|
|
||||||
rm()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if c.MaxSize != 0 && stat.Size() > c.MaxSize {
|
|
||||||
rm()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dir.Close()
|
|
||||||
time.Sleep(time.Second * time.Duration(CFG.Cache.UpdateInterval))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func CopyTemplatesToMemory() {
|
|
||||||
try := func(e error) {
|
|
||||||
if e != nil {
|
|
||||||
panic(e.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dir, e := os.ReadDir(CFG.TemplatesDir)
|
|
||||||
try(e)
|
|
||||||
|
|
||||||
for _, x := range dir {
|
|
||||||
n := CFG.TemplatesDir + "/" + x.Name()
|
|
||||||
file, e := os.ReadFile(n)
|
|
||||||
try(e)
|
|
||||||
Templates[n] = string(file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@ -17,17 +18,33 @@ var wr = io.WriteString
|
|||||||
var Templates = make(map[string]string)
|
var Templates = make(map[string]string)
|
||||||
|
|
||||||
type skunkyart struct {
|
type skunkyart struct {
|
||||||
Writer http.ResponseWriter
|
Writer http.ResponseWriter
|
||||||
|
|
||||||
Args url.Values
|
Args url.Values
|
||||||
BasePath string
|
BasePath string
|
||||||
Type rune
|
Type rune
|
||||||
Query, QueryRaw string
|
Query, QueryRaw string
|
||||||
Page int
|
Page int
|
||||||
Atom bool
|
Atom bool
|
||||||
Templates struct {
|
|
||||||
|
Templates struct {
|
||||||
About struct {
|
About struct {
|
||||||
Proxy bool
|
Proxy bool
|
||||||
Nsfw bool
|
Nsfw bool
|
||||||
|
Instances []struct {
|
||||||
|
Title string
|
||||||
|
Country string
|
||||||
|
Urls []struct {
|
||||||
|
I2P string `json:"i2p"`
|
||||||
|
Ygg string
|
||||||
|
Tor string
|
||||||
|
Clearnet string
|
||||||
|
}
|
||||||
|
Settings struct {
|
||||||
|
Nsfw bool
|
||||||
|
Proxy bool
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SomeList string
|
SomeList string
|
||||||
@ -127,7 +144,7 @@ func (s skunkyart) GRUser() {
|
|||||||
|
|
||||||
case "cover_deviation":
|
case "cover_deviation":
|
||||||
group.About.BGMeta = x.ModuleData.CoverDeviation.Deviation
|
group.About.BGMeta = x.ModuleData.CoverDeviation.Deviation
|
||||||
group.About.BGMeta.Url = s.ConvertDeviantArtUrlToSkunkyArt(group.About.BGMeta.Url)
|
group.About.BGMeta.Url = ConvertDeviantArtUrlToSkunkyArt(group.About.BGMeta.Url)
|
||||||
group.About.BG = ParseMedia(group.About.BGMeta.Media)
|
group.About.BG = ParseMedia(group.About.BGMeta.Media)
|
||||||
case "group_admins":
|
case "group_admins":
|
||||||
var htm strings.Builder
|
var htm strings.Builder
|
||||||
@ -146,7 +163,7 @@ func (s skunkyart) GRUser() {
|
|||||||
|
|
||||||
gallery := g.Gallery(s.Page, folderid)
|
gallery := g.Gallery(s.Page, folderid)
|
||||||
if folderid > 0 {
|
if folderid > 0 {
|
||||||
group.Gallery.List = s.DeviationList(gallery.Content.Results, dlist{
|
group.Gallery.List = s.DeviationList(gallery.Content.Results, DeviationList{
|
||||||
More: gallery.Content.HasMore,
|
More: gallery.Content.HasMore,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
@ -157,13 +174,18 @@ func (s skunkyart) GRUser() {
|
|||||||
for _, x := range x.ModuleData.Folders.Results {
|
for _, x := range x.ModuleData.Folders.Results {
|
||||||
folders.WriteString(`<div class="block folder-item">`)
|
folders.WriteString(`<div class="block folder-item">`)
|
||||||
|
|
||||||
folders.WriteString(`<a href="`)
|
if !(x.Thumb.NSFW && !CFG.Nsfw) {
|
||||||
folders.WriteString(s.ConvertDeviantArtUrlToSkunkyArt(x.Thumb.Url))
|
folders.WriteString(`<a href="`)
|
||||||
folders.WriteString(`"><img loading="lazy" src="`)
|
folders.WriteString(ConvertDeviantArtUrlToSkunkyArt(x.Thumb.Url))
|
||||||
folders.WriteString(ParseMedia(x.Thumb.Media))
|
folders.WriteString(`"><img loading="lazy" src="`)
|
||||||
folders.WriteString(`" title="`)
|
folders.WriteString(ParseMedia(x.Thumb.Media))
|
||||||
folders.WriteString(x.Thumb.Title)
|
folders.WriteString(`" title="`)
|
||||||
folders.WriteString(`"></a><br>`)
|
folders.WriteString(x.Thumb.Title)
|
||||||
|
folders.WriteString(`"></a>`)
|
||||||
|
} else {
|
||||||
|
folders.WriteString(`<h1>[ <span class="nsfw">NSFW</span> ]</h1>`)
|
||||||
|
}
|
||||||
|
folders.WriteString("<br>")
|
||||||
|
|
||||||
folders.WriteString(`<a href="?folder=`)
|
folders.WriteString(`<a href="?folder=`)
|
||||||
folders.WriteString(strconv.Itoa(x.FolderId))
|
folders.WriteString(strconv.Itoa(x.FolderId))
|
||||||
@ -182,7 +204,7 @@ func (s skunkyart) GRUser() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if x.Name == "folder_deviations" {
|
if x.Name == "folder_deviations" {
|
||||||
group.Gallery.List = s.DeviationList(x.ModuleData.Folder.Deviations, dlist{
|
group.Gallery.List = s.DeviationList(x.ModuleData.Folder.Deviations, DeviationList{
|
||||||
Pages: x.ModuleData.Folder.Pages,
|
Pages: x.ModuleData.Folder.Pages,
|
||||||
More: x.ModuleData.Folder.HasMore,
|
More: x.ModuleData.Folder.HasMore,
|
||||||
})
|
})
|
||||||
@ -217,7 +239,7 @@ func (s skunkyart) Deviation(author, postname string) {
|
|||||||
post.Post.IMG = ParseMedia(post.Post.Deviation.Media)
|
post.Post.IMG = ParseMedia(post.Post.Deviation.Media)
|
||||||
for _, x := range post.Post.Deviation.Extended.RelatedContent {
|
for _, x := range post.Post.Deviation.Extended.RelatedContent {
|
||||||
if len(x.Deviations) != 0 {
|
if len(x.Deviations) != 0 {
|
||||||
post.Related = s.DeviationList(x.Deviations)
|
post.Related += s.DeviationList(x.Deviations)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -247,7 +269,7 @@ func (s skunkyart) Deviation(author, postname string) {
|
|||||||
|
|
||||||
func (s skunkyart) DD() {
|
func (s skunkyart) DD() {
|
||||||
dd := devianter.DailyDeviationsFunc(s.Page)
|
dd := devianter.DailyDeviationsFunc(s.Page)
|
||||||
s.Templates.SomeList = s.DeviationList(dd.Deviations, dlist{
|
s.Templates.SomeList = s.DeviationList(dd.Deviations, DeviationList{
|
||||||
Pages: 0,
|
Pages: 0,
|
||||||
More: dd.HasMore,
|
More: dd.HasMore,
|
||||||
})
|
})
|
||||||
@ -281,17 +303,13 @@ func (s skunkyart) Search() {
|
|||||||
url.WriteString(strconv.Itoa(10 * s.Page))
|
url.WriteString(strconv.Itoa(10 * s.Page))
|
||||||
}
|
}
|
||||||
|
|
||||||
r, err := http.Get(url.String())
|
dwnld := Download(url.String())
|
||||||
if err != nil {
|
|
||||||
s.ReturnHTTPError(502)
|
|
||||||
}
|
|
||||||
defer r.Body.Close()
|
|
||||||
|
|
||||||
for z := html.NewTokenizer(r.Body); ; {
|
for z := html.NewTokenizer(strings.NewReader(string(dwnld.Body))); ; {
|
||||||
if n, token := z.Next(), z.Token(); n == html.StartTagToken && token.Data == "a" {
|
if n, token := z.Next(), z.Token(); n == html.StartTagToken && token.Data == "a" {
|
||||||
for _, x := range token.Attr {
|
for _, x := range token.Attr {
|
||||||
if x.Key == "class" && x.Val == "u regular username" {
|
if x.Key == "class" && x.Val == "u regular username" {
|
||||||
usernames[num] = tagval(z)
|
usernames[num] = GetValueOfTag(z)
|
||||||
num++
|
num++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -308,15 +326,17 @@ func (s skunkyart) Search() {
|
|||||||
ss.List += BuildUserPlate(usernames[x])
|
ss.List += BuildUserPlate(usernames[x])
|
||||||
}
|
}
|
||||||
ss.List += `</div>`
|
ss.List += `</div>`
|
||||||
ss.List += s.NavBase(dlist{})
|
ss.List += s.NavBase(DeviationList{
|
||||||
|
More: true,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
s.ReturnHTTPError(400)
|
s.ReturnHTTPError(400)
|
||||||
}
|
}
|
||||||
err(e)
|
try(e)
|
||||||
|
|
||||||
if s.Type != 'r' {
|
if s.Type != 'r' {
|
||||||
ss.List = s.DeviationList(ss.Content.Results, dlist{
|
ss.List = s.DeviationList(ss.Content.Results, DeviationList{
|
||||||
Pages: ss.Content.Pages,
|
Pages: ss.Content.Pages,
|
||||||
More: ss.Content.HasMore,
|
More: ss.Content.HasMore,
|
||||||
})
|
})
|
||||||
@ -340,5 +360,6 @@ func (s skunkyart) Emojitar(name string) {
|
|||||||
func (s skunkyart) About() {
|
func (s skunkyart) About() {
|
||||||
s.Templates.About.Nsfw = CFG.Nsfw
|
s.Templates.About.Nsfw = CFG.Nsfw
|
||||||
s.Templates.About.Proxy = CFG.Proxy
|
s.Templates.About.Proxy = CFG.Proxy
|
||||||
|
try(json.Unmarshal([]byte(Templates["instances.json"]), &s.Templates.About))
|
||||||
s.ExecuteTemplate("html/about.htm", &s)
|
s.ExecuteTemplate("html/about.htm", &s)
|
||||||
}
|
}
|
||||||
|
@ -8,8 +8,11 @@
|
|||||||
"max-size": 100000,
|
"max-size": 100000,
|
||||||
"update-interval": 5
|
"update-interval": 5
|
||||||
},
|
},
|
||||||
"templates-dir": "html",
|
"dirs-to-memory": [
|
||||||
"wixmp-proxy": "http://127.0.0.1:8080",
|
"html",
|
||||||
|
"css"
|
||||||
|
],
|
||||||
|
"download-proxy": "",
|
||||||
"proxy": true,
|
"proxy": true,
|
||||||
"nsfw": true
|
"nsfw": false
|
||||||
}
|
}
|
116
css/skunky.css
116
css/skunky.css
@ -1,5 +1,6 @@
|
|||||||
|
/* TAGS */
|
||||||
html {
|
html {
|
||||||
font-family: ubuntu;
|
font-family: Ubuntu;
|
||||||
background-color:black;
|
background-color:black;
|
||||||
color: rgb(234, 216, 216);
|
color: rgb(234, 216, 216);
|
||||||
}
|
}
|
||||||
@ -8,7 +9,7 @@ a {
|
|||||||
color: cadetblue;
|
color: cadetblue;
|
||||||
}
|
}
|
||||||
a:hover {
|
a:hover {
|
||||||
color: yellow;
|
color: #d0ff00;
|
||||||
transition: 400ms;
|
transition: 400ms;
|
||||||
}
|
}
|
||||||
header h1 {
|
header h1 {
|
||||||
@ -28,37 +29,8 @@ form input, button, select {
|
|||||||
border: 0px;
|
border: 0px;
|
||||||
border-radius: 1px;
|
border-radius: 1px;
|
||||||
}
|
}
|
||||||
.nsfw, .about-false {
|
|
||||||
color: red;
|
/* BLOCKS */
|
||||||
}
|
|
||||||
.about-true {
|
|
||||||
color: green;
|
|
||||||
}
|
|
||||||
.author {
|
|
||||||
color: seagreen;
|
|
||||||
}
|
|
||||||
.msg {
|
|
||||||
background-color: #091f19;
|
|
||||||
color: whitesmoke;
|
|
||||||
width: fit-content;
|
|
||||||
max-width: 90%;
|
|
||||||
padding: 4px;
|
|
||||||
border-radius: 2px;
|
|
||||||
margin-top: 6px;
|
|
||||||
text-wrap: pretty;
|
|
||||||
transition: 350ms;
|
|
||||||
}
|
|
||||||
.msg:hover {
|
|
||||||
background-color: #134134;
|
|
||||||
}
|
|
||||||
.reply {
|
|
||||||
border-radius: 0px 2px 2px 0px;
|
|
||||||
border-left: #258268 solid;
|
|
||||||
margin-left: 40px;
|
|
||||||
}
|
|
||||||
.dd {
|
|
||||||
color: rgb(160, 0, 147);
|
|
||||||
}
|
|
||||||
.content {
|
.content {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
@ -82,12 +54,59 @@ form input, button, select {
|
|||||||
border: 3px solid #4d27d6;
|
border: 3px solid #4d27d6;
|
||||||
transition: 400ms;
|
transition: 400ms;
|
||||||
}
|
}
|
||||||
.block img, .plates .user-plate img{
|
.block img, .plates .user-plate img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.block p {
|
.block p {
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* MESSAGES */
|
||||||
|
.msg {
|
||||||
|
background-color: #091f19;
|
||||||
|
color: whitesmoke;
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 90%;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
margin-top: 6px;
|
||||||
|
text-wrap: pretty;
|
||||||
|
transition: 350ms;
|
||||||
|
}
|
||||||
|
.msg:hover {
|
||||||
|
background-color: #134134;
|
||||||
|
}
|
||||||
|
.reply {
|
||||||
|
border-radius: 0px 2px 2px 0px;
|
||||||
|
border-left: #258268 solid;
|
||||||
|
margin-left: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FOLDER & PLATES */
|
||||||
|
.folders, .plates {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.folder-item {
|
||||||
|
background-color: #060820;
|
||||||
|
border: 3px solid #060820;
|
||||||
|
width: 10%
|
||||||
|
}
|
||||||
|
.admins .user-plate {
|
||||||
|
width: 5%;
|
||||||
|
background-color: #011522;
|
||||||
|
}
|
||||||
|
.plates .user-plate {
|
||||||
|
margin-left: 1%;
|
||||||
|
margin-bottom: 1%;
|
||||||
|
background-color: #091f19;
|
||||||
|
padding: 3px;
|
||||||
|
word-break: break-all;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 15%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* USER/GROUP BACKGROUNDS */
|
||||||
.ubg {
|
.ubg {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@ -96,29 +115,22 @@ form input, button, select {
|
|||||||
.ubg img {
|
.ubg img {
|
||||||
width: 20%;
|
width: 20%;
|
||||||
}
|
}
|
||||||
.folders {
|
|
||||||
display: flex
|
/* COLORS */
|
||||||
|
.nsfw, .about-false {
|
||||||
|
color: red;
|
||||||
}
|
}
|
||||||
.folder-item {
|
.about-true {
|
||||||
background-color: #060820;
|
color: green;
|
||||||
border: 3px solid #060820;
|
|
||||||
}
|
}
|
||||||
.plates {
|
.author {
|
||||||
display: flex
|
color: seagreen;
|
||||||
}
|
}
|
||||||
.admins .user-plate {
|
.dd {
|
||||||
width: 5%;
|
color: rgb(160, 0, 147);
|
||||||
background-color: #011522;
|
|
||||||
}
|
|
||||||
.plates .user-plate {
|
|
||||||
margin-left: 1%;
|
|
||||||
background-color: #091f19;
|
|
||||||
padding: 3px;
|
|
||||||
word-break: break-all;
|
|
||||||
text-align: center;
|
|
||||||
max-width: 15%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* SCREEN OPTIMISATIONS */
|
||||||
@media screen and (orientation: portrait) {
|
@media screen and (orientation: portrait) {
|
||||||
header {
|
header {
|
||||||
scale: 155%;
|
scale: 155%;
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>SkunkyArt</title>
|
<title>SkunkyArt</title>
|
||||||
<link rel="stylesheet" href="{{.BasePath}}gui/css/skunky.css">
|
<link rel="stylesheet" href="{{.BasePath}}stylesheet">
|
||||||
</head>
|
</head>
|
||||||
<main>
|
<main>
|
||||||
<header>
|
<header>
|
||||||
@ -20,11 +20,44 @@
|
|||||||
<p>
|
<p>
|
||||||
SkunkyArt is an alternative frontend for deviantart.com, written in Go.
|
SkunkyArt is an alternative frontend for deviantart.com, written in Go.
|
||||||
</p>
|
</p>
|
||||||
|
<h3><a href="https://go.kde.org/matrix/#/#skunkyart:ebloid.ru" target="_blank">Room in Matrix</a></h3>
|
||||||
<h2>Instance settings:</h2>
|
<h2>Instance settings:</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li><b>NSFW</b>: <span class="about-{{.Templates.About.Nsfw}}">{{if .Templates.About.Nsfw}}YES{{else}}NO{{end}}</span></li>
|
<li><b>NSFW</b>: <span class="about-{{.Templates.About.Nsfw}}">{{if .Templates.About.Nsfw}}YES{{else}}NO{{end}}</span></li>
|
||||||
<li><b>Proxyfing</b>: <span class="about-{{.Templates.About.Proxy}}">{{if .Templates.About.Proxy}}YES{{else}}NO{{end}}</span></li>
|
<li><b>Proxyfing</b>: <span class="about-{{.Templates.About.Proxy}}">{{if .Templates.About.Proxy}}YES{{else}}NO{{end}}</span></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<h2>Instances:</h2>
|
||||||
|
<ul>
|
||||||
|
{{range .Templates.About.Instances}}
|
||||||
|
<li><u><b>{{.Title}}</b></u>:
|
||||||
|
<ul>
|
||||||
|
<li><b>Country</b>: {{.Country}}</li>
|
||||||
|
<li><b>URLs</b>: </li>
|
||||||
|
<ul>
|
||||||
|
{{range .Urls}}
|
||||||
|
{{if ne .I2P ""}}
|
||||||
|
<li><b>I2P</b>: <a href="{{.I2P}}">Yes</a></li>
|
||||||
|
{{end}}
|
||||||
|
{{if ne .Ygg ""}}
|
||||||
|
<li><b>Ygg</b>: <a href="{{.Ygg}}">Yes</a></li>
|
||||||
|
{{end}}
|
||||||
|
{{if ne .Tor ""}}
|
||||||
|
<li><b>Tor</b>: <a href="{{.Tor}}">Yes</a></li>
|
||||||
|
{{end}}
|
||||||
|
{{if ne .Clearnet ""}}
|
||||||
|
<li><b>Clearnet</b>: <a href="{{.Clearnet}}">{{.Clearnet}}</a></li>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
<li><b>Settings</b>: </li>
|
||||||
|
<ul>
|
||||||
|
<li><b>NSFW</b>: <span class="about-{{.Settings.Nsfw}}">{{if .Settings.Nsfw}}YES{{else}}NO{{end}}</span></li>
|
||||||
|
<li><b>Proxyfing</b>: <span class="about-{{.Settings.Proxy}}">{{if .Settings.Proxy}}YES{{else}}NO{{end}}</span></li>
|
||||||
|
</ul>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
<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>
|
<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>
|
||||||
</main>
|
</main>
|
||||||
</html>
|
</html>
|
@ -2,7 +2,7 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>SkunkyArt | {{.Templates.Deviation.Post.Deviation.Author.Username}} - {{.Templates.Deviation.Post.Deviation.Title}}</title>
|
<title>SkunkyArt | {{.Templates.Deviation.Post.Deviation.Author.Username}} - {{.Templates.Deviation.Post.Deviation.Title}}</title>
|
||||||
<link rel="stylesheet" href="{{.BasePath}}gui/css/skunky.css">
|
<link rel="stylesheet" href="{{.BasePath}}stylesheet">
|
||||||
<meta name="referrer" content="no-referrer" />
|
<meta name="referrer" content="no-referrer" />
|
||||||
</head>
|
</head>
|
||||||
<main>
|
<main>
|
||||||
@ -34,7 +34,7 @@
|
|||||||
{{if (ne .Templates.Deviation.Tags "")}}
|
{{if (ne .Templates.Deviation.Tags "")}}
|
||||||
{{.Templates.Deviation.Tags}}<br>
|
{{.Templates.Deviation.Tags}}<br>
|
||||||
{{end}}
|
{{end}}
|
||||||
<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>
|
<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>
|
||||||
<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>
|
<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>
|
||||||
</span>
|
</span>
|
||||||
{{if (ne .Templates.Deviation.Post.Description "")}}
|
{{if (ne .Templates.Deviation.Post.Description "")}}
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
gallery of {{.Templates.GroupUser.GR.Owner.Username}}
|
gallery of {{.Templates.GroupUser.GR.Owner.Username}}
|
||||||
{{end}}
|
{{end}}
|
||||||
</title>
|
</title>
|
||||||
<link rel="stylesheet" href="{{.BasePath}}gui/css/skunky.css">
|
<link rel="stylesheet" href="{{.BasePath}}stylesheet">
|
||||||
</head>
|
</head>
|
||||||
<main>
|
<main>
|
||||||
<header>
|
<header>
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>SkunkyArt</title>
|
<title>SkunkyArt</title>
|
||||||
<link rel="stylesheet" href="{{.}}gui/css/skunky.css"/>
|
<link rel="stylesheet" href="{{.}}stylesheet"/>
|
||||||
</head>
|
</head>
|
||||||
<main>
|
<main>
|
||||||
<center>
|
<center>
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>SkunkyArt</title>
|
<title>SkunkyArt</title>
|
||||||
<link rel="stylesheet" href="{{.BasePath}}gui/css/skunky.css">
|
<link rel="stylesheet" href="{{.BasePath}}stylesheet">
|
||||||
</head>
|
</head>
|
||||||
<main>
|
<main>
|
||||||
<header>
|
<header>
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>SkunkyArt | Search "{{.QueryRaw}}"</title>
|
<title>SkunkyArt | Search "{{.QueryRaw}}"</title>
|
||||||
<link rel="stylesheet" href="{{.BasePath}}gui/css/skunky.css">
|
<link rel="stylesheet" href="{{.BasePath}}stylesheet">
|
||||||
</head>
|
</head>
|
||||||
<main>
|
<main>
|
||||||
<header>
|
<header>
|
||||||
|
@ -1,14 +1,49 @@
|
|||||||
{
|
{
|
||||||
"instances": [{
|
"instances": [
|
||||||
"urls": [{
|
{
|
||||||
"i2p": "http://skunky.i2p/art",
|
"title": "skunky.ebloid.ru",
|
||||||
"tor": "http://skunky0wjkf8j8ajofgh98aOZjhu8f.onion/art",
|
"country": "Russia",
|
||||||
"ygg": "http://[324:71e:281a:9ed3::fa11]/skunkyart",
|
"urls": [{
|
||||||
"clearnet": "https://skunky.net/art"
|
"ygg": "http://[201:eba5:d1fc:bf7b:cfcb:a811:4b8b:7ea3]/art",
|
||||||
}],
|
"clearnet": "https://skunky.ebloid.ru/art"
|
||||||
"settings": {
|
}],
|
||||||
"nsfw": true,
|
"settings": {
|
||||||
"proxy": true
|
"nsfw": false,
|
||||||
|
"proxy": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "clovius.club",
|
||||||
|
"country": "Sweden",
|
||||||
|
"urls": [{
|
||||||
|
"clearnet": "https://skunky.clovius.club"
|
||||||
|
}],
|
||||||
|
"settings": {
|
||||||
|
"nsfw": true,
|
||||||
|
"proxy": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "bloat.cat",
|
||||||
|
"country": "Romania",
|
||||||
|
"urls": [{
|
||||||
|
"clearnet": "https://skunky.bloat.cat"
|
||||||
|
}],
|
||||||
|
"settings": {
|
||||||
|
"nsfw": true,
|
||||||
|
"proxy": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "frontendfriendly.xyz",
|
||||||
|
"country": "Finland",
|
||||||
|
"urls": [{
|
||||||
|
"clearnet": "https://skunkyart.frontendfriendly.xyz"
|
||||||
|
}],
|
||||||
|
"settings": {
|
||||||
|
"nsfw": true,
|
||||||
|
"proxy": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}]
|
]
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user