Compare commits
No commits in common. "master" and "v1.3.1" have entirely different histories.
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,4 +1,3 @@
|
|||||||
**/cache
|
**/cache
|
||||||
**/config.json
|
**/config.json
|
||||||
**/skunkyart
|
**/skunkyart
|
||||||
**/skunkyart-*
|
|
||||||
|
10
INSTANCES.md
10
INSTANCES.md
@ -1,9 +1,7 @@
|
|||||||
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|
|
|Instance|Yggdrasil|I2P|Tor|NSFW|Proxifying|Modified Sources|Country|
|
||||||
|:------:|:-------:|:-:|:-:|:--:|:--------:|:--------------:|:-----:|
|
|:------:|:-------:|:-:|:-:|:--:|:--------:|:--------------:|:-----:|
|
||||||
|[ls.404.mn](https://ls.404.mn/skunkyart)|[Yes](http://[201:f137:d1ac:920e:cd42:bfd1:1e83:da1d]/skunkyart)|No|No| No | Yes | No | Germany |
|
|[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 |
|
||||||
|[clovius.club](https://skunky.clovius.club)|No|No|No| Yes | Yes | No | Sweden |
|
|[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 |
|
|[bloat.cat](https://skunky.bloat.cat)|No|No|No| Yes | Yes | No | Romania |
|
||||||
|[lumaeris.com](https://skunkyart.lumaeris.com)|No|No|No| Yes | Yes | No | Germany |
|
|[frontendfriendly.xyz](https://skunkyart.frontendfriendly.xyz)|No|No|No| Yes | Yes | No | Finland |
|
||||||
|[art.bloat.cat](https://art.bloat.cat)|No|No|No| Yes | Yes | No | Germany |
|
|[lumaeris.com](https://skunkyart.lumaeris.com)|No|No|No| Yes | Yes | No | US |
|
26
README.md
26
README.md
@ -1,25 +1,15 @@
|
|||||||
> [!NOTE]
|
<img src="https://git.macaw.me/skunky/SkunkyArt/raw/branch/master/misc/logo.png" alt="SkunkyArt" title="SkunkyArt Logo" width="20%" loading="lazy"/>
|
||||||
> Currently, due to school, I cannot actively develop this project :(
|
|
||||||
> However, this does not mean that development has stopped. Just wait for the summer. For questions, write either to the Matrix room or to me in DM.
|
|
||||||
|
|
||||||
<img src="static/images/logo.png" alt="SkunkyArt" title="SkunkyArt Logo" width="20%" loading="lazy"/>
|
[![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)
|
||||||
|
|
||||||
[![Matrix room](https://img.shields.io/badge/matrix-000000?style=for-the-badge&logo=Matrix&logoColor=white)](https://go.kde.org/matrix/#/#skunkyart:ls.404.mn)
|
Instances: [`INSTANCES.md`](https://git.macaw.me/skunky/SkunkyArt/src/branch/master/INSTANCES.md)
|
||||||
|
|
||||||
Instances: [`INSTANCES.md`](/skunky/SkunkyArt/src/branch/master/INSTANCES.md)
|
|
||||||
|
|
||||||
# EN 🇺🇸
|
# EN 🇺🇸
|
||||||
## Description
|
## Description
|
||||||
SkunkyArt 🦨 — alternative frontend for DevianArt, which works without JS.
|
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
|
## Setup
|
||||||
The sample config is in the `config.example.json` file. For custom config, use `--config` option.
|
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.
|
See the [`SETUP.md`](https://git.macaw.me/skunky/SkunkyArt/src/branch/master/SETUP.md) file for more info about directives.
|
||||||
## Adding instance to the list
|
## Adding instance to the list
|
||||||
To do this, you must either make a PR by adding your instance to the `instances.json` and `INSTANCES.md` files (you can use `--add-instance` cli-argument to automatically add the instance to these files), or create an Issue, or report it to the room in Matrix. Keep in mind that your instance must comply with the following rules:
|
To do this, you must either make a PR by adding your instance to the `instances.json` and `INSTANCES.md` files (you can use `--add-instance` cli-argument to automatically add the instance to these files), or create an Issue, or report it to the room in Matrix. Keep in mind that your instance must comply with the following rules:
|
||||||
1. the Instance must not use Cloudflare.
|
1. the Instance must not use Cloudflare.
|
||||||
@ -31,15 +21,9 @@ To do this, you must either make a PR by adding your instance to the `instances.
|
|||||||
# RU 🇷🇺
|
# RU 🇷🇺
|
||||||
## Описание
|
## Описание
|
||||||
SkunkyArt 🦨 — альтернативный фронтенд к DeviantArt, который полностью работает без JS (JavaScript).
|
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`.
|
Пример конфига находится в файле `config.example.json`. Чтобы указать свой конфиг, используйте cli-аргумент `--config`.
|
||||||
См. [`SETUP-RU.md`](/skunky/SkunkyArt/src/branch/master/SETUP-RU.md) для информации о настройки фронтенда.
|
См. [`SETUP-RU.md`](https://git.macaw.me/skunky/SkunkyArt/src/branch/master/SETUP-RU.md) для информации о настройки фронтенда.
|
||||||
## Добавление инстанса в список
|
## Добавление инстанса в список
|
||||||
Чтобы это сделать, вы должны либо сделать PR, добавив в файлы `instances.json` и `INSTANCES.md` свой инстанс (можете воспользоваться cli-аргументом `--add-instance`, который автоматически это сделает), либо создать Issue, или сообщить о нём в комнате в Matrix. Учтите, что ваш инстанс должен соблюсти следущие правила:
|
Чтобы это сделать, вы должны либо сделать PR, добавив в файлы `instances.json` и `INSTANCES.md` свой инстанс (можете воспользоваться cli-аргументом `--add-instance`, который автоматически это сделает), либо создать Issue, или сообщить о нём в комнате в Matrix. Учтите, что ваш инстанс должен соблюсти следущие правила:
|
||||||
1. Инстанс не должен использовать Cloudflare итп.
|
1. Инстанс не должен использовать Cloudflare итп.
|
||||||
|
13
REDIRECTS.md
13
REDIRECTS.md
@ -1,13 +0,0 @@
|
|||||||
# 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`
|
|
@ -1,4 +1,4 @@
|
|||||||
[English version 🇬🇧](/skunky/SkunkyArt/src/branch/master/SETUP.md)
|
[English version 🇬🇧](https://git.macaw.me/skunky/SkunkyArt/src/branch/master/SETUP.md)
|
||||||
|
|
||||||
# Единицы измерения
|
# Единицы измерения
|
||||||
Размер файла в кеше измеряется в мегабайтах.<br>
|
Размер файла в кеше измеряется в мегабайтах.<br>
|
||||||
@ -12,13 +12,13 @@
|
|||||||
# Конфигурация
|
# Конфигурация
|
||||||
* `listen` — IP и порт для слушанья; заполняется по такой форме: ip:port
|
* `listen` — IP и порт для слушанья; заполняется по такой форме: ip:port
|
||||||
* `uri` — URI инстанса. Пример: `"uri":"/art/"` -> https://skunky.ebloid.ru/art/
|
* `uri` — URI инстанса. Пример: `"uri":"/art/"` -> https://skunky.ebloid.ru/art/
|
||||||
* `cache` — Система кеширования; по умолчанию выключена
|
* `cache` — Система кеширования; по умолчанию выключена.
|
||||||
* `enabled` — Состояние системы кеширования; требуется булёвое значение
|
* `enabled` — Состояние системы кеширования; требуется булёвое значение
|
||||||
* `path` — Полный путь до каталога, куда будет сохраняться кеш
|
* `path` — Полный путь до каталога, куда будет сохраняться кеш
|
||||||
* `lifetime` — Время жизни файла в кеше, требует целочисленное значение, дополненное суффиксом времени (см. 'Единицы времени')
|
* `lifetime` — Время жизни файла в кеше, требует целочисленное значение, дополненное суффиксом времени (см. 'Единицы времени')
|
||||||
* `max-size` — Максимальный размер файла
|
* `max-size` — Максимальный размер файла
|
||||||
* `update-interval` — Интервал для автоматической ротации кеша
|
* `update-interval` — Интервал для автоматической ротации кеша
|
||||||
* `static-path` — Строка, являющаяся путём до статики. SkunkyArt при запуске скопирует содержимое этого каталога в ОЗУ. Однако, если вы собрали фронтенд с тегом 'embed', то этого не произайдёт
|
* `dirs-to-memory` — Массив, заполнив который скопируются все файлы из указанных каталогов
|
||||||
* `download-proxy` — Адрес прокси для загрузки файлов
|
* `download-proxy` — Адрес прокси для загрузки файлов
|
||||||
* `user-agent` — Строка, которая используется в качестве User-Agent'а
|
* `user-agent` — Строка, которая используется в качестве User-Agent'а
|
||||||
|
|
||||||
|
4
SETUP.md
4
SETUP.md
@ -1,4 +1,4 @@
|
|||||||
[Версия на русском языке 🇷🇺](/skunky/SkunkyArt/src/branch/master/SETUP-RU.md)
|
[Версия на русском языке 🇷🇺](https://git.macaw.me/skunky/SkunkyArt/src/branch/master/SETUP-RU.md)
|
||||||
|
|
||||||
# Units
|
# Units
|
||||||
Maximum file size in megabytes, requires numeric value.<br>
|
Maximum file size in megabytes, requires numeric value.<br>
|
||||||
@ -18,7 +18,7 @@ Time units:
|
|||||||
* `lifetime` — Cached file life time, requires numeric value, followed by multiplicative suffix (see Time Units for details)
|
* `lifetime` — Cached file life time, requires numeric value, followed by multiplicative suffix (see Time Units for details)
|
||||||
* `max-size` — Maximum file size in megabytes
|
* `max-size` — Maximum file size in megabytes
|
||||||
* `update-interval` — Automatic rotation interval
|
* `update-interval` — Automatic rotation interval
|
||||||
* `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.
|
* `dirs-to-memory` — This setting determines which directories will be copied to RAM when SkunkyArt is started. Mandatory
|
||||||
* `download-proxy` — Proxy address for downloading files.
|
* `download-proxy` — Proxy address for downloading files.
|
||||||
* `user-agent` — String, which SkunkyArt uses as UA
|
* `user-agent` — String, which SkunkyArt uses as UA
|
||||||
|
|
||||||
|
24
TODO.md
24
TODO.md
@ -1,23 +1,19 @@
|
|||||||
# v1.3.x
|
# v1.3.x
|
||||||
* Почистить говнокод
|
* Почистить говнокод
|
||||||
* Добавить фильтры поиска
|
|
||||||
* ~~Сделать порт под FreeBSD~~ ✔️
|
|
||||||
* **Доделать парсинг описания**
|
* **Доделать парсинг описания**
|
||||||
* ~~Реализовать стрипы в ежедневных артах~~ ✔️
|
* Избавиться от хардкода под Linux
|
||||||
* ~~Исправить баг с навигацией по страницам~~ ✔️
|
* ~~Реализовать стрипы в ежедневных артах~~
|
||||||
* ~~Сделать нормальное отображение ошибок~~ ✔️
|
* Сделать нормальное отображение ошибок
|
||||||
* ~~Сделать единицы в конфиге более понятными~~ ✔️
|
* ~~Исправить баг с навигацией по страницам~~
|
||||||
* Добавить чекер инстанса на работоспособность
|
* ~~Сделать единицы в конфиге более понятными~~
|
||||||
* ~~Добавить просмотр понравившихся артов пользователю~~ ✔️
|
* Добавить возможность включить темплейты в бинарник
|
||||||
* Добавить возможность включить темплейты в бинарник [P]
|
* ~~Реализовать миниатюры и оптимизировать CSS под маленькие экраны~~
|
||||||
* ~~Реализовать миниатюры и оптимизировать CSS под маленькие экраны~~ ✔️
|
|
||||||
* Написать Makefile и скрипт для автоматического развёртывания инстанса
|
* Написать Makefile и скрипт для автоматического развёртывания инстанса
|
||||||
|
* **Реализовать отображение контента, отличного от картинок (видео, аудио, etc)**
|
||||||
* Исправить баг с эмоджи, когда некоторые кастомные эмоции могут не отображаться
|
* Исправить баг с эмоджи, когда некоторые кастомные эмоции могут не отображаться
|
||||||
* ~~Добавить аргумент &filename, который будет выдавать файл с нормально выглядещем именем~~ ✔️
|
* Добавить флаг сборки, который позволит собрать бинарник со встроенными темплейтами
|
||||||
* ~~Улучшить систему кеширования: добавить рейтинг для удаления и копирование изображений в ОЗУ~~ ✔️
|
* Улучшить систему кеширования: добавить рейтинг для удаления и копирование изображений в ОЗУ
|
||||||
# v1.4
|
# v1.4
|
||||||
* Реализовать API
|
* Реализовать API
|
||||||
* Реализовать темы
|
* Реализовать темы
|
||||||
* Перейти на арены в кеше
|
|
||||||
* Реализовать многоязычный интерфейс
|
* Реализовать многоязычный интерфейс
|
||||||
|
|
||||||
|
76
app/api.go
76
app/api.go
@ -1,76 +0,0 @@
|
|||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"math/rand"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.macaw.me/skunky/devianter"
|
|
||||||
)
|
|
||||||
|
|
||||||
type API struct {
|
|
||||||
main *skunkyart
|
|
||||||
}
|
|
||||||
|
|
||||||
type info struct {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a API) sendMedia(d *devianter.Deviation) {
|
|
||||||
mediaUrl, name := devianter.UrlFromMedia(d.Media)
|
|
||||||
a.main.SetFilename(name)
|
|
||||||
|
|
||||||
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:])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
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))]
|
|
||||||
|
|
||||||
if deviation.NSFW && !CFG.Nsfw {
|
|
||||||
attempt++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
a.sendMedia(deviation)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
138
app/cache.go
138
app/cache.go
@ -1,138 +0,0 @@
|
|||||||
// TODO: реализовать кеширование JSON и почистить код
|
|
||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/sha1"
|
|
||||||
"encoding/hex"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type file struct {
|
|
||||||
Score int
|
|
||||||
Content []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
var tempFS = make(map[[20]byte]*file)
|
|
||||||
var mx = &sync.RWMutex{}
|
|
||||||
|
|
||||||
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)
|
|
||||||
if t := s.Args.Get("token"); t != "" {
|
|
||||||
url.WriteString("?token=")
|
|
||||||
url.WriteString(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
var response []byte
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case CFG.Cache.Enabled:
|
|
||||||
fileName := sha1.Sum([]byte(subdomain + path))
|
|
||||||
filePath := CFG.Cache.Path + "/" + hex.EncodeToString(fileName[:])
|
|
||||||
|
|
||||||
mx.Lock()
|
|
||||||
if tempFS[fileName] == nil {
|
|
||||||
tempFS[fileName] = &file{}
|
|
||||||
}
|
|
||||||
mx.Unlock()
|
|
||||||
|
|
||||||
if tempFS[fileName].Content != nil {
|
|
||||||
response = tempFS[fileName].Content
|
|
||||||
tempFS[fileName].Score += 2
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
file, err := os.Open(filePath)
|
|
||||||
if err != nil {
|
|
||||||
if dwnld := Download(url.String()); dwnld.Status == 200 && dwnld.Headers["Content-Type"][0][:5] == "image" {
|
|
||||||
response = dwnld.Body
|
|
||||||
try(os.WriteFile(filePath, response, 0700))
|
|
||||||
} else {
|
|
||||||
s.ReturnHTTPError(dwnld.Status)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
file, e := io.ReadAll(file)
|
|
||||||
try(e)
|
|
||||||
response = file
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer restore()
|
|
||||||
|
|
||||||
mx.RLock()
|
|
||||||
tempFS[fileName].Content = response
|
|
||||||
mx.RUnlock()
|
|
||||||
|
|
||||||
for {
|
|
||||||
time.Sleep(1 * time.Minute)
|
|
||||||
|
|
||||||
mx.Lock()
|
|
||||||
if tempFS[fileName].Score <= 0 {
|
|
||||||
delete(tempFS, fileName)
|
|
||||||
mx.Unlock()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tempFS[fileName].Score--
|
|
||||||
mx.Unlock()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
case CFG.Proxy:
|
|
||||||
dwnld := Download(url.String())
|
|
||||||
if dwnld.Status != 200 {
|
|
||||||
s.ReturnHTTPError(dwnld.Status)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
response = dwnld.Body
|
|
||||||
default:
|
|
||||||
s.Writer.WriteHeader(403)
|
|
||||||
response = []byte("Sorry, butt proxy on this instance are disabled.")
|
|
||||||
}
|
|
||||||
|
|
||||||
s.Writer.Write(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
func InitCacheSystem() {
|
|
||||||
c := &CFG.Cache
|
|
||||||
for {
|
|
||||||
dir, err := os.ReadDir(c.Path)
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
os.Mkdir(c.Path, 0700)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
println(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, file := range dir {
|
|
||||||
fileName := c.Path + "/" + file.Name()
|
|
||||||
fileInfo, err := file.Info()
|
|
||||||
try(err)
|
|
||||||
|
|
||||||
if c.Lifetime != "" {
|
|
||||||
now := time.Now().UnixMilli()
|
|
||||||
|
|
||||||
stat := fileInfo.Sys().(*syscall.Stat_t)
|
|
||||||
time := statTime(stat)
|
|
||||||
|
|
||||||
if time+lifetimeParsed <= now {
|
|
||||||
try(os.RemoveAll(fileName))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.MaxSize != 0 && fileInfo.Size() > c.MaxSize {
|
|
||||||
try(os.RemoveAll(fileName))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(time.Second * time.Duration(c.UpdateInterval))
|
|
||||||
}
|
|
||||||
}
|
|
74
app/cli.go
74
app/cli.go
@ -4,20 +4,19 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"html/template"
|
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ExecuteCommandLineArguments() {
|
func ExecuteCommandLineArguments() {
|
||||||
var helpmsg = `SkunkyArt v{{.Version}} [{{.Description}}]
|
const helpmsg = `SkunkyArt v1.3.1 [CSS improvements for mobile and the strips on Daily Deviations]
|
||||||
Usage:
|
Usage:
|
||||||
- [-c|--config] | path to config
|
- [-c|--config] | path to config
|
||||||
- [-a|--add-instance] | generates 'instances.json' and 'INSTANCES.md' files with ur instance
|
- [-a|--add-instance] | generates 'instances.json' and 'INSTANCES.md' files with ur instance
|
||||||
- [-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/v{{.Version}}`
|
Copyright lost+skunk, X11. https://git.macaw.me/skunky/skunkyart/src/tag/v1.3.1`
|
||||||
|
|
||||||
a := os.Args[1:]
|
a := os.Args[1:]
|
||||||
for n, x := range a {
|
for n, x := range a {
|
||||||
@ -29,11 +28,7 @@ Copyright lost+skunk, X11. https://git.macaw.me/skunky/skunkyart/src/tag/v{{.Ver
|
|||||||
exit("Not enought arguments", 1)
|
exit("Not enought arguments", 1)
|
||||||
}
|
}
|
||||||
case "-h", "--help":
|
case "-h", "--help":
|
||||||
var buf bytes.Buffer
|
exit(helpmsg, 0)
|
||||||
t := template.New("help")
|
|
||||||
t.Parse(helpmsg)
|
|
||||||
t.Execute(&buf, &Release)
|
|
||||||
exit(buf.String(), 0)
|
|
||||||
case "-a", "--add-instance":
|
case "-a", "--add-instance":
|
||||||
addInstance()
|
addInstance()
|
||||||
}
|
}
|
||||||
@ -83,16 +78,16 @@ func addInstance() {
|
|||||||
try(err)
|
try(err)
|
||||||
defer instancesJson.Close()
|
defer instancesJson.Close()
|
||||||
|
|
||||||
instancesFile, err := os.OpenFile("INSTANCES.md", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
instances, err := os.OpenFile("INSTANCES.md", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||||
try(err)
|
try(err)
|
||||||
defer instancesFile.Close()
|
defer instances.Close()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
if string(instances) == "" {
|
if Templates["instances.json"] == "" {
|
||||||
print("\rDownloading instance list...")
|
print("\rDownloading instance list...")
|
||||||
} else {
|
} else {
|
||||||
println("\r\033[2KDownloaded!")
|
println("\r\033[2KDownloaded!")
|
||||||
try(json.Unmarshal(instances, &settingsVar))
|
try(json.Unmarshal([]byte(Templates["instances.json"]), &settingsVar))
|
||||||
|
|
||||||
settingsVar.Instances = append(settingsVar.Instances, settings{
|
settingsVar.Instances = append(settingsVar.Instances, settings{
|
||||||
Title: prompt("Title", true),
|
Title: prompt("Title", true),
|
||||||
@ -118,46 +113,51 @@ func addInstance() {
|
|||||||
settingsVar := &settingsVar.Instances[len(settingsVar.Instances)-1]
|
settingsVar := &settingsVar.Instances[len(settingsVar.Instances)-1]
|
||||||
var mdstr bytes.Buffer
|
var mdstr bytes.Buffer
|
||||||
|
|
||||||
mdbuilder := func(yes bool, link string, title string) {
|
|
||||||
switch {
|
|
||||||
case yes && (title != "" && link != ""):
|
|
||||||
mdstr.WriteString("[")
|
|
||||||
mdstr.WriteString(title)
|
|
||||||
mdstr.WriteString("](")
|
|
||||||
mdstr.WriteString(link)
|
|
||||||
mdstr.WriteString(")")
|
|
||||||
case yes && link != "":
|
|
||||||
mdstr.WriteString("[Yes](")
|
|
||||||
mdstr.WriteString(link)
|
|
||||||
mdstr.WriteString(")")
|
|
||||||
case yes:
|
|
||||||
mdstr.WriteString("Yes")
|
|
||||||
default:
|
|
||||||
mdstr.WriteString("No")
|
|
||||||
}
|
|
||||||
mdstr.WriteString("|")
|
|
||||||
}
|
|
||||||
|
|
||||||
mdstr.WriteString("\n|")
|
mdstr.WriteString("\n|")
|
||||||
mdbuilder(settingsVar.Urls.Clearnet != "", settingsVar.Urls.Clearnet, settingsVar.Title)
|
if settingsVar.Urls.Clearnet != "" {
|
||||||
|
mdstr.WriteString("[")
|
||||||
|
mdstr.WriteString(settingsVar.Title)
|
||||||
|
mdstr.WriteString("](")
|
||||||
|
mdstr.WriteString(settingsVar.Urls.Clearnet)
|
||||||
|
mdstr.WriteString(")")
|
||||||
|
} else {
|
||||||
|
mdstr.WriteString(settingsVar.Title)
|
||||||
|
}
|
||||||
|
mdstr.WriteString("|")
|
||||||
|
|
||||||
urls := []string{settingsVar.Urls.Ygg, settingsVar.Urls.I2P, settingsVar.Urls.Tor}
|
urls := []string{settingsVar.Urls.Ygg, settingsVar.Urls.I2P, settingsVar.Urls.Tor}
|
||||||
for i, l := 0, len(urls); i < l; i++ {
|
for i, l := 0, len(urls); i < l; i++ {
|
||||||
url := urls[i]
|
url := urls[i]
|
||||||
mdbuilder(url != "", url, "")
|
if url != "" {
|
||||||
|
mdstr.WriteString("[Yes](")
|
||||||
|
mdstr.WriteString(url)
|
||||||
|
mdstr.WriteString(")|")
|
||||||
|
} else {
|
||||||
|
mdstr.WriteString("No|")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
settings := []bool{settingsVar.Settings.Nsfw, settingsVar.Settings.Proxy}
|
settings := []bool{settingsVar.Settings.Nsfw, settingsVar.Settings.Proxy}
|
||||||
for i, l := 0, len(settings); i < l; i++ {
|
for i, l := 0, len(settings); i < l; i++ {
|
||||||
mdbuilder(settings[i], "", "")
|
if settings[i] {
|
||||||
|
mdstr.WriteString("Yes|")
|
||||||
|
} else {
|
||||||
|
mdstr.WriteString("No|")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mdbuilder(settingsVar.ModifiedSrc != "", settingsVar.ModifiedSrc, "")
|
if settingsVar.ModifiedSrc != "" {
|
||||||
|
mdstr.WriteString("[Yes](")
|
||||||
|
mdstr.WriteString(settingsVar.ModifiedSrc)
|
||||||
|
mdstr.WriteString(")|")
|
||||||
|
} else {
|
||||||
|
mdstr.WriteString("No|")
|
||||||
|
}
|
||||||
|
|
||||||
mdstr.WriteString(settingsVar.Country)
|
mdstr.WriteString(settingsVar.Country)
|
||||||
mdstr.WriteString("|")
|
mdstr.WriteString("|")
|
||||||
|
|
||||||
instancesFile.Write(mdstr.Bytes())
|
instances.Write(mdstr.Bytes())
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
time.Sleep(500 * time.Millisecond)
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
@ -4,18 +4,12 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
"skunkyart/static"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.macaw.me/skunky/devianter"
|
"git.macaw.me/skunky/devianter"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Release struct {
|
|
||||||
Version string
|
|
||||||
Description string
|
|
||||||
}
|
|
||||||
|
|
||||||
type cache_config struct {
|
type cache_config struct {
|
||||||
Enabled bool
|
Enabled bool
|
||||||
Path string
|
Path string
|
||||||
@ -30,9 +24,9 @@ type config struct {
|
|||||||
URI string `json:"uri"`
|
URI string `json:"uri"`
|
||||||
Cache cache_config
|
Cache cache_config
|
||||||
Proxy, Nsfw bool
|
Proxy, Nsfw bool
|
||||||
UserAgent string `json:"user-agent"`
|
UserAgent string `json:"user-agent"`
|
||||||
DownloadProxy string `json:"download-proxy"`
|
DownloadProxy string `json:"download-proxy"`
|
||||||
StaticPath string `json:"static-path"`
|
Dirs []string `json:"dirs-to-memory"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var CFG = config{
|
var CFG = config{
|
||||||
@ -44,10 +38,10 @@ var CFG = config{
|
|||||||
Path: "cache",
|
Path: "cache",
|
||||||
UpdateInterval: 1,
|
UpdateInterval: 1,
|
||||||
},
|
},
|
||||||
StaticPath: "static",
|
Dirs: []string{"html", "css", "misc"},
|
||||||
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36",
|
UserAgent: "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,
|
Proxy: true,
|
||||||
Nsfw: true,
|
Nsfw: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
var lifetimeParsed int64
|
var lifetimeParsed int64
|
||||||
@ -56,6 +50,7 @@ func ExecuteConfig() {
|
|||||||
if CFG.cfg != "" {
|
if CFG.cfg != "" {
|
||||||
f, err := os.ReadFile(CFG.cfg)
|
f, err := os.ReadFile(CFG.cfg)
|
||||||
tryWithExitStatus(err, 1)
|
tryWithExitStatus(err, 1)
|
||||||
|
|
||||||
tryWithExitStatus(json.Unmarshal(f, &CFG), 1)
|
tryWithExitStatus(json.Unmarshal(f, &CFG), 1)
|
||||||
if CFG.Cache.Enabled && !CFG.Proxy {
|
if CFG.Cache.Enabled && !CFG.Proxy {
|
||||||
exit("Incompatible settings detected: cannot use caching media content without proxy", 1)
|
exit("Incompatible settings detected: cannot use caching media content without proxy", 1)
|
||||||
@ -87,16 +82,9 @@ func ExecuteConfig() {
|
|||||||
|
|
||||||
lifetimeParsed = duration * int64(num)
|
lifetimeParsed = duration * int64(num)
|
||||||
}
|
}
|
||||||
CFG.Cache.MaxSize *= 1024 ^ 2
|
CFG.Cache.MaxSize /= 1024 ^ 2
|
||||||
go InitCacheSystem()
|
go InitCacheSystem()
|
||||||
}
|
}
|
||||||
|
|
||||||
About = instanceAbout{
|
|
||||||
Proxy: CFG.Proxy,
|
|
||||||
Nsfw: CFG.Nsfw,
|
|
||||||
}
|
|
||||||
|
|
||||||
static.StaticPath = CFG.StaticPath
|
|
||||||
devianter.UserAgent = CFG.UserAgent
|
devianter.UserAgent = CFG.UserAgent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,11 +9,7 @@ import (
|
|||||||
"golang.org/x/net/html"
|
"golang.org/x/net/html"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s skunkyart) ParseComments(c devianter.Comments, daError devianter.Error) string {
|
func (s skunkyart) ParseComments(c devianter.Comments) string {
|
||||||
if daError.RAW != nil {
|
|
||||||
return "Failed to fetch comments :("
|
|
||||||
}
|
|
||||||
|
|
||||||
var cmmts strings.Builder
|
var cmmts strings.Builder
|
||||||
replied := make(map[int]string)
|
replied := make(map[int]string)
|
||||||
|
|
||||||
@ -45,9 +41,7 @@ func (s skunkyart) ParseComments(c devianter.Comments, daError devianter.Error)
|
|||||||
cmmts.WriteString("</b></a> ")
|
cmmts.WriteString("</b></a> ")
|
||||||
|
|
||||||
if x.Parent > 0 {
|
if x.Parent > 0 {
|
||||||
cmmts.WriteString(` In reply to <a href="`)
|
cmmts.WriteString(` In reply to <a href="#`)
|
||||||
cmmts.WriteString(s._pth)
|
|
||||||
cmmts.WriteString("#")
|
|
||||||
cmmts.WriteString(strconv.Itoa(x.Parent))
|
cmmts.WriteString(strconv.Itoa(x.Parent))
|
||||||
cmmts.WriteString(`">`)
|
cmmts.WriteString(`">`)
|
||||||
if replied[x.Parent] == "" {
|
if replied[x.Parent] == "" {
|
||||||
@ -88,7 +82,6 @@ func (s skunkyart) DeviationList(devs []devianter.Deviation, allowAtom bool, con
|
|||||||
data := &devs[i]
|
data := &devs[i]
|
||||||
if preview, fullview := ParseMedia(data.Media, 320), ParseMedia(data.Media); !(data.NSFW && !CFG.Nsfw) {
|
if preview, fullview := ParseMedia(data.Media, 320), ParseMedia(data.Media); !(data.NSFW && !CFG.Nsfw) {
|
||||||
if allowAtom && s.Atom {
|
if allowAtom && s.Atom {
|
||||||
s.Writer.Header().Add("Content-type", "application/atom+xml")
|
|
||||||
id := strconv.Itoa(data.ID)
|
id := strconv.Itoa(data.ID)
|
||||||
listContent.WriteString(`<entry><author><name>`)
|
listContent.WriteString(`<entry><author><name>`)
|
||||||
listContent.WriteString(data.Author.Username)
|
listContent.WriteString(data.Author.Username)
|
||||||
@ -188,7 +181,6 @@ type text struct {
|
|||||||
To int
|
To int
|
||||||
}
|
}
|
||||||
|
|
||||||
// переписать весь этот пиздец нахуй
|
|
||||||
func ParseDescription(dscr devianter.Text) string {
|
func ParseDescription(dscr devianter.Text) string {
|
||||||
var parsedDescription strings.Builder
|
var parsedDescription strings.Builder
|
||||||
TagBuilder := func(content string, tags ...string) string {
|
TagBuilder := func(content string, tags ...string) string {
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
url "net/url"
|
u "net/url"
|
||||||
"skunkyart/static"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@ -43,61 +41,41 @@ func Router() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
open := func(name string) []byte {
|
|
||||||
file, err := static.Templates.Open(name)
|
|
||||||
try(err)
|
|
||||||
fileReaded, err := io.ReadAll(file)
|
|
||||||
try(err)
|
|
||||||
|
|
||||||
return fileReaded
|
|
||||||
}
|
|
||||||
|
|
||||||
// функция, что управляет всем
|
// функция, что управляет всем
|
||||||
handle := func(w http.ResponseWriter, r *http.Request) {
|
handle := func(w http.ResponseWriter, r *http.Request) {
|
||||||
path := parsepath(r.URL.Path)
|
|
||||||
Host = "http://" + r.Host
|
|
||||||
if h := r.Header["X-Forwarded-Proto"]; len(h) != 0 && h[0] == "https" {
|
if h := r.Header["X-Forwarded-Proto"]; len(h) != 0 && h[0] == "https" {
|
||||||
Host = "https://" + r.Host
|
Host = h[0] + "://" + r.Host
|
||||||
|
} else {
|
||||||
|
Host = "http://" + r.Host
|
||||||
}
|
}
|
||||||
|
|
||||||
var skunky = skunkyart{Version: Release.Version}
|
path := parsepath(r.URL.Path)
|
||||||
skunky._pth = r.URL.Path
|
// структура с функциями
|
||||||
|
var skunky skunkyart
|
||||||
skunky.Args = r.URL.Query()
|
|
||||||
arg := skunky.Args.Get
|
|
||||||
p, _ := strconv.Atoi(arg("p"))
|
|
||||||
|
|
||||||
skunky.Endpoint = path[1]
|
|
||||||
skunky.API.main = &skunky
|
|
||||||
skunky.Writer = w
|
skunky.Writer = w
|
||||||
|
skunky.Args = r.URL.Query()
|
||||||
skunky.BasePath = CFG.URI
|
skunky.BasePath = CFG.URI
|
||||||
|
|
||||||
|
arg := skunky.Args.Get
|
||||||
skunky.QueryRaw = arg("q")
|
skunky.QueryRaw = arg("q")
|
||||||
skunky.Query = url.QueryEscape(skunky.QueryRaw)
|
skunky.Query = u.QueryEscape(skunky.QueryRaw)
|
||||||
skunky.Page = p
|
|
||||||
|
|
||||||
if t := arg("type"); len(t) > 0 {
|
if t := arg("type"); len(t) > 0 {
|
||||||
skunky.Type = rune(t[0])
|
skunky.Type = rune(t[0])
|
||||||
}
|
}
|
||||||
|
p, _ := strconv.Atoi(arg("p"))
|
||||||
|
skunky.Page = p
|
||||||
|
|
||||||
if arg("atom") == "true" {
|
if arg("atom") == "true" {
|
||||||
skunky.Atom = true
|
skunky.Atom = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if CFG.Proxy {
|
// пути
|
||||||
w.Header().Add("Content-Security-Policy", "default-src 'self'; script-src 'none'; style-src 'self' 'unsafe-inline'")
|
switch path[1] {
|
||||||
} else {
|
default:
|
||||||
w.Header().Add("Content-Security-Policy", "default-src 'self'; img-src 'self' *.wixmp.com; script-src 'none'; style-src 'self' 'unsafe-inline'")
|
skunky.ReturnHTTPError(404)
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Add("X-Frame-Options", "DENY")
|
|
||||||
|
|
||||||
switch skunky.Endpoint {
|
|
||||||
// main
|
|
||||||
case "":
|
case "":
|
||||||
skunky.ExecuteTemplate("index.htm", "html", &CFG.URI)
|
skunky.ExecuteTemplate("index.htm", &CFG.URI)
|
||||||
case "about":
|
|
||||||
skunky.Templates.About = About
|
|
||||||
skunky.ExecuteTemplate("about.htm", "html", &skunky)
|
|
||||||
case "post":
|
case "post":
|
||||||
skunky.Deviation(path[2], path[3])
|
skunky.Deviation(path[2], path[3])
|
||||||
case "search":
|
case "search":
|
||||||
@ -107,38 +85,20 @@ func Router() {
|
|||||||
case "group_user":
|
case "group_user":
|
||||||
skunky.GRUser()
|
skunky.GRUser()
|
||||||
|
|
||||||
// media
|
|
||||||
case "media":
|
case "media":
|
||||||
switch path[2] {
|
switch path[2] {
|
||||||
case "file":
|
case "file":
|
||||||
if a := arg("filename"); a != "" {
|
|
||||||
skunky.SetFilename(a)
|
|
||||||
}
|
|
||||||
skunky.DownloadAndSendMedia(path[3], next(path, 4))
|
skunky.DownloadAndSendMedia(path[3], next(path, 4))
|
||||||
case "emojitar":
|
case "emojitar":
|
||||||
skunky.Emojitar(path[3])
|
skunky.Emojitar(path[3])
|
||||||
}
|
}
|
||||||
|
case "about":
|
||||||
|
skunky.About()
|
||||||
case "stylesheet":
|
case "stylesheet":
|
||||||
w.Header().Add("content-type", "text/css")
|
w.Header().Add("content-type", "text/css")
|
||||||
w.Write(open("css/skunky.css"))
|
wr(w, Templates["skunky.css"])
|
||||||
case "favicon.ico":
|
case "favicon.ico":
|
||||||
w.Write(open("images/logo.png"))
|
wr(w, Templates["logo.png"])
|
||||||
|
|
||||||
// API
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 404
|
|
||||||
default:
|
|
||||||
skunky.ReturnHTTPError(404)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
//go:build freebsd
|
|
||||||
// +build freebsd
|
|
||||||
|
|
||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func statTime(stat *syscall.Stat_t) int64 {
|
|
||||||
return time.Unix(stat.Ctimespec.Unix()).UnixMilli()
|
|
||||||
}
|
|
13
app/stat.go
13
app/stat.go
@ -1,13 +0,0 @@
|
|||||||
//go:build !freebsd
|
|
||||||
// +build !freebsd
|
|
||||||
|
|
||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func statTime(stat *syscall.Stat_t) int64 {
|
|
||||||
return time.Unix(stat.Ctim.Unix()).UnixMilli()
|
|
||||||
}
|
|
231
app/util.go
231
app/util.go
@ -1,14 +1,14 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/base64"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
u "net/url"
|
||||||
"os"
|
"os"
|
||||||
"skunkyart/static"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
"text/template"
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -17,8 +17,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
/* INTERNAL */
|
/* INTERNAL */
|
||||||
var wr = io.WriteString
|
|
||||||
|
|
||||||
func exit(msg string, code int) {
|
func exit(msg string, code int) {
|
||||||
println(msg)
|
println(msg)
|
||||||
os.Exit(code)
|
os.Exit(code)
|
||||||
@ -34,99 +32,26 @@ func tryWithExitStatus(err error, code int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func restore() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
recover()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var instances []byte
|
|
||||||
var About instanceAbout
|
|
||||||
|
|
||||||
func RefreshInstances() {
|
func RefreshInstances() {
|
||||||
for {
|
for {
|
||||||
func() {
|
func() {
|
||||||
defer restore()
|
defer func() {
|
||||||
instances = Download("https://git.macaw.me/skunky/SkunkyArt/raw/branch/master/instances.json").Body
|
if r := recover(); r != nil {
|
||||||
try(json.Unmarshal(instances, &About))
|
recover()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
Templates["instances.json"] = string(Download("https://git.macaw.me/skunky/SkunkyArt/raw/branch/master/instances.json").Body)
|
||||||
}()
|
}()
|
||||||
time.Sleep(1 * time.Hour)
|
time.Sleep(1 * time.Hour)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// some crap for frontend
|
// some crap for frontend
|
||||||
type instanceAbout struct {
|
func (s skunkyart) ExecuteTemplate(file string, data any) {
|
||||||
Proxy bool
|
|
||||||
Nsfw bool
|
|
||||||
Instances []settings
|
|
||||||
}
|
|
||||||
|
|
||||||
type skunkyart struct {
|
|
||||||
Writer http.ResponseWriter
|
|
||||||
_pth string
|
|
||||||
|
|
||||||
Args url.Values
|
|
||||||
Page int
|
|
||||||
Type rune
|
|
||||||
Atom bool
|
|
||||||
|
|
||||||
BasePath, Endpoint string
|
|
||||||
Query, QueryRaw string
|
|
||||||
|
|
||||||
API API
|
|
||||||
Version string
|
|
||||||
|
|
||||||
Templates struct {
|
|
||||||
About instanceAbout
|
|
||||||
|
|
||||||
SomeList string
|
|
||||||
DDStrips string
|
|
||||||
Deviation struct {
|
|
||||||
Post devianter.Post
|
|
||||||
Related string
|
|
||||||
StringTime string
|
|
||||||
Tags string
|
|
||||||
Comments string
|
|
||||||
}
|
|
||||||
|
|
||||||
GroupUser struct {
|
|
||||||
GR devianter.GRuser
|
|
||||||
Admins string
|
|
||||||
Group bool
|
|
||||||
CreationDate string
|
|
||||||
|
|
||||||
About struct {
|
|
||||||
A devianter.About
|
|
||||||
|
|
||||||
DescriptionFormatted string
|
|
||||||
Interests, Social string
|
|
||||||
Comments string
|
|
||||||
BG string
|
|
||||||
BGMeta devianter.Deviation
|
|
||||||
}
|
|
||||||
|
|
||||||
Gallery struct {
|
|
||||||
Folders string
|
|
||||||
Pages int
|
|
||||||
List string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Search struct {
|
|
||||||
Content devianter.Search
|
|
||||||
List string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s skunkyart) ExecuteTemplate(file, dir string, data any) {
|
|
||||||
var buf strings.Builder
|
var buf strings.Builder
|
||||||
tmp := template.New(file)
|
tmp := template.New(file)
|
||||||
tmp, err := tmp.ParseFS(static.Templates, dir+"/*")
|
tmp, e := tmp.Parse(Templates[file])
|
||||||
if err != nil {
|
try(e)
|
||||||
s.Writer.WriteHeader(500)
|
|
||||||
wr(s.Writer, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try(tmp.Execute(&buf, &data))
|
try(tmp.Execute(&buf, &data))
|
||||||
wr(s.Writer, buf.String())
|
wr(s.Writer, buf.String())
|
||||||
}
|
}
|
||||||
@ -138,26 +63,13 @@ func UrlBuilder(strs ...string) string {
|
|||||||
str.WriteString(CFG.URI)
|
str.WriteString(CFG.URI)
|
||||||
for n, x := range strs {
|
for n, x := range strs {
|
||||||
str.WriteString(x)
|
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+1 < l && !(strs[n+1][0] == '?' || strs[n+1][0] == '&') && !(x[0] == '?' || x[0] == '&') {
|
||||||
str.WriteString("/")
|
str.WriteString("/")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return str.String()
|
return str.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s skunkyart) Error(dAerr devianter.Error) {
|
|
||||||
s.Writer.WriteHeader(502)
|
|
||||||
|
|
||||||
var msg strings.Builder
|
|
||||||
msg.WriteString(`<html><link rel="stylesheet" href="`)
|
|
||||||
msg.WriteString(UrlBuilder("stylesheet"))
|
|
||||||
msg.WriteString(`" /><h3>DeviantArt error — '`)
|
|
||||||
msg.WriteString(dAerr.Error)
|
|
||||||
msg.WriteString("'</h3></html>")
|
|
||||||
|
|
||||||
wr(s.Writer, msg.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s skunkyart) ReturnHTTPError(status int) {
|
func (s skunkyart) ReturnHTTPError(status int) {
|
||||||
s.Writer.WriteHeader(status)
|
s.Writer.WriteHeader(status)
|
||||||
|
|
||||||
@ -173,29 +85,21 @@ func (s skunkyart) ReturnHTTPError(status int) {
|
|||||||
wr(s.Writer, msg.String())
|
wr(s.Writer, msg.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s skunkyart) SetFilename(name string) {
|
|
||||||
var filename strings.Builder
|
|
||||||
filename.WriteString(`filename="`)
|
|
||||||
filename.WriteString(name)
|
|
||||||
filename.WriteString(`"`)
|
|
||||||
s.Writer.Header().Add("Content-Disposition", filename.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
type Downloaded struct {
|
type Downloaded struct {
|
||||||
Headers http.Header
|
Headers http.Header
|
||||||
Status int
|
Status int
|
||||||
Body []byte
|
Body []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func Download(urlString string) (d Downloaded) {
|
func Download(url string) (d Downloaded) {
|
||||||
cli := &http.Client{}
|
cli := &http.Client{}
|
||||||
if CFG.DownloadProxy != "" {
|
if CFG.DownloadProxy != "" {
|
||||||
u, e := url.Parse(CFG.DownloadProxy)
|
u, e := u.Parse(CFG.DownloadProxy)
|
||||||
try(e)
|
try(e)
|
||||||
cli.Transport = &http.Transport{Proxy: http.ProxyURL(u)}
|
cli.Transport = &http.Transport{Proxy: http.ProxyURL(u)}
|
||||||
}
|
}
|
||||||
|
|
||||||
req, e := http.NewRequest("GET", urlString, nil)
|
req, e := http.NewRequest("GET", url, nil)
|
||||||
try(e)
|
try(e)
|
||||||
req.Header.Set("User-Agent", CFG.UserAgent)
|
req.Header.Set("User-Agent", CFG.UserAgent)
|
||||||
|
|
||||||
@ -211,20 +115,97 @@ func Download(urlString string) (d Downloaded) {
|
|||||||
return
|
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 {
|
||||||
|
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
|
||||||
|
os.Mkdir(CFG.Cache.Path, 0700)
|
||||||
|
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 != "" {
|
||||||
|
now := time.Now().UnixMilli()
|
||||||
|
|
||||||
|
f, _ := os.Stat(a)
|
||||||
|
stat := f.Sys().(*syscall.Stat_t)
|
||||||
|
time := time.Unix(stat.Ctim.Unix()).UnixMilli()
|
||||||
|
|
||||||
|
if time+lifetimeParsed <= 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)
|
||||||
|
tryWithExitStatus(e, 1)
|
||||||
|
|
||||||
|
for _, x := range dir {
|
||||||
|
file, e := os.ReadFile(dirname + "/" + x.Name())
|
||||||
|
tryWithExitStatus(e, 1)
|
||||||
|
Templates[x.Name()] = string(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* PARSING HELPERS */
|
/* PARSING HELPERS */
|
||||||
func ParseMedia(media devianter.Media, thumb ...int) string {
|
func ParseMedia(media devianter.Media, thumb ...int) string {
|
||||||
mediaUrl, filename := devianter.UrlFromMedia(media, thumb...)
|
url := devianter.UrlFromMedia(media, thumb...)
|
||||||
if len(mediaUrl) != 0 && CFG.Proxy {
|
if len(url) != 0 && CFG.Proxy {
|
||||||
mediaUrl = mediaUrl[21:]
|
url = url[21:]
|
||||||
dot := strings.Index(mediaUrl, ".")
|
dot := strings.Index(url, ".")
|
||||||
if filename == "" {
|
|
||||||
filename = "image.gif"
|
return UrlBuilder("media", "file", url[:dot], url[dot+11:])
|
||||||
}
|
|
||||||
return UrlBuilder("media", "file", mediaUrl[:dot], mediaUrl[dot+11:], "&filename=", filename)
|
|
||||||
} else if !CFG.Proxy {
|
|
||||||
return mediaUrl
|
|
||||||
}
|
}
|
||||||
return ""
|
return url
|
||||||
}
|
}
|
||||||
|
|
||||||
func ConvertDeviantArtUrlToSkunkyArt(url string) (output string) {
|
func ConvertDeviantArtUrlToSkunkyArt(url string) (output string) {
|
||||||
@ -274,9 +255,7 @@ func (s skunkyart) NavBase(c DeviationList) string {
|
|||||||
list.WriteString("<br>")
|
list.WriteString("<br>")
|
||||||
prevrev := func(msg string, page int, onpage bool) {
|
prevrev := func(msg string, page int, onpage bool) {
|
||||||
if !onpage {
|
if !onpage {
|
||||||
list.WriteString(`<a href="`)
|
list.WriteString(`<a href="?p=`)
|
||||||
list.WriteString(s._pth)
|
|
||||||
list.WriteString(`?p=`)
|
|
||||||
list.WriteString(strconv.Itoa(page))
|
list.WriteString(strconv.Itoa(page))
|
||||||
if s.Type != 0 {
|
if s.Type != 0 {
|
||||||
list.WriteString("&type=")
|
list.WriteString("&type=")
|
||||||
|
221
app/wrapper.go
221
app/wrapper.go
@ -1,6 +1,10 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -10,6 +14,65 @@ import (
|
|||||||
"golang.org/x/net/html"
|
"golang.org/x/net/html"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var wr = io.WriteString
|
||||||
|
var Templates = make(map[string]string)
|
||||||
|
|
||||||
|
type skunkyart struct {
|
||||||
|
Writer http.ResponseWriter
|
||||||
|
|
||||||
|
Args url.Values
|
||||||
|
BasePath string
|
||||||
|
Type rune
|
||||||
|
Query, QueryRaw string
|
||||||
|
Page int
|
||||||
|
Atom bool
|
||||||
|
|
||||||
|
Templates struct {
|
||||||
|
About struct {
|
||||||
|
Proxy bool
|
||||||
|
Nsfw bool
|
||||||
|
Instances []settings
|
||||||
|
}
|
||||||
|
|
||||||
|
SomeList string
|
||||||
|
DDStrips string
|
||||||
|
Deviation struct {
|
||||||
|
Post devianter.Post
|
||||||
|
Related string
|
||||||
|
StringTime string
|
||||||
|
Tags string
|
||||||
|
Comments string
|
||||||
|
}
|
||||||
|
|
||||||
|
GroupUser struct {
|
||||||
|
GR devianter.GRuser
|
||||||
|
Admins string
|
||||||
|
Group bool
|
||||||
|
CreationDate string
|
||||||
|
|
||||||
|
About struct {
|
||||||
|
A devianter.About
|
||||||
|
|
||||||
|
DescriptionFormatted string
|
||||||
|
Interests, Social string
|
||||||
|
Comments string
|
||||||
|
BG string
|
||||||
|
BGMeta devianter.Deviation
|
||||||
|
}
|
||||||
|
|
||||||
|
Gallery struct {
|
||||||
|
Folders string
|
||||||
|
Pages int
|
||||||
|
List string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Search struct {
|
||||||
|
Content devianter.Search
|
||||||
|
List string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s skunkyart) GRUser() {
|
func (s skunkyart) GRUser() {
|
||||||
if len(s.Query) < 1 {
|
if len(s.Query) < 1 {
|
||||||
s.ReturnHTTPError(400)
|
s.ReturnHTTPError(400)
|
||||||
@ -17,15 +80,10 @@ func (s skunkyart) GRUser() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var g devianter.Group
|
var g devianter.Group
|
||||||
var daError devianter.Error
|
|
||||||
g.Name = s.Query
|
g.Name = s.Query
|
||||||
var err error
|
var err error
|
||||||
s.Templates.GroupUser.GR, err, daError = g.Get()
|
s.Templates.GroupUser.GR, err = g.GetGroup()
|
||||||
try(err)
|
try(err)
|
||||||
if daError.RAW != nil {
|
|
||||||
s.Error(daError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
group := &s.Templates.GroupUser
|
group := &s.Templates.GroupUser
|
||||||
|
|
||||||
@ -46,6 +104,7 @@ func (s skunkyart) GRUser() {
|
|||||||
group.About.A = x.ModuleData.About
|
group.About.A = x.ModuleData.About
|
||||||
var about = &group.About.A
|
var about = &group.About.A
|
||||||
group.CreationDate = time.Unix(time.Now().Unix()-x.ModuleData.About.RegDate, 0).UTC().String()
|
group.CreationDate = time.Unix(time.Now().Unix()-x.ModuleData.About.RegDate, 0).UTC().String()
|
||||||
|
|
||||||
group.About.DescriptionFormatted = ParseDescription(about.Description)
|
group.About.DescriptionFormatted = ParseDescription(about.Description)
|
||||||
|
|
||||||
for _, val := range x.ModuleData.About.SocialLinks {
|
for _, val := range x.ModuleData.About.SocialLinks {
|
||||||
@ -67,7 +126,12 @@ func (s skunkyart) GRUser() {
|
|||||||
group.About.Interests += interest.String()
|
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":
|
case "cover_deviation":
|
||||||
group.About.BGMeta = x.ModuleData.CoverDeviation.Deviation
|
group.About.BGMeta = x.ModuleData.CoverDeviation.Deviation
|
||||||
@ -82,70 +146,51 @@ func (s skunkyart) GRUser() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
case 'g', 'f':
|
case 'g':
|
||||||
var all bool
|
|
||||||
var content devianter.Group
|
|
||||||
|
|
||||||
folderid, _ := strconv.Atoi(s.Args.Get("folder"))
|
folderid, _ := strconv.Atoi(s.Args.Get("folder"))
|
||||||
|
|
||||||
if a := s.Args.Get("all"); a == "true" {
|
|
||||||
all = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.Page == 0 {
|
if s.Page == 0 {
|
||||||
s.Page++
|
s.Page++
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.Type == 'f' {
|
gallery, err := g.GetGallery(s.Page, folderid)
|
||||||
content, daError = g.Favourites(s.Page, all, folderid)
|
try(err)
|
||||||
} else {
|
|
||||||
content, err, daError = g.Gallery(s.Page, folderid)
|
|
||||||
try(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if daError.RAW != nil {
|
if folderid > 0 {
|
||||||
s.Error(daError)
|
group.Gallery.List = s.DeviationList(gallery.Content.Results, true, DeviationList{
|
||||||
return
|
More: gallery.Content.HasMore,
|
||||||
}
|
|
||||||
|
|
||||||
if folderid > 0 || (s.Type == 'f' && all) {
|
|
||||||
group.Gallery.List = s.DeviationList(content.Content.Results, true, DeviationList{
|
|
||||||
More: content.Content.HasMore,
|
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
for _, x := range content.Content.Gruser.Page.Modules {
|
for _, x := range gallery.Content.Gruser.Page.Modules {
|
||||||
if l := len(x.ModuleData.Folders.Results); l != 0 {
|
if l := len(x.ModuleData.Folders.Results); l != 0 {
|
||||||
var folders strings.Builder
|
var folders strings.Builder
|
||||||
folders.WriteString(`<h1 id="folders"><a href="#folder">#</a> Folders</h1><div class="folders"><br>`)
|
folders.WriteString(`<h1 id="folders"><a href="#folder">#</a> Folders</h1><div class="folders"><br>`)
|
||||||
for _, x := range x.ModuleData.Folders.Results {
|
for _, x := range x.ModuleData.Folders.Results {
|
||||||
if x.FolderId != -1 && x.Size != 0 {
|
folders.WriteString(`<div class="block folder-item">`)
|
||||||
folders.WriteString(`<div class="block folder-item">`)
|
|
||||||
|
|
||||||
if !(x.Thumb.NSFW && !CFG.Nsfw) {
|
if !(x.Thumb.NSFW && !CFG.Nsfw) {
|
||||||
folders.WriteString(`<a href="`)
|
folders.WriteString(`<a href="`)
|
||||||
folders.WriteString(ConvertDeviantArtUrlToSkunkyArt(x.Thumb.Url))
|
folders.WriteString(ConvertDeviantArtUrlToSkunkyArt(x.Thumb.Url))
|
||||||
folders.WriteString(`"><img loading="lazy" src="`)
|
folders.WriteString(`"><img loading="lazy" src="`)
|
||||||
folders.WriteString(ParseMedia(x.Thumb.Media))
|
folders.WriteString(ParseMedia(x.Thumb.Media))
|
||||||
folders.WriteString(`" title="`)
|
folders.WriteString(`" title="`)
|
||||||
folders.WriteString(x.Thumb.Title)
|
folders.WriteString(x.Thumb.Title)
|
||||||
folders.WriteString(`"></a>`)
|
folders.WriteString(`"></a>`)
|
||||||
} else {
|
} else {
|
||||||
folders.WriteString(`<h1>[ <span class="nsfw">NSFW</span> ]</h1>`)
|
folders.WriteString(`<h1>[ <span class="nsfw">NSFW</span> ]</h1>`)
|
||||||
}
|
|
||||||
folders.WriteString("<br>")
|
|
||||||
|
|
||||||
folders.WriteString(`<a href="group_user?folder=`)
|
|
||||||
folders.WriteString(strconv.Itoa(x.FolderId))
|
|
||||||
folders.WriteString("&q=")
|
|
||||||
folders.WriteString(s.Query)
|
|
||||||
folders.WriteString("&type=")
|
|
||||||
folders.WriteString(string(s.Type))
|
|
||||||
folders.WriteString(`">`)
|
|
||||||
folders.WriteString(x.Name)
|
|
||||||
folders.WriteString(`</a>`)
|
|
||||||
|
|
||||||
folders.WriteString("</div>")
|
|
||||||
}
|
}
|
||||||
|
folders.WriteString("<br>")
|
||||||
|
|
||||||
|
folders.WriteString(`<a href="?folder=`)
|
||||||
|
folders.WriteString(strconv.Itoa(x.FolderId))
|
||||||
|
folders.WriteString("&q=")
|
||||||
|
folders.WriteString(s.Query)
|
||||||
|
folders.WriteString("&type=")
|
||||||
|
folders.WriteString(string(s.Type))
|
||||||
|
folders.WriteString(`">`)
|
||||||
|
folders.WriteString(x.Name)
|
||||||
|
folders.WriteString(`</a>`)
|
||||||
|
|
||||||
|
folders.WriteString("</div>")
|
||||||
}
|
}
|
||||||
folders.WriteString(`</div><h1 id="content"><a href="#content">#</a> Content</h1>`)
|
folders.WriteString(`</div><h1 id="content"><a href="#content">#</a> Content</h1>`)
|
||||||
group.Gallery.Folders = folders.String()
|
group.Gallery.Folders = folders.String()
|
||||||
@ -164,7 +209,7 @@ func (s skunkyart) GRUser() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !s.Atom {
|
if !s.Atom {
|
||||||
s.ExecuteTemplate("gruser.htm", "html", &s)
|
s.ExecuteTemplate("gruser.htm", &s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,26 +221,19 @@ func (s skunkyart) Deviation(author, postname string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var err devianter.Error
|
|
||||||
post := &s.Templates.Deviation
|
post := &s.Templates.Deviation
|
||||||
|
|
||||||
id := id_search[len(id_search)-1]
|
id := id_search[len(id_search)-1]
|
||||||
post.Post, err = devianter.GetDeviation(id, author)
|
post.Post = devianter.GetDeviation(id, author)
|
||||||
if err.RAW != nil {
|
|
||||||
s.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if post.Post.Comments.Total <= 50 {
|
|
||||||
post.Post.Comments.Cursor = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if post.Post.Deviation.TextContent.Excerpt != "" {
|
if post.Post.Deviation.TextContent.Excerpt != "" {
|
||||||
post.Post.Description = ParseDescription(post.Post.Deviation.TextContent)
|
post.Post.Description = ParseDescription(post.Post.Deviation.TextContent)
|
||||||
} else {
|
} else {
|
||||||
post.Post.Description = ParseDescription(post.Post.Deviation.Extended.DescriptionText)
|
post.Post.Description = ParseDescription(post.Post.Deviation.Extended.DescriptionText)
|
||||||
}
|
}
|
||||||
|
// время публикации
|
||||||
|
post.StringTime = post.Post.Deviation.PublishedTime.UTC().String()
|
||||||
|
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, false)
|
post.Related += s.DeviationList(x.Deviations, false)
|
||||||
@ -214,19 +252,17 @@ func (s skunkyart) Deviation(author, postname string) {
|
|||||||
post.Tags += tag.String()
|
post.Tags += tag.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
post.Comments = s.ParseComments(devianter.GetComments(id, post.Post.Comments.Cursor, s.Page, 1))
|
if post.Post.Comments.Total <= 50 {
|
||||||
post.StringTime = post.Post.Deviation.PublishedTime.UTC().String()
|
post.Post.Comments.Cursor = ""
|
||||||
post.Post.IMG = ParseMedia(post.Post.Deviation.Media)
|
}
|
||||||
|
|
||||||
s.ExecuteTemplate("deviantion.htm", "html", &s)
|
post.Comments = s.ParseComments(devianter.GetComments(id, post.Post.Comments.Cursor, s.Page, 1))
|
||||||
|
|
||||||
|
s.ExecuteTemplate("deviantion.htm", &s)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s skunkyart) DD() {
|
func (s skunkyart) DD() {
|
||||||
dd, err := devianter.GetDailyDeviations(s.Page)
|
dd := devianter.GetDailyDeviations(s.Page)
|
||||||
if err.RAW != nil {
|
|
||||||
s.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var strips strings.Builder
|
var strips strings.Builder
|
||||||
for _, x := range dd.Strips {
|
for _, x := range dd.Strips {
|
||||||
strips.WriteString(`<h3 class="`)
|
strips.WriteString(`<h3 class="`)
|
||||||
@ -245,24 +281,18 @@ func (s skunkyart) DD() {
|
|||||||
More: dd.HasMore,
|
More: dd.HasMore,
|
||||||
})
|
})
|
||||||
if !s.Atom {
|
if !s.Atom {
|
||||||
s.ExecuteTemplate("daily.htm", "html", &s)
|
s.ExecuteTemplate("daily.htm", &s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s skunkyart) Search() {
|
func (s skunkyart) Search() {
|
||||||
if s.Query == "" {
|
|
||||||
s.ReturnHTTPError(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
var daError devianter.Error
|
|
||||||
ss := &s.Templates.Search
|
ss := &s.Templates.Search
|
||||||
switch s.Type {
|
switch s.Type {
|
||||||
case 'a', 't':
|
case 'a', 't':
|
||||||
ss.Content, err, daError = devianter.PerformSearch(s.Query, s.Page, s.Type)
|
ss.Content, err = devianter.PerformSearch(s.Query, s.Page, s.Type)
|
||||||
case 'g', 'f':
|
case 'g':
|
||||||
ss.Content, err, daError = devianter.PerformSearch(s.Query, s.Page, s.Type, s.Args.Get("usr"))
|
ss.Content, err = devianter.PerformSearch(s.Query, s.Page, s.Type, s.Args.Get("usr"))
|
||||||
case 'r': // скраппер, поскольку девиантартовцы зажопили гостевое API для поиска групп
|
case 'r': // скраппер, поскольку девиантартовцы зажопили гостевое API для поиска групп
|
||||||
var (
|
var (
|
||||||
usernames = make(map[int]string)
|
usernames = make(map[int]string)
|
||||||
@ -308,23 +338,17 @@ func (s skunkyart) Search() {
|
|||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
s.ReturnHTTPError(400)
|
s.ReturnHTTPError(400)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
try(err)
|
try(err)
|
||||||
|
|
||||||
if s.Type != 'r' {
|
if s.Type != 'r' {
|
||||||
if daError.RAW != nil {
|
|
||||||
s.Error(daError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ss.List = s.DeviationList(ss.Content.Results, false, DeviationList{
|
ss.List = s.DeviationList(ss.Content.Results, false, DeviationList{
|
||||||
Pages: ss.Content.Pages,
|
Pages: ss.Content.Pages,
|
||||||
More: ss.Content.HasMore,
|
More: ss.Content.HasMore,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
s.ExecuteTemplate("search.htm", "html", &s)
|
s.ExecuteTemplate("search.htm", &s)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s skunkyart) Emojitar(name string) {
|
func (s skunkyart) Emojitar(name string) {
|
||||||
@ -339,3 +363,10 @@ func (s skunkyart) Emojitar(name string) {
|
|||||||
}
|
}
|
||||||
wr(s.Writer, ae)
|
wr(s.Writer, ae)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s skunkyart) About() {
|
||||||
|
s.Templates.About.Nsfw = CFG.Nsfw
|
||||||
|
s.Templates.About.Proxy = CFG.Proxy
|
||||||
|
try(json.Unmarshal([]byte(Templates["instances.json"]), &s.Templates.About))
|
||||||
|
s.ExecuteTemplate("about.htm", &s)
|
||||||
|
}
|
||||||
|
@ -1,16 +1,20 @@
|
|||||||
{
|
{
|
||||||
"listen": "0.0.0.0:3003",
|
"listen": "0:3003",
|
||||||
"uri": "/",
|
"uri": "/",
|
||||||
"cache": {
|
"cache": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"path": "cache",
|
"path": "cache",
|
||||||
"lifetime": null,
|
"lifetime": null,
|
||||||
"max-size": 200,
|
"max-size": 1024,
|
||||||
"update-interval": 5
|
"update-interval": 5
|
||||||
},
|
},
|
||||||
"static-path": "static",
|
"dirs-to-memory": [
|
||||||
|
"html",
|
||||||
|
"css",
|
||||||
|
"misc"
|
||||||
|
],
|
||||||
"download-proxy": "http://127.0.0.1:8080",
|
"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",
|
"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,
|
"proxy": true,
|
||||||
"nsfw": true
|
"nsfw": false
|
||||||
}
|
}
|
||||||
|
@ -19,21 +19,15 @@ header h1 {
|
|||||||
header form {
|
header form {
|
||||||
align-self: center;
|
align-self: center;
|
||||||
}
|
}
|
||||||
header, form {
|
header {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
form {
|
|
||||||
border: solid #164e3e 1px;
|
|
||||||
max-width: fit-content;
|
|
||||||
}
|
|
||||||
form input, button, select {
|
form input, button, select {
|
||||||
background-color: #134134;
|
background-color: #134134;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
color: whitesmoke;
|
color: whitesmoke;
|
||||||
border: 0;
|
border: 0px;
|
||||||
}
|
border-radius: 1px;
|
||||||
input:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* BLOCKS */
|
/* BLOCKS */
|
||||||
@ -146,17 +140,18 @@ input:focus {
|
|||||||
font-size: 80%
|
font-size: 80%
|
||||||
}
|
}
|
||||||
|
|
||||||
header {
|
center form {
|
||||||
margin-left: 3%;
|
font-size: 60%
|
||||||
text-align: center;
|
|
||||||
display: inline-block;
|
|
||||||
clear: both;
|
|
||||||
font-size: 200%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
form {
|
header form {
|
||||||
font-size: 60%;
|
font-size: 60%;
|
||||||
border: solid #164e3e 5px;
|
}
|
||||||
|
header, center {
|
||||||
|
text-align: center;
|
||||||
|
display: block;
|
||||||
|
clear: both;
|
||||||
|
font-size: 200%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
4
go.mod
4
go.mod
@ -1,8 +1,8 @@
|
|||||||
module skunkyart
|
module skunkyart
|
||||||
|
|
||||||
go 1.18
|
go 1.22.3
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.macaw.me/skunky/devianter v0.2.6-0.20240904171839-b3c99749f133
|
git.macaw.me/skunky/devianter v0.2.5
|
||||||
golang.org/x/net v0.27.0
|
golang.org/x/net v0.27.0
|
||||||
)
|
)
|
||||||
|
4
go.sum
4
go.sum
@ -1,4 +1,4 @@
|
|||||||
git.macaw.me/skunky/devianter v0.2.6-0.20240904171839-b3c99749f133 h1:ziutYUyDmdbsptR8Lj4lNmZUxfgwGsNbHM1mO9ATph8=
|
git.macaw.me/skunky/devianter v0.2.5 h1:aAc6CG/ghvG130Ob7gGUdK4IV3MSeCD5t3QIJjto1M0=
|
||||||
git.macaw.me/skunky/devianter v0.2.6-0.20240904171839-b3c99749f133/go.mod h1:ZLn527xBlnpXrUB1B8z/MhyeiWVK4nPWjyfnhWOE8Is=
|
git.macaw.me/skunky/devianter v0.2.5/go.mod h1:ZLn527xBlnpXrUB1B8z/MhyeiWVK4nPWjyfnhWOE8Is=
|
||||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||||
|
@ -1,12 +1,27 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
{{template "head" .}}
|
<head>
|
||||||
|
<title>SkunkyArt</title>
|
||||||
|
<link rel="stylesheet" href="{{.BasePath}}stylesheet">
|
||||||
|
<link rel="icon" type="image/x-icon" href="{{.BasePath}}favicon.ico">
|
||||||
|
</head>
|
||||||
<main>
|
<main>
|
||||||
{{template "header" .}}
|
<header>
|
||||||
|
<h1><a href="{{.BasePath}}">HOME</a> | <a href="{{.BasePath}}dd">DD</a></h1>
|
||||||
|
<form method="get" action="{{.BasePath}}search">
|
||||||
|
<input type="text" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false">
|
||||||
|
<select name="type">
|
||||||
|
<option value="all">All</option>
|
||||||
|
<option value="tag">Tag</option>
|
||||||
|
<option value="r">Groups</option>
|
||||||
|
</select>
|
||||||
|
<button type="submit">Search!</button>
|
||||||
|
</form>
|
||||||
|
</header>
|
||||||
<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:ls.404.mn" 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>
|
<b>Instance settings:</b>
|
||||||
<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>
|
||||||
@ -44,6 +59,6 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
</ul>
|
</ul>
|
||||||
</details>
|
</details>
|
||||||
<p>Copyright <a href="https://go.kde.org/matrix/#/@softpigeones:ls.404.mn" target="_blank">lost+skunk</a>, X11. <a href="https://git.macaw.me/skunky/skunkyart/src/tag/v{{.Version}}" target="_blank">SkunkyArt v{{.Version}}</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.1" target="_blank">SkunkyArt v1.3.1</a></p>
|
||||||
</main>
|
</main>
|
||||||
</html>
|
</html>
|
28
html/daily.htm
Normal file
28
html/daily.htm
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>SkunkyArt | Daily Deviations</title>
|
||||||
|
<link rel="stylesheet" href="{{.BasePath}}stylesheet">
|
||||||
|
<link rel="icon" type="image/x-icon" href="{{.BasePath}}favicon.ico">
|
||||||
|
</head>
|
||||||
|
<main>
|
||||||
|
<header>
|
||||||
|
<h1><a href="{{.BasePath}}">HOME</a> | <a href="{{.BasePath}}dd">DD</a> | <a href="?atom=true">RSS</a></h1>
|
||||||
|
<form method="get" action="{{.BasePath}}search">
|
||||||
|
<input type="text" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false">
|
||||||
|
<select name="type">
|
||||||
|
<option value="all">All</option>
|
||||||
|
<option value="tag">Tag</option>
|
||||||
|
<option value="r">Groups</option>
|
||||||
|
</select>
|
||||||
|
<button type="submit">Search!</button>
|
||||||
|
</form>
|
||||||
|
</header>
|
||||||
|
{{if ne .Templates.DDStrips ""}}
|
||||||
|
<h2 id="strips"><a href="#strips">#</a> Strips</h2>
|
||||||
|
{{.Templates.DDStrips}}
|
||||||
|
{{end}}
|
||||||
|
<h2 id="content"><a href="#content">#</a> Content</h2>
|
||||||
|
{{.Templates.SomeList}}
|
||||||
|
</main>
|
||||||
|
</html>
|
@ -1,8 +1,24 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
{{template "head" . }}
|
<head>
|
||||||
|
<title>SkunkyArt | {{.Templates.Deviation.Post.Deviation.Author.Username}} - {{.Templates.Deviation.Post.Deviation.Title}}</title>
|
||||||
|
<link rel="stylesheet" href="{{.BasePath}}stylesheet">
|
||||||
|
<meta name="referrer" content="no-referrer" />
|
||||||
|
<link rel="icon" type="image/x-icon" href="{{.BasePath}}favicon.ico">
|
||||||
|
</head>
|
||||||
<main>
|
<main>
|
||||||
{{template "header" . }}
|
<header>
|
||||||
|
<h1><a href="{{.BasePath}}">HOME</a> | <a href="{{.BasePath}}dd">DD</a></h1>
|
||||||
|
<form method="get" action="{{.BasePath}}search">
|
||||||
|
<input type="text" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false">
|
||||||
|
<select name="type">
|
||||||
|
<option value="all">All</option>
|
||||||
|
<option value="tag">Tag</option>
|
||||||
|
<option value="r">Groups</option>
|
||||||
|
</select>
|
||||||
|
<button type="submit">Search!</button>
|
||||||
|
</form>
|
||||||
|
</header>
|
||||||
<figure>
|
<figure>
|
||||||
<img src="{{.BasePath}}media/emojitar/{{.Templates.Deviation.Post.Deviation.Author.Username}}?type=a" width="30px">
|
<img src="{{.BasePath}}media/emojitar/{{.Templates.Deviation.Post.Deviation.Author.Username}}?type=a" width="30px">
|
||||||
<span><strong><a href="{{.BasePath}}group_user?type=about&q={{.Templates.Deviation.Post.Deviation.Author.Username}}">{{.Templates.Deviation.Post.Deviation.Author.Username}}</a></strong> — {{if (.Templates.Deviation.Post.Deviation.DD)}}
|
<span><strong><a href="{{.BasePath}}group_user?type=about&q={{.Templates.Deviation.Post.Deviation.Author.Username}}">{{.Templates.Deviation.Post.Deviation.Author.Username}}</a></strong> — {{if (.Templates.Deviation.Post.Deviation.DD)}}
|
@ -1,27 +1,26 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
{{template "head" . }}
|
<head>
|
||||||
|
<title>SkunkyArt |
|
||||||
|
{{if eq .Type 'a'}}
|
||||||
|
{{.Templates.GroupUser.GR.Owner.Username}}
|
||||||
|
{{else}}
|
||||||
|
gallery of {{.Templates.GroupUser.GR.Owner.Username}}
|
||||||
|
{{end}}
|
||||||
|
</title>
|
||||||
|
<link rel="stylesheet" href="{{.BasePath}}stylesheet">
|
||||||
|
<link rel="icon" type="image/x-icon" href="{{.BasePath}}favicon.ico">
|
||||||
|
</head>
|
||||||
<main>
|
<main>
|
||||||
<header>
|
<header>
|
||||||
<h1>
|
<h1><a href="{{.BasePath}}">HOME</a> | <a href="{{.BasePath}}dd">DD</a>
|
||||||
<a href="{{.BasePath}}">HOME</a>
|
| <a href="?q={{.Templates.GroupUser.GR.Owner.Username}}&type={{if eq .Type 'a'}}gallery">Gallery{{else}}about">About{{end}}</a>
|
||||||
| <a href="{{.BasePath}}dd">DD</a>
|
| <a href="?q={{.Templates.GroupUser.GR.Owner.Username}}&type=gallery&atom=true">RSS</a></h1>
|
||||||
{{if ne .Type 'f'}}
|
|
||||||
| <a href="group_user?q={{.Templates.GroupUser.GR.Owner.Username}}&type={{if eq .Type 'a'}}gallery">Gallery{{else}}about">About{{end}}</a>
|
|
||||||
| <a href="group_user?q={{.Templates.GroupUser.GR.Owner.Username}}&type=favourites">Favourites</a>
|
|
||||||
{{else}}
|
|
||||||
| <a href="group_user?q={{.Templates.GroupUser.GR.Owner.Username}}&type=about">About</a>
|
|
||||||
| <a href="group_user?q={{.Templates.GroupUser.GR.Owner.Username}}&type=gallery">Gallery</a>
|
|
||||||
| <a href="group_user?q={{.Templates.GroupUser.GR.Owner.Username}}&type=favourites">Favourites</a>
|
|
||||||
{{end}}
|
|
||||||
| <a href="group_user?q={{.Templates.GroupUser.GR.Owner.Username}}&type=gallery&atom=true">RSS</a>
|
|
||||||
</h1>
|
|
||||||
<form method="get" action="{{.BasePath}}search">
|
<form method="get" action="{{.BasePath}}search">
|
||||||
<input type="gallery" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false">
|
<input type="gallery" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false">
|
||||||
<input type="hidden" name="usr" value="{{.Templates.GroupUser.GR.Owner.Username}}">
|
<input type="hidden" name="usr" value="{{.Templates.GroupUser.GR.Owner.Username}}">
|
||||||
<select name="type">
|
<select name="type">
|
||||||
<option value="gallery">Gallery</option>
|
<option value="gallery">Gallery</option>
|
||||||
<option value="folders">Folders</option>
|
|
||||||
<option value="all">All</option>
|
<option value="all">All</option>
|
||||||
<option value="tag">Tag</option>
|
<option value="tag">Tag</option>
|
||||||
<option value="r">Groups</option>
|
<option value="r">Groups</option>
|
22
html/index.htm
Normal file
22
html/index.htm
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>SkunkyArt</title>
|
||||||
|
<link rel="stylesheet" href="{{.}}stylesheet"/>
|
||||||
|
<link rel="icon" type="image/x-icon" href="{{.}}favicon.ico">
|
||||||
|
</head>
|
||||||
|
<main>
|
||||||
|
<center>
|
||||||
|
<form method="get" action="{{.}}search">
|
||||||
|
<input type="text" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false">
|
||||||
|
<select name="type">
|
||||||
|
<option value="all">All</option>
|
||||||
|
<option value="tag">Tag</option>
|
||||||
|
<option value="r">Groups</option>
|
||||||
|
</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>
|
||||||
|
</main>
|
||||||
|
</html>
|
30
html/search.htm
Normal file
30
html/search.htm
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>SkunkyArt | Search "{{.QueryRaw}}"</title>
|
||||||
|
<link rel="stylesheet" href="{{.BasePath}}stylesheet">
|
||||||
|
<link rel="icon" type="image/x-icon" href="{{.BasePath}}favicon.ico">
|
||||||
|
</head>
|
||||||
|
<main>
|
||||||
|
<header>
|
||||||
|
<h1><a href="{{.BasePath}}">HOME</a> | <a href="{{.BasePath}}dd">DD</a></h1>
|
||||||
|
<form method="get" action="search">
|
||||||
|
<input type="text" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false">
|
||||||
|
<select name="type">
|
||||||
|
<option value="all">All</option>
|
||||||
|
<option value="tag">Tag</option>
|
||||||
|
<option value="r">Groups</option>
|
||||||
|
</select>
|
||||||
|
<button type="submit">Search!</button>
|
||||||
|
</form>
|
||||||
|
</header>
|
||||||
|
{{if ne .Templates.Search.List ""}}
|
||||||
|
{{if ne .Templates.Search.Content.Total 0}}
|
||||||
|
<h1>Results by request '{{.QueryRaw}}': {{.Templates.Search.Content.Total}}</h1>
|
||||||
|
{{end}}
|
||||||
|
{{.Templates.Search.List}}
|
||||||
|
{{else}}
|
||||||
|
<p>No results :(</p>
|
||||||
|
{{end}}
|
||||||
|
</main>
|
||||||
|
</html>
|
@ -1,15 +1,15 @@
|
|||||||
{
|
{
|
||||||
"instances": [
|
"instances": [
|
||||||
{
|
{
|
||||||
"title": "ls.404.mn",
|
"title": "skunky.ebloid.ru",
|
||||||
"country": "Germany",
|
"country": "Russia",
|
||||||
"urls": {
|
"urls": {
|
||||||
"ygg": "http://[201:f137:d1ac:920e:cd42:bfd1:1e83:da1d]/skunkyart",
|
"ygg": "http://[201:eba5:d1fc:bf7b:cfcb:a811:4b8b:7ea3]/art",
|
||||||
"clearnet": "https://ls.404.mn/skunkyart"
|
"clearnet": "https://skunky.ebloid.ru/art"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"proxy": true,
|
"nsfw": false,
|
||||||
"nsfw": false
|
"proxy": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -19,41 +19,41 @@
|
|||||||
"clearnet": "https://skunky.clovius.club"
|
"clearnet": "https://skunky.clovius.club"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"proxy": true,
|
"nsfw": true,
|
||||||
"nsfw": true
|
"proxy": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "bloat.cat",
|
"title": "bloat.cat",
|
||||||
"country": "Germany",
|
"country": "Romania",
|
||||||
"urls": {
|
"urls": {
|
||||||
"clearnet": "https://skunky.bloat.cat"
|
"clearnet": "https://skunky.bloat.cat"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"proxy": true,
|
"nsfw": true,
|
||||||
"nsfw": true
|
"proxy": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "frontendfriendly.xyz",
|
||||||
|
"country": "Finland",
|
||||||
|
"urls": {
|
||||||
|
"clearnet": "https://skunkyart.frontendfriendly.xyz"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"nsfw": true,
|
||||||
|
"proxy": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "lumaeris.com",
|
"title": "lumaeris.com",
|
||||||
"country": "Germany",
|
"country": "US",
|
||||||
"urls": {
|
"urls": {
|
||||||
"clearnet": "https://skunkyart.lumaeris.com"
|
"clearnet": "https://skunkyart.lumaeris.com"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"proxy": true,
|
"nsfw": true,
|
||||||
"nsfw": true
|
"proxy": true
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "art.bloat.cat",
|
|
||||||
"country": "Germany",
|
|
||||||
"urls": {
|
|
||||||
"clearnet": "https://art.bloat.cat"
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"proxy": true,
|
|
||||||
"nsfw": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
5
main.go
5
main.go
@ -2,20 +2,17 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"skunkyart/app"
|
"skunkyart/app"
|
||||||
"skunkyart/static"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.macaw.me/skunky/devianter"
|
"git.macaw.me/skunky/devianter"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
app.Release.Version = "1.3.2"
|
|
||||||
app.Release.Description = "Two API endpoints and template embedding into binary"
|
|
||||||
go app.RefreshInstances()
|
go app.RefreshInstances()
|
||||||
|
|
||||||
app.ExecuteCommandLineArguments()
|
app.ExecuteCommandLineArguments()
|
||||||
app.ExecuteConfig()
|
app.ExecuteConfig()
|
||||||
static.CopyTemplatesToMemory()
|
app.CopyTemplatesToMemory()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
|
Before Width: | Height: | Size: 398 KiB After Width: | Height: | Size: 398 KiB |
@ -1,13 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
{{template "head" . }}
|
|
||||||
<main>
|
|
||||||
{{template "header" . }}
|
|
||||||
{{if ne .Templates.DDStrips ""}}
|
|
||||||
<h2 id="strips"><a href="#strips">#</a> Strips</h2>
|
|
||||||
{{.Templates.DDStrips}}
|
|
||||||
{{end}}
|
|
||||||
<h2 id="content"><a href="#content">#</a> Content</h2>
|
|
||||||
{{.Templates.SomeList}}
|
|
||||||
</main>
|
|
||||||
</html>
|
|
@ -1,26 +0,0 @@
|
|||||||
{{define "head"}}
|
|
||||||
<head>
|
|
||||||
<title>SkunkyArt |
|
|
||||||
{{if eq .Endpoint "search"}}
|
|
||||||
"{{.QueryRaw}}"
|
|
||||||
{{else if eq .Endpoint "post"}}
|
|
||||||
{{.Templates.Deviation.Post.Deviation.Author.Username}} — {{.Templates.Deviation.Post.Deviation.Title}}
|
|
||||||
{{else if eq .Endpoint "group_user"}}
|
|
||||||
{{if eq .Type 'g'}}
|
|
||||||
gallery of
|
|
||||||
{{else if eq .Type 'f'}}
|
|
||||||
favourites of
|
|
||||||
{{end}}
|
|
||||||
{{.Templates.GroupUser.GR.Owner.Username}}
|
|
||||||
{{else}}
|
|
||||||
{{.Endpoint}}
|
|
||||||
{{end}}
|
|
||||||
</title>
|
|
||||||
|
|
||||||
<base href="{{.BasePath}}">
|
|
||||||
<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}}
|
|
@ -1,14 +0,0 @@
|
|||||||
{{define "header"}}
|
|
||||||
<header>
|
|
||||||
<h1><a href="">HOME</a> | <a href="dd">DD</a> {{if eq .Endpoint "dd"}}| <a href="{{.Endpoint}}?atom=true">RSS</a>{{end}}</h1>
|
|
||||||
<form method="get" action="search">
|
|
||||||
<input type="text" name="q" placeholder="Search for ..." autocomplete="off" autocapitalize="none" spellcheck="false" value="{{.QueryRaw}}">
|
|
||||||
<select name="type">
|
|
||||||
<option value="all">All</option>
|
|
||||||
<option value="tag">Tag</option>
|
|
||||||
<option value="r">Groups</option>
|
|
||||||
</select>
|
|
||||||
<button type="submit">Search!</button>
|
|
||||||
</form>
|
|
||||||
</header>
|
|
||||||
{{end}}
|
|
@ -1,90 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<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>
|
|
||||||
<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">
|
|
||||||
<option value="all">All</option>
|
|
||||||
<option value="tag">Tag</option>
|
|
||||||
<option value="r">Groups</option>
|
|
||||||
</select>
|
|
||||||
<button type="submit">Search!</button>
|
|
||||||
</form>
|
|
||||||
<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>
|
|
@ -1,16 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
{{template "head" . }}
|
|
||||||
<main>
|
|
||||||
{{template "header" . }}
|
|
||||||
|
|
||||||
{{if ne .Templates.Search.List ""}}
|
|
||||||
{{if ne .Templates.Search.Content.Total 0}}
|
|
||||||
<h1>Results by request '{{.QueryRaw}}': {{.Templates.Search.Content.Total}}</h1>
|
|
||||||
{{end}}
|
|
||||||
{{.Templates.Search.List}}
|
|
||||||
{{else}}
|
|
||||||
<p>No results :(</p>
|
|
||||||
{{end}}
|
|
||||||
</main>
|
|
||||||
</html>
|
|
@ -1,148 +0,0 @@
|
|||||||
//go:build !embed
|
|
||||||
// +build !embed
|
|
||||||
|
|
||||||
package static
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"io/fs"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
var Templates FS
|
|
||||||
|
|
||||||
type file struct {
|
|
||||||
path string
|
|
||||||
name string
|
|
||||||
content []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
var templateNames = []string{}
|
|
||||||
var templates = make(map[string][]file)
|
|
||||||
var StaticPath string
|
|
||||||
|
|
||||||
func CopyTemplatesToMemory() {
|
|
||||||
baseDir, err := os.ReadDir(StaticPath)
|
|
||||||
try(err)
|
|
||||||
|
|
||||||
for _, c := range baseDir {
|
|
||||||
if c.IsDir() {
|
|
||||||
templateNames = append(templateNames, c.Name())
|
|
||||||
|
|
||||||
var filePath strings.Builder
|
|
||||||
filePath.WriteString(StaticPath)
|
|
||||||
filePath.WriteString("/")
|
|
||||||
filePath.WriteString(c.Name())
|
|
||||||
|
|
||||||
dir, err := os.ReadDir(filePath.String())
|
|
||||||
try(err)
|
|
||||||
|
|
||||||
filePath.WriteString("/")
|
|
||||||
for _, cd := range dir {
|
|
||||||
f, err := os.ReadFile(filePath.String() + cd.Name())
|
|
||||||
try(err)
|
|
||||||
templates[c.Name()] = append(templates[c.Name()], file{
|
|
||||||
content: f,
|
|
||||||
name: cd.Name(),
|
|
||||||
path: c.Name() + "/" + cd.Name(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type FS struct{}
|
|
||||||
|
|
||||||
func (FS) Open(name string) (fs.File, error) {
|
|
||||||
for i, l := 0, len(templateNames); i < l; i++ {
|
|
||||||
for _, x := range templates[templateNames[i]] {
|
|
||||||
if x.content != nil && name == x.path {
|
|
||||||
return &File{
|
|
||||||
name: x.path,
|
|
||||||
content: bytes.NewBuffer(x.content),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, &fs.PathError{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (FS) Glob(pattern string) ([]string, error) {
|
|
||||||
trimmed := strings.Split(pattern, "/")
|
|
||||||
var matches = []string{}
|
|
||||||
for x, s := range templates {
|
|
||||||
for i, l := 0, len(s); i < l && trimmed[0] == x; i++ {
|
|
||||||
s := s[i]
|
|
||||||
matches = append(matches, s.path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(matches) != 0 {
|
|
||||||
return matches, nil
|
|
||||||
}
|
|
||||||
return nil, &fs.PathError{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func try(err error) {
|
|
||||||
if err != nil {
|
|
||||||
println(err.Error())
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* сделано на основе https://github.com/psanford/memfs; требуется для корректной работы templates.ParseFS */
|
|
||||||
type fileInfo struct {
|
|
||||||
name string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fi fileInfo) Name() string {
|
|
||||||
return fi.name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fi fileInfo) Size() int64 {
|
|
||||||
return 4096
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fileInfo) Mode() fs.FileMode {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fileInfo) ModTime() time.Time {
|
|
||||||
return time.Time{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fileInfo) IsDir() bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fileInfo) Sys() interface{} {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type File struct {
|
|
||||||
name string
|
|
||||||
content *bytes.Buffer
|
|
||||||
closed bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *File) Stat() (fs.FileInfo, error) {
|
|
||||||
return fileInfo{
|
|
||||||
name: f.name,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *File) Read(b []byte) (int, error) {
|
|
||||||
if f.closed {
|
|
||||||
return 0, fs.ErrClosed
|
|
||||||
}
|
|
||||||
return f.content.Read(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *File) Close() error {
|
|
||||||
if f.closed {
|
|
||||||
return fs.ErrClosed
|
|
||||||
}
|
|
||||||
f.closed = true
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
//go:build embed
|
|
||||||
// +build embed
|
|
||||||
|
|
||||||
package static
|
|
||||||
|
|
||||||
import "embed"
|
|
||||||
|
|
||||||
//go:embed *
|
|
||||||
var Templates embed.FS
|
|
||||||
var Enabled bool = true
|
|
||||||
|
|
||||||
var StaticPath string
|
|
||||||
|
|
||||||
func CopyTemplatesToMemory() {
|
|
||||||
_ = StaticPath
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user