This commit is contained in:
lost+skunk 2024-09-23 09:38:32 +03:00
parent c39399403e
commit 191984b31e
19 changed files with 230 additions and 124 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
**/cache
**/config.json
**/skunkyart
**/skunkyart-*

View File

@ -1,6 +1,8 @@
JSON variant should be used from master — https://git.macaw.me/skunky/SkunkyArt/raw/branch/master/instances.json
|Instance|Yggdrasil|I2P|Tor|NSFW|Proxifying|Modified Sources|Country|
|:------:|:-------:|:-:|:-:|:--:|:--------:|:--------------:|:-----:|
|[skunky.ebloid.ru](https://skunky.ebloid.ru/art)|[Yes](http://[201:eba5:d1fc:bf7b:cfcb:a811:4b8b:7ea3]/art)|No|No| No | No | No | Russia |
|[skunky.ebloid.ru](https://skunky.ebloid.ru/art)|[Yes](http://[201:eba5:d1fc:bf7b:cfcb:a811:4b8b:7ea3]/art)|No|No| No | Yes | No | Russia |
|[clovius.club](https://skunky.clovius.club)|No|No|No| Yes | Yes | No | Sweden |
|[bloat.cat](https://skunky.bloat.cat)|No|No|No| Yes | Yes | No | Germany |
|[lumaeris.com](https://skunkyart.lumaeris.com)|No|No|No| Yes | Yes | No | Germany |

View File

@ -7,6 +7,12 @@ Instances: [`INSTANCES.md`](/skunky/SkunkyArt/src/branch/master/INSTANCES.md)
# EN 🇺🇸
## Description
SkunkyArt 🦨 — alternative frontend for DevianArt, which works without JS.
## Build (translated via DeepL)
It is recommended to build with the 'embed' tag because it embeds the presets in the binary. If you plan to modify the templates, then do not use this tag. You can also add the `-ldflags "-w -s"` argument (GCCGO has a different name for it — `gccgoflags`) to reduce the size of the output file. Here is an example:
`go build -tags embed -ldflags "-w -s"`
Pre-compiled binaries can be found in the [Releases](https://git.macaw.me/skunky/skunkyart/releases) tab.
## Setup
The sample config is in the `config.example.json` file. For custom config, use `--config` option.
See the [`SETUP.md`](/skunky/SkunkyArt/src/branch/master/SETUP.md) file for more info about directives.
@ -21,6 +27,12 @@ To do this, you must either make a PR by adding your instance to the `instances.
# RU 🇷🇺
## Описание
SkunkyArt 🦨 — альтернативный фронтенд к DeviantArt, который полностью работает без JS (JavaScript).
## Сборка
Рекомендуется производить сборку с тегом 'embed', поскольку он встраивает заготовки в бинарный файл. Если вы планируете изменять заготовки, то не используйте этот тег. Также вы можете добавить аргумент `-ldflags "-w -s"` (у GCCGO он называется по-другому — `gccgoflags`) для уменьшения размера выходного файла. Вот пример:
`go build -tags embed -ldflags "-w -s"`
Готовые бинари находятся во вкладке [Releases](https://git.macaw.me/skunky/skunkyart/releases).
## Настройка
Пример конфига находится в файле `config.example.json`. Чтобы указать свой конфиг, используйте cli-аргумент `--config`.
См. [`SETUP-RU.md`](/skunky/SkunkyArt/src/branch/master/SETUP-RU.md) для информации о настройки фронтенда.

13
REDIRECTS.md Normal file
View File

@ -0,0 +1,13 @@
# Search
* `deviantart.com/search?q=$QUERY` => `/search?q=$QUERY&type=all`
# Daily Deviations
* `deviantart.com` => `/dd`
# Deviations
* (`$USER_GROUP.deviantart.com/art/$ID`|`deviantart.com/$USER_GROUP/art/$ID`) => `/post/$USER_GROUP/$ID`
# Groups and users
## Main user page
* (`$USER_GROUP.deviantart.com`|`deviantart.com/$USER_GROUP`) => `/group_user?type=about&q=$USER_GROUP`
## Gallery
* (`$USER_GROUP.deviantart.com/gallery`|`deviantart.com/$USER_GROUP/gallery`) => `/group_user?type=gallery&q=$USER_GROUP`
## Favourites
* (`$USER_GROUP.deviantart.com/favourites`|`deviantart.com/$USER_GROUP/favourites`) => `/group_user?type=favourites&q=$USER_GROUP`

View File

@ -12,13 +12,13 @@
# Конфигурация
* `listen` — IP и порт для слушанья; заполняется по такой форме: ip:port
* `uri` — URI инстанса. Пример: `"uri":"/art/"` -> https://skunky.ebloid.ru/art/
* `cache` — Система кеширования; по умолчанию выключена.
* `cache` — Система кеширования; по умолчанию выключена
* `enabled` — Состояние системы кеширования; требуется булёвое значение
* `path` — Полный путь до каталога, куда будет сохраняться кеш
* `lifetime` — Время жизни файла в кеше, требует целочисленное значение, дополненное суффиксом времени (см. 'Единицы времени')
* `max-size` — Максимальный размер файла
* `update-interval` — Интервал для автоматической ротации кеша
* `dirs-to-memory` — Массив, заполнив который скопируются все файлы из указанных каталогов
* `static-path` — Строка, являющаяся путём до статики. SkunkyArt при запуске скопирует содержимое этого каталога в ОЗУ. Однако, если вы собрали фронтенд с тегом 'embed', то этого не произайдёт
* `download-proxy` — Адрес прокси для загрузки файлов
* `user-agent` — Строка, которая используется в качестве User-Agent'а

View File

@ -18,7 +18,7 @@ Time units:
* `lifetime` — Cached file life time, requires numeric value, followed by multiplicative suffix (see Time Units for details)
* `max-size` — Maximum file size in megabytes
* `update-interval` — Automatic rotation interval
* `dirs-to-memory` — This setting determines which directories will be copied to RAM when SkunkyArt is started. Mandatory
* `static-path` — This setting determines path to static, which will be copied to RAM when SkunkyArt is started. Useless if you're use binary compiled with 'embed' tag.
* `download-proxy` — Proxy address for downloading files.
* `user-agent` — String, which SkunkyArt uses as UA

View File

@ -9,69 +9,68 @@ import (
)
type API struct {
main *skunkyart
main *skunkyart
}
type info struct {
Version string `json:"version"`
Settings settingsParams `json:"settings"`
Version string `json:"version"`
Settings settingsParams `json:"settings"`
}
func (a API) Info() {
json, err := json.Marshal(info{
Version: a.main.Version,
Settings: settingsParams{
Nsfw: CFG.Nsfw,
Proxy: CFG.Proxy,
},
})
try(err)
a.main.Writer.Write(json)
return
json, err := json.Marshal(info{
Version: a.main.Version,
Settings: settingsParams{
Nsfw: CFG.Nsfw,
Proxy: CFG.Proxy,
},
})
try(err)
a.main.Writer.Write(json)
}
func (a API) Error(description string, status int) {
a.main.Writer.WriteHeader(status)
var response strings.Builder
response.WriteString(`{"error":"`)
response.WriteString(description)
response.WriteString(`"}`)
wr(a.main.Writer, response.String())
a.main.Writer.WriteHeader(status)
var response strings.Builder
response.WriteString(`{"error":"`)
response.WriteString(description)
response.WriteString(`"}`)
wr(a.main.Writer, response.String())
}
func (a API) sendMedia(d *devianter.Deviation) {
mediaUrl, name := devianter.UrlFromMedia(d.Media)
mediaUrl, name := devianter.UrlFromMedia(d.Media)
a.main.SetFilename(name)
if len(mediaUrl) != 0 {
mediaUrl = mediaUrl[21:]
if len(mediaUrl) != 0 {
mediaUrl = mediaUrl[21:]
dot := strings.Index(mediaUrl, ".")
a.main.Writer.Header().Del("Content-Type")
a.main.DownloadAndSendMedia(mediaUrl[:dot], mediaUrl[dot+11:])
}
a.main.DownloadAndSendMedia(mediaUrl[:dot], mediaUrl[dot+11:])
}
}
// TODO: сделать фильтры
func (a API) Random() {
for attempt := 1;; {
if attempt > 3 {
a.Error("Sorry, butt NSFW on this are disabled, and the instance failed to find a random art without NSFW", 500)
}
for attempt := 1; ; {
if attempt > 3 {
a.Error("Sorry, butt NSFW on this are disabled, and the instance failed to find a random art without NSFW", 500)
}
s, err, daErr := devianter.PerformSearch(string(rand.Intn(999)), rand.Intn(30), 'a')
try(err)
if daErr.RAW != nil {
continue
}
s, err, daErr := devianter.PerformSearch(string(rand.Intn(999)), rand.Intn(30), 'a')
try(err)
if daErr.RAW != nil {
continue
}
deviation := &s.Results[rand.Intn(len(s.Results))]
deviation := &s.Results[rand.Intn(len(s.Results))]
if deviation.NSFW && !CFG.Nsfw {
attempt++
continue
}
if deviation.NSFW && !CFG.Nsfw {
attempt++
continue
}
a.sendMedia(deviation)
return
}
a.sendMedia(deviation)
return
}
}

View File

@ -27,8 +27,8 @@ func (s skunkyart) DownloadAndSendMedia(subdomain, path string) {
url.WriteString(".wixmp.com/")
url.WriteString(path)
if t := s.Args.Get("token"); t != "" {
url.WriteString("?token=")
url.WriteString(t)
url.WriteString("?token=")
url.WriteString(t)
}
var response []byte

View File

@ -56,13 +56,11 @@ func ExecuteConfig() {
if CFG.cfg != "" {
f, err := os.ReadFile(CFG.cfg)
tryWithExitStatus(err, 1)
tryWithExitStatus(json.Unmarshal(f, &CFG), 1)
if CFG.Cache.Enabled && !CFG.Proxy {
exit("Incompatible settings detected: cannot use caching media content without proxy", 1)
}
static.StaticPath = CFG.StaticPath
if CFG.Cache.Enabled {
if CFG.Cache.Lifetime != "" {
var duration int64
@ -92,6 +90,13 @@ func ExecuteConfig() {
CFG.Cache.MaxSize *= 1024 ^ 2
go InitCacheSystem()
}
About = instanceAbout{
Proxy: CFG.Proxy,
Nsfw: CFG.Nsfw,
}
static.StaticPath = CFG.StaticPath
devianter.UserAgent = CFG.UserAgent
}
}

View File

@ -3,7 +3,7 @@ package app
import (
"io"
"net/http"
u "net/url"
url "net/url"
"skunkyart/static"
"strconv"
"strings"
@ -73,7 +73,7 @@ func Router() {
skunky.Writer = w
skunky.BasePath = CFG.URI
skunky.QueryRaw = arg("q")
skunky.Query = u.QueryEscape(skunky.QueryRaw)
skunky.Query = url.QueryEscape(skunky.QueryRaw)
skunky.Page = p
if t := arg("type"); len(t) > 0 {
@ -84,12 +84,21 @@ func Router() {
skunky.Atom = true
}
if CFG.Proxy {
w.Header().Add("Content-Security-Policy", "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'")
} else {
w.Header().Add("Content-Security-Policy", "default-src 'self'; img-src 'self' *.wixmp.com; script-src 'none'; style-src 'self' 'unsafe-inline'")
}
w.Header().Add("X-Frame-Options", "DENY")
switch skunky.Endpoint {
// main
case "":
skunky.ExecuteTemplate("index.htm", "html", &CFG.URI)
case "about":
skunky.About()
skunky.Templates.About = About
skunky.ExecuteTemplate("about.htm", "html", &skunky)
case "post":
skunky.Deviation(path[2], path[3])
case "search":
@ -120,12 +129,12 @@ func Router() {
case "api":
w.Header().Add("Content-Type", "application/json")
switch path[2] {
case "instance":
skunky.API.Info()
case "random":
skunky.API.Random()
default:
skunky.API.Error("Not Found", 404)
case "instance":
skunky.API.Info()
case "random":
skunky.API.Random()
default:
skunky.API.Error("Not Found", 404)
}
// 404

View File

@ -1,6 +1,7 @@
package app
import (
"encoding/json"
"io"
"net/http"
"net/url"
@ -40,18 +41,26 @@ func restore() {
}
var instances []byte
var About instanceAbout
func RefreshInstances() {
for {
func() {
defer restore()
instances = Download("https://git.macaw.me/skunky/SkunkyArt/raw/branch/master/instances.json").Body
try(json.Unmarshal(instances, &About))
}()
time.Sleep(1 * time.Hour)
}
}
// some crap for frontend
type instanceAbout struct {
Proxy bool
Nsfw bool
Instances []settings
}
type skunkyart struct {
Writer http.ResponseWriter
@ -63,15 +72,11 @@ type skunkyart struct {
BasePath, Endpoint string
Query, QueryRaw string
API API
Version string
API API
Version string
Templates struct {
About struct {
Proxy bool
Nsfw bool
Instances []settings
}
About instanceAbout
SomeList string
DDStrips string
@ -132,7 +137,7 @@ func UrlBuilder(strs ...string) string {
str.WriteString(CFG.URI)
for n, x := range strs {
str.WriteString(x)
if n := n+1; n < l && len(strs[n]) != 0 && !(strs[n][0] == '?' || strs[n][0] == '&') && !(x[0] == '?' || x[0] == '&') {
if n := n + 1; n < l && len(strs[n]) != 0 && !(strs[n][0] == '?' || strs[n][0] == '&') && !(x[0] == '?' || x[0] == '&') {
str.WriteString("/")
}
}
@ -215,6 +220,8 @@ func ParseMedia(media devianter.Media, thumb ...int) string {
filename = "image.gif"
}
return UrlBuilder("media", "file", mediaUrl[:dot], mediaUrl[dot+11:], "&filename=", filename)
} else if !CFG.Proxy {
return mediaUrl
}
return ""
}

View File

@ -1,7 +1,6 @@
package app
import (
"encoding/json"
"regexp"
"strconv"
"strings"
@ -24,8 +23,8 @@ func (s skunkyart) GRUser() {
s.Templates.GroupUser.GR, err, daError = g.Get()
try(err)
if daError.RAW != nil {
s.Error(daError)
return
s.Error(daError)
return
}
group := &s.Templates.GroupUser
@ -68,7 +67,7 @@ func (s skunkyart) GRUser() {
group.About.Interests += interest.String()
}
}
group.About.Comments = s.ParseComments(devianter.GetComments(strconv.Itoa(group.GR.Gruser.ID),"",s.Page,4))
group.About.Comments = s.ParseComments(devianter.GetComments(strconv.Itoa(group.GR.Gruser.ID), "", s.Page, 4))
case "cover_deviation":
group.About.BGMeta = x.ModuleData.CoverDeviation.Deviation
@ -225,8 +224,8 @@ func (s skunkyart) Deviation(author, postname string) {
func (s skunkyart) DD() {
dd, err := devianter.GetDailyDeviations(s.Page)
if err.RAW != nil {
s.Error(err)
return
s.Error(err)
return
}
var strips strings.Builder
for _, x := range dd.Strips {
@ -312,12 +311,13 @@ func (s skunkyart) Search() {
return
}
try(err)
if daError.RAW != nil {
s.Error(daError)
return
}
if s.Type != 'r' {
if daError.RAW != nil {
s.Error(daError)
return
}
ss.List = s.DeviationList(ss.Content.Results, false, DeviationList{
Pages: ss.Content.Pages,
More: ss.Content.HasMore,
@ -339,10 +339,3 @@ func (s skunkyart) Emojitar(name string) {
}
wr(s.Writer, ae)
}
func (s skunkyart) About() {
s.Templates.About.Nsfw = CFG.Nsfw
s.Templates.About.Proxy = CFG.Proxy
try(json.Unmarshal(instances, &s.Templates.About))
s.ExecuteTemplate("about.htm", "html", &s)
}

View File

@ -1,6 +1,6 @@
{
"listen": "0.0.0.0:3003",
"uri": "/huy/",
"uri": "/",
"cache": {
"enabled": true,
"path": "cache",
@ -12,5 +12,5 @@
"download-proxy": "http://127.0.0.1:8080",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36",
"proxy": true,
"nsfw": false
"nsfw": true
}

View File

@ -8,8 +8,8 @@
"clearnet": "https://skunky.ebloid.ru/art"
},
"settings": {
"nsfw": false,
"proxy": false
"proxy": true,
"nsfw": false
}
},
{
@ -19,8 +19,8 @@
"clearnet": "https://skunky.clovius.club"
},
"settings": {
"nsfw": true,
"proxy": true
"proxy": true,
"nsfw": true
}
},
{
@ -30,8 +30,8 @@
"clearnet": "https://skunky.bloat.cat"
},
"settings": {
"nsfw": true,
"proxy": true
"proxy": true,
"nsfw": true
}
},
{
@ -41,8 +41,8 @@
"clearnet": "https://skunkyart.lumaeris.com"
},
"settings": {
"nsfw": true,
"proxy": true
"proxy": true,
"nsfw": true
}
},
{
@ -52,8 +52,8 @@
"clearnet": "https://art.bloat.cat"
},
"settings": {
"nsfw": true,
"proxy": true
"proxy": true,
"nsfw": true
}
}
]

View File

@ -9,7 +9,7 @@ import (
)
func main() {
app.Release.Version = "1.3.2-alpha"
app.Release.Version = "1.3.2"
app.Release.Description = "Two API endpoints and template embedding into binary"
go app.RefreshInstances()

View File

@ -19,11 +19,10 @@ header h1 {
header form {
align-self: center;
}
header {
header, form {
display: flex;
}
form {
font-size: 0;
border: solid #164e3e 1px;
max-width: fit-content;
}
@ -147,22 +146,19 @@ input:focus {
font-size: 80%
}
center form {
font-size: 60%
}
header form {
font-size: 60%;
max-width: unset;
border: 0;
}
header, center {
header {
margin-left: 3%;
text-align: center;
display: block;
display: inline-block;
clear: both;
font-size: 200%;
}
form {
font-size: 60%;
border: solid #164e3e 5px;
}
.content {
margin: auto;
display: inherit;

View File

@ -6,7 +6,7 @@
<p>
SkunkyArt is an alternative frontend for deviantart.com, written in Go.
</p>
<h3><a href="https://go.kde.org/matrix/#/#skunkyart:ebloid.ru" target="_blank">Room in Matrix</a></h3>
<h3><a href="https://go.kde.org/matrix/#/#skunkyart:ebloid.ru" target="_blank">Room in [matrix]</a></h3>
<b>Instance settings:</b>
<ul>
<li><b>NSFW</b>: <span class="about-{{.Templates.About.Nsfw}}">{{if .Templates.About.Nsfw}}YES{{else}}NO{{end}}</span></li>

View File

@ -21,5 +21,6 @@
<meta name="referrer" content="no-referrer" />
<link rel="stylesheet" href="{{.BasePath}}stylesheet">
<link rel="icon" type="image/x-icon" href="{{.BasePath}}favicon.ico">
<meta name="viewport" content="width=device-width, height=device-height, initial-scale=0.4, user-scalable=no; user-scalable=0"/>
</head>
{{end}}

View File

@ -4,9 +4,75 @@
<title>SkunkyArt</title>
<link rel="stylesheet" href="{{.}}stylesheet"/>
<link rel="icon" type="image/x-icon" href="{{.}}favicon.ico">
<meta name="viewport" content="width=device-width, height=device-height, initial-scale=0.4, user-scalable=no; user-scalable=0"/>
<style>
main {
display: flex;
max-width: fit-content;
position: absolute;
top: 50%;
right: 50%;
transform: translate(50%, -50%);
}
div {
transform: translate(0, 50%);
margin-left: 4%;
flex-basis: 100%;
height: 30%;
display: block;
max-width: fit-content;
}
div h1, form {
margin: 0;
}
div form {
font-size: 100%;
max-width: 100%
}
div form input {
width: 100%
}
img {
width: 30%;
height: 30%;
}
@media (orientation: portrait) {
main {
display: block;
width: 200%;
}
img {
width: 100%;
height: 100%;
}
form {
width: 200%;
}
div {
margin: -25%;
margin-top: auto;
width: 200%;
}
div h1 {
text-align: center;
}
}
@media (max-width: 1155px) and (orientation: landscape) {
img {
width: 50%;
}
div {
transform: none;
}
}
</style>
</head>
<main>
<center>
<img src="{{.}}favicon.ico" title="SkunkyArt logo" draggable="false">
<div>
<h1><a href="{{.}}dd">Daily Deviations</a> | <a href="{{.}}about">About</a></h1>
<form method="get" action="{{.}}search">
<input type="text" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false">
<select name="type">
@ -16,7 +82,9 @@
</select>
<button type="submit">Search!</button>
</form>
<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>
</center>
<h1 style="margin-top: 5%; font-size: 200%; text-align: center;">
<a href="https://git.macaw.me/skunky/SkunkyArt" target="_blank" title="Source Code">SkunkyArt</a>
</h1>
</div>
</main>
</html>