mirror of
https://git.macaw.me/skunky/SkunkyArt.git
synced 2025-04-29 12:35:06 +03:00
Compare commits
No commits in common. "master" and "v1.3" have entirely different histories.
45 changed files with 804 additions and 1791 deletions
|
@ -1,6 +0,0 @@
|
||||||
cache
|
|
||||||
compose.yaml
|
|
||||||
*.json
|
|
||||||
LICENSE
|
|
||||||
*.md
|
|
||||||
services
|
|
2
.gitignore
vendored
Executable file → Normal file
2
.gitignore
vendored
Executable file → Normal file
|
@ -1,5 +1,3 @@
|
||||||
**/cache
|
**/cache
|
||||||
**/compose.yaml
|
|
||||||
**/config.json
|
**/config.json
|
||||||
**/skunkyart
|
**/skunkyart
|
||||||
**/skunkyart-*
|
|
||||||
|
|
23
Dockerfile
23
Dockerfile
|
@ -1,23 +0,0 @@
|
||||||
ARG GO_VERSION=1.18
|
|
||||||
|
|
||||||
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION} AS build
|
|
||||||
ARG TARGETOS
|
|
||||||
ARG TARGETARCH
|
|
||||||
|
|
||||||
WORKDIR /build
|
|
||||||
COPY . .
|
|
||||||
RUN CGO_ENABLED=0 GOARCH=${TARGETARCH} GOOS=${TARGETOS} go build -ldflags "-s -w -extldflags '-static'" && \
|
|
||||||
echo "skunkyart:x:10000:10000:SkunkyArt user:/:/sbin/nologin" > /etc/minimal-passwd && \
|
|
||||||
echo "skunkyart:x:10000:" > /etc/minimal-group
|
|
||||||
|
|
||||||
FROM scratch
|
|
||||||
|
|
||||||
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
|
||||||
COPY --from=build /build/static /static
|
|
||||||
COPY --from=build /build/skunkyart /skunkyart
|
|
||||||
COPY --from=build /etc/minimal-passwd /etc/passwd
|
|
||||||
COPY --from=build /etc/minimal-group /etc/group
|
|
||||||
|
|
||||||
USER skunkyart
|
|
||||||
|
|
||||||
ENTRYPOINT ["/skunkyart"]
|
|
11
INSTANCES.md
11
INSTANCES.md
|
@ -1,11 +0,0 @@
|
||||||
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|
|
|
||||||
|:------:|:-------:|:-:|:-:|:--:|:--------:|:--------------:|:-----:|
|
|
||||||
|[lost-skunk.cc](https://lost-skunk.cc/skunkyart)|[Yes](http://[201:f137:d1ac:920e:cd42:bfd1:1e83:da1d]/skunkyart)|No|No| No | Yes | No | Finland |
|
|
||||||
|[orehus.club](https://sa.orehus.club)|No|No|No| Yes | No | No | Germany |
|
|
||||||
|[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 |
|
|
||||||
|[art.bloat.cat](https://art.bloat.cat)|No|No|No| Yes | Yes | No | Germany |
|
|
||||||
|[dc09.ru](https://sa.dc09.ru)|No|No|No| No | Yes | No | Russia |
|
|
||||||
|[opnxng.com](https://da.opnxng.com)|No|No|No| Yes | Yes | No | Singapore |
|
|
0
LICENSE
Executable file → Normal file
0
LICENSE
Executable file → Normal file
105
README.md
Executable file → Normal file
105
README.md
Executable file → Normal file
|
@ -1,51 +1,78 @@
|
||||||
> [!NOTE]
|
[](https://go.kde.org/matrix/#/#skunkyart:ebloid.ru)
|
||||||
> Currently, due to school, I cannot actively develop this project :(
|
# Instances
|
||||||
> 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.
|
|Инстанс|Yggdrasil|I2P|Tor|NSFW|Proxifying|Country|
|
||||||
|
|:-----:|:-------:|:-:|:-:|:--:|:--------:|:-----:|
|
||||||
<img src="static/images/logo.png" alt="SkunkyArt" title="SkunkyArt Logo" width="20%" loading="lazy"/>
|
|[skunky.ebloid.ru](https://skunky.ebloid.ru/art)|[Yes](http://[201:eba5:d1fc:bf7b:cfcb:a811:4b8b:7ea3]/art)|No|No| No | No | Russia |
|
||||||
|
|[clovius.club](https://skunky.clovius.club)|No|No|No| Yes | Yes | Sweden |
|
||||||
[](https://go.kde.org/matrix/#/#skunkyart:gnulinux.club)
|
|[bloat.cat](https://skunky.bloat.cat)|No|No|No| Yes | Yes | Romania |
|
||||||
|
|[frontendfriendly.xyz](https://skunkyart.frontendfriendly.xyz)|No|No|No| Yes | Yes | Finland |
|
||||||
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 to DeviantArt, which will work without problems even on quite old hardware, due to the lack of JavaScript.
|
||||||
## Build (translated via DeepL)
|
## Config
|
||||||
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:
|
The sample config is in the `config.example.json` file. To specify your own path to the config, use the CLI argument `-c` or `--config`.
|
||||||
|
* `listen` -- the address and port on which SkunkyArt will listen
|
||||||
|
* `base-path` -- the path to the instance. Example: "`base-path`:"/art/" -> https://skunky.ebloid.ru/art/
|
||||||
|
* `cache` -- caching system; default is off.
|
||||||
|
* * `path` -- the path to the cache
|
||||||
|
* * `lifetime` -- cache file lifetime; measured in Unix milliseconds.
|
||||||
|
* * `max-size` -- maximum file size in bytes.
|
||||||
|
* `dirs-to-memory` -- this setting determines which directories will be copied to RAM when SkunkyArt is started. Required
|
||||||
|
* `download-proxy` -- proxy address for downloading files.
|
||||||
|
## Examples of reverse proxies
|
||||||
|
Nginx:
|
||||||
|
```apache
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name skunky.example.com;
|
||||||
|
|
||||||
`go build -tags embed -ldflags "-w -s"`
|
location ((BASE URL)) { # if you have a separate subdomain for the frontend, insert '/' instead of '((BASE URL))'.
|
||||||
|
proxy_set_header Scheme $scheme;
|
||||||
Pre-compiled binaries can be found in the [Releases](https://git.macaw.me/skunky/skunkyart/releases) tab.
|
proxy_set_header Host $host;
|
||||||
## Setup
|
proxy_http_version 1.1;
|
||||||
The sample config is in the `config.example.json` file. For custom config, use `--config` option.
|
proxy_pass http://((IP)):((PORT));
|
||||||
See the [`SETUP.md`](/skunky/SkunkyArt/src/branch/master/SETUP.md) file for more info about directives.
|
}
|
||||||
## 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:
|
```
|
||||||
1. the Instance must not use Cloudflare.
|
## How do I add my instance to the list?
|
||||||
|
To do this, you must either make a PR by adding your instance to the `instances.json` file, or report it to the room in Matrix. I don't think it needs any description. However, be aware, this list has a couple rules:
|
||||||
|
1. the instance must not use Cloudflare.
|
||||||
2. If your instance has modified source code, you need to publish it to any free platform. For example, Github and Gitlab are not.
|
2. If your instance has modified source code, you need to publish it to any free platform. For example, Github and Gitlab are not.
|
||||||
## Acknowledgements
|
## Acknowledgements
|
||||||
* [vlnst](https://git.bloat.cat/vlnst) — wrote a Docker file.
|
* [Лис⚛](https://go.kde.org/matrix/#/@fox:matrix.org) -- helped me understand Go and gave me a lot of useful advice on this language.
|
||||||
* [Лис⚛](https://go.kde.org/matrix/#/@fox:matrix.org) — helped me understand Go and gave me a lot of useful advice on this language.
|
|
||||||
* [meoww](https://codeberg.org/meoww) — translated some sentences into English and wrote a service for openrc
|
|
||||||
|
|
||||||
# RU 🇷🇺
|
# RU 🇷🇺
|
||||||
## Описание
|
## Описание
|
||||||
SkunkyArt 🦨 — альтернативный фронтенд к DeviantArt, который полностью работает без JS (JavaScript).
|
SkunkyArt 🦨 -- альтернативный фронтенд к DeviantArt, который будет работать без проблем даже на довольно старом оборудовании, за счёт отсутствия JavaScript.
|
||||||
## Сборка
|
## Конфиг
|
||||||
Рекомендуется производить сборку с тегом 'embed', поскольку он встраивает заготовки в бинарный файл. Если вы планируете изменять заготовки, то не используйте этот тег. Также вы можете добавить аргумент `-ldflags "-w -s"` (у GCCGO он называется по-другому — `gccgoflags`) для уменьшения размера выходного файла. Вот пример:
|
Пример конфига находится в файле `config.example.json`. Чтобы указать свой путь до конфига, используйте CLI-аргумент `-c` или `--config`.
|
||||||
|
* `listen` -- адрес и порт, на котором будет слушать SkunkyArt
|
||||||
|
* `base-path` -- путь к инстансу. Пример: "base-path": "/art/" -> https://skunky.ebloid.ru/art/
|
||||||
|
* `cache` -- система кеширования; по умолчанию - выкл.
|
||||||
|
* * `path` -- путь до кеша
|
||||||
|
* * `lifetime` -- время жизни файла в кеше; измеряется в Unix-миллисекундах
|
||||||
|
* * `max-size` -- максимальный размер файла в байтах
|
||||||
|
* `dirs-to-memory` -- данная настройка определяет какие каталоги будут скопированы в ОЗУ при запуске SkunkyArt. Обязательна
|
||||||
|
* `download-proxy` -- адрес прокси для загрузки файлов
|
||||||
|
## Примеры reverse-прокси
|
||||||
|
Nginx:
|
||||||
|
```apache
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name skunky.example.com;
|
||||||
|
|
||||||
`go build -tags embed -ldflags "-w -s"`
|
location ((BASE URL)) { # если у вас отдельный поддомен для фронтенда, вместо '((BASE URL))' вставляйте '/'
|
||||||
|
proxy_set_header Scheme $scheme;
|
||||||
Готовые бинари находятся во вкладке [Releases](https://git.macaw.me/skunky/skunkyart/releases).
|
proxy_set_header Host $host;
|
||||||
## Настройка
|
proxy_http_version 1.1;
|
||||||
Пример конфига находится в файле `config.example.json`. Чтобы указать свой конфиг, используйте cli-аргумент `--config`.
|
proxy_pass http://((IP)):((PORT));
|
||||||
См. [`SETUP-RU.md`](/skunky/SkunkyArt/src/branch/master/SETUP-RU.md) для информации о настройки фронтенда.
|
}
|
||||||
## Добавление инстанса в список
|
}
|
||||||
Чтобы это сделать, вы должны либо сделать PR, добавив в файлы `instances.json` и `INSTANCES.md` свой инстанс (можете воспользоваться cli-аргументом `--add-instance`, который автоматически это сделает), либо создать Issue, или сообщить о нём в комнате в Matrix. Учтите, что ваш инстанс должен соблюсти следущие правила:
|
```
|
||||||
1. Инстанс не должен использовать Cloudflare итп.
|
## Как добавить свой инстанс в список?
|
||||||
|
Чтобы это сделать, вы должны либо сделать PR, добавив в файл `instances.json` свой инстанс, либо сообщить о нём в комнате в Matrix. Думаю, он не нуждается в описании. Однако учтите, у этого списка есть пара правил:
|
||||||
|
1. Инстанс не должен использовать Cloudflare.
|
||||||
2. Если ваш инстанс имеет модифицированный исходный код, то вам нужно опубликовать его на любую свободную площадку. Например, Github и Gitlab таковыми не являются.
|
2. Если ваш инстанс имеет модифицированный исходный код, то вам нужно опубликовать его на любую свободную площадку. Например, Github и Gitlab таковыми не являются.
|
||||||
## Благодарности
|
## Благодарности
|
||||||
* [vlnst](https://git.bloat.cat/vlnst) — написал Docker-файл.
|
* [Лис⚛](https://go.kde.org/matrix/#/@fox:matrix.org) -- помог разобраться в Go и много чего полезного посоветовал по этому языку.
|
||||||
* [Лис⚛](https://go.kde.org/matrix/#/@fox:matrix.org) — помог разобраться в Go и много чего полезного посоветовал по этому языку.
|
|
||||||
* [meoww](https://codeberg.org/meoww) — перевела некоторые предложения на английский язык и написала сервис для openrc
|
|
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`
|
|
42
SETUP-RU.md
42
SETUP-RU.md
|
@ -1,42 +0,0 @@
|
||||||
[English version 🇬🇧](/skunky/SkunkyArt/src/branch/master/SETUP.md)
|
|
||||||
|
|
||||||
# Единицы измерения
|
|
||||||
Размер файла в кеше измеряется в мегабайтах.<br>
|
|
||||||
Единицы времени:
|
|
||||||
* `i` — минуты
|
|
||||||
* `h` — часы
|
|
||||||
* `w` — недели
|
|
||||||
* `m` — месяца
|
|
||||||
* `y` — года
|
|
||||||
|
|
||||||
# Конфигурация
|
|
||||||
* `listen` — IP и порт для слушанья; заполняется по такой форме: ip:port
|
|
||||||
* `uri` — URI инстанса. Пример: `"uri":"/art/"` -> https://skunky.ebloid.ru/art/
|
|
||||||
* `cache` — Система кеширования; по умолчанию выключена
|
|
||||||
* `enabled` — Состояние системы кеширования; требуется булёвое значение
|
|
||||||
* `path` — Полный путь до каталога, куда будет сохраняться кеш
|
|
||||||
* `lifetime` — Время жизни файла в кеше, требует целочисленное значение, дополненное суффиксом времени (см. 'Единицы времени')
|
|
||||||
* `max-size` — Максимальный размер файла
|
|
||||||
* `update-interval` — Интервал для автоматической ротации кеша
|
|
||||||
* `static-path` — Строка, являющаяся путём до статики. SkunkyArt при запуске скопирует содержимое этого каталога в ОЗУ. Однако, если вы собрали фронтенд с тегом 'embed', то этого не произайдёт
|
|
||||||
* `download-proxy` — Адрес прокси для загрузки файлов
|
|
||||||
* `user-agent` — Строка, которая используется в качестве User-Agent'а
|
|
||||||
|
|
||||||
# Настройка обратного прокси
|
|
||||||
Если вы собираетесь хостить инстанс в Интернете, то вам следует настроить заголовок прокси [`X-Forwarded-Proto`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto). В противном случае, все ссылки на вашем инстансе будут вида "http". Ниже есть информация о том, как настроить обратное проксирование:
|
|
||||||
|
|
||||||
Nginx:
|
|
||||||
```apache
|
|
||||||
server {
|
|
||||||
listen 443 ssl;
|
|
||||||
server_name skunky.example.com;
|
|
||||||
|
|
||||||
# Если используется поддомен, то вместо ((BASE_URL)), укажите '/'.
|
|
||||||
location ((BASE_URL)) {
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_pass http://((IP)):((PORT));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
42
SETUP.md
42
SETUP.md
|
@ -1,42 +0,0 @@
|
||||||
[Версия на русском языке 🇷🇺](/skunky/SkunkyArt/src/branch/master/SETUP-RU.md)
|
|
||||||
|
|
||||||
# Units
|
|
||||||
Maximum file size in megabytes, requires numeric value.<br>
|
|
||||||
Time units:
|
|
||||||
* `i` — minutes
|
|
||||||
* `h` — hours
|
|
||||||
* `w` — weeks
|
|
||||||
* `m` — months
|
|
||||||
* `y` — years
|
|
||||||
|
|
||||||
# Config
|
|
||||||
* `listen` — IP and port to listen on in the following form: ip:port
|
|
||||||
* `uri` — Instance URI. Example: `"uri":"/art/"` -> https://skunky.ebloid.ru/art/
|
|
||||||
* `cache` — Caching system; default is off.
|
|
||||||
* `enabled` — Caching system state, requires boolean value
|
|
||||||
* `path` — Path to cache directory, requires absolute filesystem path
|
|
||||||
* `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
|
|
||||||
* `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
|
|
||||||
|
|
||||||
# Setting up reverse proxy
|
|
||||||
Pretty much business as usual, except for the [`X-Forwarded-Proto`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto) header setting.
|
|
||||||
|
|
||||||
Nginx example configuration:
|
|
||||||
```apache
|
|
||||||
server {
|
|
||||||
listen 443 ssl;
|
|
||||||
server_name skunky.example.com;
|
|
||||||
|
|
||||||
# In case of subdomain, use / instend of ((BASE_URL))
|
|
||||||
location ((BASE_URL)) {
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_pass http://((IP)):((PORT));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
22
TODO.md
Executable file → Normal file
22
TODO.md
Executable file → Normal file
|
@ -1,23 +1,7 @@
|
||||||
# v1.3.x
|
# v1.3.x
|
||||||
* Почистить говнокод
|
* Доделать парсинг описания
|
||||||
* Добавить фильтры поиска
|
* Реализовать миниатюры и оптимизировать CSS под маленькие экраны
|
||||||
* ~~Сделать порт под FreeBSD~~ ✔️
|
|
||||||
* **Доделать парсинг описания**
|
|
||||||
* ~~Реализовать стрипы в ежедневных артах~~ ✔️
|
|
||||||
* ~~Исправить баг с навигацией по страницам~~ ✔️
|
|
||||||
* ~~Сделать нормальное отображение ошибок~~ ✔️
|
|
||||||
* ~~Сделать единицы в конфиге более понятными~~ ✔️
|
|
||||||
* Добавить чекер инстанса на работоспособность
|
|
||||||
* ~~Добавить просмотр понравившихся артов пользователю~~ ✔️
|
|
||||||
* Добавить возможность включить темплейты в бинарник [P]
|
|
||||||
* ~~Реализовать миниатюры и оптимизировать CSS под маленькие экраны~~ ✔️
|
|
||||||
* Написать Makefile и скрипт для автоматического развёртывания инстанса
|
|
||||||
* Исправить баг с эмоджи, когда некоторые кастомные эмоции могут не отображаться
|
|
||||||
* ~~Добавить аргумент &filename, который будет выдавать файл с нормально выглядещем именем~~ ✔️
|
|
||||||
* ~~Улучшить систему кеширования: добавить рейтинг для удаления и копирование изображений в ОЗУ~~ ✔️
|
|
||||||
# v1.4
|
# v1.4
|
||||||
* Реализовать API
|
|
||||||
* Реализовать темы
|
* Реализовать темы
|
||||||
* Перейти на арены в кеше
|
|
||||||
* Реализовать многоязычный интерфейс
|
* Реализовать многоязычный интерфейс
|
||||||
|
* Реализовать API
|
82
app/api.go
82
app/api.go
|
@ -1,82 +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 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if CFG.Proxy {
|
|
||||||
mediaUrl = mediaUrl[21:]
|
|
||||||
dot := strings.Index(mediaUrl, ".")
|
|
||||||
a.main.Writer.Header().Del("Content-Type")
|
|
||||||
a.main.DownloadAndSendMedia(mediaUrl[:dot], mediaUrl[dot+11:])
|
|
||||||
} else {
|
|
||||||
a.main.Writer.Header().Add("Location", mediaUrl)
|
|
||||||
a.main.Writer.WriteHeader(302)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
152
app/cache.go
152
app/cache.go
|
@ -1,152 +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[:])
|
|
||||||
|
|
||||||
c := func() {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if CFG.Cache.MemCache {
|
|
||||||
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 {
|
|
||||||
c()
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
c()
|
|
||||||
}
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
|
|
||||||
var total int64
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
total += fileInfo.Size()
|
|
||||||
// if c.MaxSize != 0 && fileInfo.Size() > c.MaxSize {
|
|
||||||
// try(os.RemoveAll(fileName))
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.MaxSize != 0 && total > c.MaxSize {
|
|
||||||
try(os.RemoveAll(c.Path))
|
|
||||||
os.Mkdir(c.Path, 0700)
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(time.Second * time.Duration(c.UpdateInterval))
|
|
||||||
}
|
|
||||||
}
|
|
166
app/cli.go
166
app/cli.go
|
@ -1,166 +0,0 @@
|
||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"html/template"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ExecuteCommandLineArguments() {
|
|
||||||
var helpmsg = `SkunkyArt v{{.Version}} [{{.Description}}]
|
|
||||||
Usage:
|
|
||||||
- [-c|--config] | path to config
|
|
||||||
- [-a|--add-instance] | generates 'instances.json' and 'INSTANCES.md' files with ur instance
|
|
||||||
- [-h|--help] | returns this message
|
|
||||||
Example:
|
|
||||||
./skunkyart -c config.json
|
|
||||||
Copyright lost+skunk, X11. https://git.macaw.me/skunky/skunkyart/src/tag/v{{.Version}}`
|
|
||||||
|
|
||||||
a := os.Args[1:]
|
|
||||||
for n, x := range a {
|
|
||||||
switch x {
|
|
||||||
case "-c", "--config":
|
|
||||||
if len(a) >= 2 {
|
|
||||||
CFG.cfg = a[n+1]
|
|
||||||
} else {
|
|
||||||
exit("Not enought arguments", 1)
|
|
||||||
}
|
|
||||||
case "-h", "--help":
|
|
||||||
var buf bytes.Buffer
|
|
||||||
t := template.New("help")
|
|
||||||
t.Parse(helpmsg)
|
|
||||||
t.Execute(&buf, &Release)
|
|
||||||
exit(buf.String(), 0)
|
|
||||||
case "-a", "--add-instance":
|
|
||||||
addInstance()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type settingsUrls struct {
|
|
||||||
I2P string `json:"i2p,omitempty"`
|
|
||||||
Ygg string `json:"ygg,omitempty"`
|
|
||||||
Tor string `json:"tor,omitempty"`
|
|
||||||
Clearnet string `json:"clearnet,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type settingsParams struct {
|
|
||||||
Nsfw bool `json:"nsfw"`
|
|
||||||
Proxy bool `json:"proxy"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type settings struct {
|
|
||||||
Title string `json:"title"`
|
|
||||||
Country string `json:"country"`
|
|
||||||
ModifiedSrc string `json:"modified-src,omitempty"`
|
|
||||||
Urls settingsUrls `json:"urls"`
|
|
||||||
Settings settingsParams `json:"settings"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func addInstance() {
|
|
||||||
prompt := func(txt string, necessary bool) string {
|
|
||||||
input := bufio.NewScanner(os.Stdin)
|
|
||||||
for {
|
|
||||||
print(txt)
|
|
||||||
print(": ")
|
|
||||||
input.Scan()
|
|
||||||
|
|
||||||
if i := input.Text(); necessary && i == "" {
|
|
||||||
println("Please specify the", txt)
|
|
||||||
} else {
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var settingsVar struct {
|
|
||||||
Instances []settings `json:"instances"`
|
|
||||||
}
|
|
||||||
instancesJson, err := os.OpenFile("instances.json", os.O_CREATE|os.O_WRONLY, 0644)
|
|
||||||
try(err)
|
|
||||||
defer instancesJson.Close()
|
|
||||||
|
|
||||||
instancesFile, err := os.OpenFile("INSTANCES.md", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
|
||||||
try(err)
|
|
||||||
defer instancesFile.Close()
|
|
||||||
|
|
||||||
for {
|
|
||||||
if string(instances) == "" {
|
|
||||||
print("\rDownloading instance list...")
|
|
||||||
} else {
|
|
||||||
println("\r\033[2KDownloaded!")
|
|
||||||
try(json.Unmarshal(instances, &settingsVar))
|
|
||||||
|
|
||||||
settingsVar.Instances = append(settingsVar.Instances, settings{
|
|
||||||
Title: prompt("Title", true),
|
|
||||||
Country: prompt("Country", true),
|
|
||||||
ModifiedSrc: prompt("Link to modified sources", false),
|
|
||||||
Settings: settingsParams{
|
|
||||||
Nsfw: CFG.Nsfw,
|
|
||||||
Proxy: CFG.Proxy,
|
|
||||||
},
|
|
||||||
Urls: settingsUrls{
|
|
||||||
Clearnet: prompt("Clearnet link", false),
|
|
||||||
Ygg: prompt("Yggdrasil link", false),
|
|
||||||
Tor: prompt("Onion link", false),
|
|
||||||
I2P: prompt("I2P link", false),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
j, err := json.MarshalIndent(&settingsVar, "", " ")
|
|
||||||
try(err)
|
|
||||||
|
|
||||||
instancesJson.Write(j)
|
|
||||||
|
|
||||||
settingsVar := &settingsVar.Instances[len(settingsVar.Instances)-1]
|
|
||||||
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|")
|
|
||||||
mdbuilder(settingsVar.Urls.Clearnet != "", settingsVar.Urls.Clearnet, settingsVar.Title)
|
|
||||||
|
|
||||||
urls := []string{settingsVar.Urls.Ygg, settingsVar.Urls.I2P, settingsVar.Urls.Tor}
|
|
||||||
for i, l := 0, len(urls); i < l; i++ {
|
|
||||||
url := urls[i]
|
|
||||||
mdbuilder(url != "", url, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
settings := []bool{settingsVar.Settings.Nsfw, settingsVar.Settings.Proxy}
|
|
||||||
for i, l := 0, len(settings); i < l; i++ {
|
|
||||||
mdbuilder(settings[i], "", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
mdbuilder(settingsVar.ModifiedSrc != "", settingsVar.ModifiedSrc, "")
|
|
||||||
|
|
||||||
mdstr.WriteString(settingsVar.Country)
|
|
||||||
mdstr.WriteString("|")
|
|
||||||
|
|
||||||
instancesFile.Write(mdstr.Bytes())
|
|
||||||
break
|
|
||||||
}
|
|
||||||
time.Sleep(500 * time.Millisecond)
|
|
||||||
}
|
|
||||||
exit("Done! Now add the files 'instances.json' and 'INSTANCES.md' to the 'master' branch in the repository https://git.macaw.me/skunky/SkunkyArt", 0)
|
|
||||||
}
|
|
112
app/config.go
Executable file → Normal file
112
app/config.go
Executable file → Normal file
|
@ -3,101 +3,87 @@ package app
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
|
||||||
"skunkyart/static"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.macaw.me/skunky/devianter"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var Release struct {
|
|
||||||
Version string
|
|
||||||
Description string
|
|
||||||
}
|
|
||||||
|
|
||||||
type cache_config struct {
|
type cache_config struct {
|
||||||
Enabled bool
|
Enabled bool
|
||||||
MemCache bool `json:"memcache"`
|
|
||||||
Path string
|
Path string
|
||||||
MaxSize int64 `json:"max-size"`
|
MaxSize int64 `json:"max-size"`
|
||||||
Lifetime string
|
Lifetime int64
|
||||||
UpdateInterval int64 `json:"update-interval"`
|
UpdateInterval int64 `json:"update-interval"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type config struct {
|
type config struct {
|
||||||
cfg string
|
cfg string
|
||||||
Listen string
|
Listen string
|
||||||
URI string `json:"uri"`
|
BasePath string `json:"base-path"`
|
||||||
Cache cache_config
|
Cache cache_config
|
||||||
Proxy, Nsfw bool
|
Proxy, Nsfw bool
|
||||||
UserAgent string `json:"user-agent"`
|
DownloadProxy string `json:"download-proxy"`
|
||||||
DownloadProxy string `json:"download-proxy"`
|
Dirs []string `json:"dirs-to-memory"`
|
||||||
StaticPath string `json:"static-path"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var CFG = config{
|
var CFG = config{
|
||||||
cfg: "config.json",
|
cfg: "config.json",
|
||||||
Listen: "127.0.0.1:3003",
|
Listen: "127.0.0.1:3003",
|
||||||
URI: "/",
|
BasePath: "/",
|
||||||
Cache: cache_config{
|
Cache: cache_config{
|
||||||
Enabled: false,
|
Enabled: true,
|
||||||
Path: "cache",
|
Path: "cache",
|
||||||
UpdateInterval: 1,
|
UpdateInterval: 1,
|
||||||
},
|
},
|
||||||
StaticPath: "static",
|
Dirs: []string{"html", "css"},
|
||||||
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
|
|
||||||
|
|
||||||
func ExecuteConfig() {
|
func ExecuteConfig() {
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
recover()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
for {
|
||||||
|
Templates["instances.json"] = string(Download("https://git.macaw.me/skunky/SkunkyArt/raw/branch/master/instances.json").Body)
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
const helpmsg = `SkunkyArt v1.3 [refactoring]
|
||||||
|
Usage:
|
||||||
|
- [-c|--config] - path to config
|
||||||
|
- [-h|--help] - returns this message
|
||||||
|
Example:
|
||||||
|
./skunkyart -c config.json
|
||||||
|
Copyright lost+skunk, X11. https://git.macaw.me/skunky/skunkyart/src/tag/v1.3`
|
||||||
|
|
||||||
|
a := os.Args
|
||||||
|
for n, x := range a {
|
||||||
|
switch x {
|
||||||
|
case "-c", "--config":
|
||||||
|
if len(a) >= 3 {
|
||||||
|
CFG.cfg = a[n+1]
|
||||||
|
} else {
|
||||||
|
exit("Not enought arguments", 1)
|
||||||
|
}
|
||||||
|
case "-h", "--help":
|
||||||
|
exit(helpmsg, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if CFG.cfg != "" {
|
if CFG.cfg != "" {
|
||||||
f, err := os.ReadFile(CFG.cfg)
|
f, err := os.ReadFile(CFG.cfg)
|
||||||
tryWithExitStatus(err, 1)
|
try_with_exitstatus(err, 1)
|
||||||
tryWithExitStatus(json.Unmarshal(f, &CFG), 1)
|
|
||||||
|
try_with_exitstatus(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)
|
||||||
}
|
}
|
||||||
|
|
||||||
if CFG.Cache.Enabled {
|
if CFG.Cache.MaxSize != 0 || CFG.Cache.Lifetime != 0 {
|
||||||
if CFG.Cache.Lifetime != "" {
|
|
||||||
var duration int64
|
|
||||||
day := 24 * time.Hour.Milliseconds()
|
|
||||||
numstr := regexp.MustCompile("[0-9]+").FindAllString(CFG.Cache.Lifetime, -1)
|
|
||||||
num, _ := strconv.Atoi(numstr[len(numstr)-1])
|
|
||||||
|
|
||||||
switch unit := CFG.Cache.Lifetime[len(CFG.Cache.Lifetime)-1:]; unit {
|
|
||||||
case "i":
|
|
||||||
duration = time.Minute.Milliseconds()
|
|
||||||
case "h":
|
|
||||||
duration = time.Hour.Milliseconds()
|
|
||||||
case "d":
|
|
||||||
duration = day
|
|
||||||
case "w":
|
|
||||||
duration = day * 7
|
|
||||||
case "m":
|
|
||||||
duration = day * 30
|
|
||||||
case "y":
|
|
||||||
duration = day * 360
|
|
||||||
default:
|
|
||||||
exit("Invalid unit specified: "+unit, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
lifetimeParsed = duration * int64(num)
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
229
app/parsers.go
Executable file → Normal file
229
app/parsers.go
Executable file → Normal file
|
@ -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] == "" {
|
||||||
|
@ -76,82 +70,17 @@ func (s skunkyart) ParseComments(c devianter.Comments, daError devianter.Error)
|
||||||
return cmmts.String()
|
return cmmts.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s skunkyart) DeviationList(devs []devianter.Deviation, allowAtom bool, content ...DeviationList) string {
|
func (s skunkyart) DeviationList(devs []devianter.Deviation, content ...DeviationList) string {
|
||||||
|
var list strings.Builder
|
||||||
if s.Atom && s.Page > 1 {
|
if s.Atom && s.Page > 1 {
|
||||||
s.ReturnHTTPError(400)
|
s.ReturnHTTPError(400)
|
||||||
return ""
|
return ""
|
||||||
}
|
} else if s.Atom {
|
||||||
|
|
||||||
var list, listContent strings.Builder
|
|
||||||
|
|
||||||
for i, l := 0, len(devs); i < l; i++ {
|
|
||||||
data := &devs[i]
|
|
||||||
if preview, fullview := ParseMedia(data.Media, 320), ParseMedia(data.Media); !(data.NSFW && !CFG.Nsfw) {
|
|
||||||
if allowAtom && s.Atom {
|
|
||||||
s.Writer.Header().Add("Content-type", "application/atom+xml")
|
|
||||||
id := strconv.Itoa(data.ID)
|
|
||||||
listContent.WriteString(`<entry><author><name>`)
|
|
||||||
listContent.WriteString(data.Author.Username)
|
|
||||||
listContent.WriteString(`</name></author><title>`)
|
|
||||||
listContent.WriteString(data.Title)
|
|
||||||
listContent.WriteString(`</title><link rel="alternate" type="text/html" href="`)
|
|
||||||
listContent.WriteString(UrlBuilder("post", data.Author.Username, "atom-"+id))
|
|
||||||
listContent.WriteString(`"/><id>`)
|
|
||||||
listContent.WriteString(id)
|
|
||||||
listContent.WriteString(`</id><published>`)
|
|
||||||
listContent.WriteString(data.PublishedTime.UTC().Format("Mon, 02 Jan 2006 15:04:05 -0700"))
|
|
||||||
listContent.WriteString(`</published>`)
|
|
||||||
listContent.WriteString(`<media:group><media:title>`)
|
|
||||||
listContent.WriteString(data.Title)
|
|
||||||
listContent.WriteString(`</media:title><media:thumbinal url="`)
|
|
||||||
listContent.WriteString(preview)
|
|
||||||
listContent.WriteString(`"/></media:group><content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><a href="`)
|
|
||||||
listContent.WriteString(ConvertDeviantArtUrlToSkunkyArt(data.Url))
|
|
||||||
listContent.WriteString(`"><img src="`)
|
|
||||||
listContent.WriteString(fullview)
|
|
||||||
listContent.WriteString(`"/></a><p>`)
|
|
||||||
listContent.WriteString(ParseDescription(data.TextContent))
|
|
||||||
listContent.WriteString(`</p></div></content></entry>`)
|
|
||||||
} else {
|
|
||||||
listContent.WriteString(`<div class="block">`)
|
|
||||||
if fullview != "" && preview != "" {
|
|
||||||
listContent.WriteString(`<a title="open/download" href="`)
|
|
||||||
listContent.WriteString(fullview)
|
|
||||||
listContent.WriteString(`"><img loading="lazy" src="`)
|
|
||||||
listContent.WriteString(preview)
|
|
||||||
listContent.WriteString(`" width="15%"></a>`)
|
|
||||||
} else {
|
|
||||||
listContent.WriteString(`<h1>[ TEXT ]</h1>`)
|
|
||||||
}
|
|
||||||
listContent.WriteString(`<br><a href="`)
|
|
||||||
listContent.WriteString(ConvertDeviantArtUrlToSkunkyArt(data.Url))
|
|
||||||
listContent.WriteString(`">`)
|
|
||||||
listContent.WriteString(data.Author.Username)
|
|
||||||
listContent.WriteString(" - ")
|
|
||||||
listContent.WriteString(data.Title)
|
|
||||||
|
|
||||||
if data.NSFW {
|
|
||||||
listContent.WriteString(` [<span class="nsfw">NSFW</span>]`)
|
|
||||||
}
|
|
||||||
if data.AI {
|
|
||||||
listContent.WriteString(" [🤖]")
|
|
||||||
}
|
|
||||||
if data.DD {
|
|
||||||
listContent.WriteString(` [<span class="dd">DD</span>]`)
|
|
||||||
}
|
|
||||||
|
|
||||||
listContent.WriteString("</a></div>")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if allowAtom && s.Atom {
|
|
||||||
list.WriteString(`<?xml version="1.0" encoding="UTF-8"?><feed xmlns:media="http://search.yahoo.com/mrss/" xmlns="http://www.w3.org/2005/Atom">`)
|
list.WriteString(`<?xml version="1.0" encoding="UTF-8"?><feed xmlns:media="http://search.yahoo.com/mrss/" xmlns="http://www.w3.org/2005/Atom">`)
|
||||||
|
|
||||||
list.WriteString(`<title>`)
|
list.WriteString(`<title>`)
|
||||||
if s.Type == 0 {
|
if s.Type == 0 {
|
||||||
list.WriteString("Daily Deviations")
|
list.WriteString("Daily Deviations")
|
||||||
} else if s.Type == 'g' && len(devs) != 0 {
|
} else if len(devs) != 0 {
|
||||||
list.WriteString(devs[0].Author.Username)
|
list.WriteString(devs[0].Author.Username)
|
||||||
} else {
|
} else {
|
||||||
list.WriteString("SkunkyArt")
|
list.WriteString("SkunkyArt")
|
||||||
|
@ -161,16 +90,75 @@ func (s skunkyart) DeviationList(devs []devianter.Deviation, allowAtom bool, con
|
||||||
list.WriteString(`<link rel="alternate" href="`)
|
list.WriteString(`<link rel="alternate" href="`)
|
||||||
list.WriteString(Host)
|
list.WriteString(Host)
|
||||||
list.WriteString(`"/>`)
|
list.WriteString(`"/>`)
|
||||||
|
|
||||||
list.WriteString(listContent.String())
|
|
||||||
|
|
||||||
list.WriteString("</feed>")
|
|
||||||
wr(s.Writer, list.String())
|
|
||||||
} else {
|
} else {
|
||||||
list.WriteString(`<div class="content">`)
|
list.WriteString(`<div class="content">`)
|
||||||
|
}
|
||||||
|
for _, data := range devs {
|
||||||
|
if !(data.NSFW && !CFG.Nsfw) {
|
||||||
|
url := ParseMedia(data.Media)
|
||||||
|
if s.Atom {
|
||||||
|
id := strconv.Itoa(data.ID)
|
||||||
|
list.WriteString(`<entry><author><name>`)
|
||||||
|
list.WriteString(data.Author.Username)
|
||||||
|
list.WriteString(`</name></author><title>`)
|
||||||
|
list.WriteString(data.Title)
|
||||||
|
list.WriteString(`</title><link rel="alternate" type="text/html" href="`)
|
||||||
|
list.WriteString(UrlBuilder("post", data.Author.Username, "atom-"+id))
|
||||||
|
list.WriteString(`"/><id>`)
|
||||||
|
list.WriteString(id)
|
||||||
|
list.WriteString(`</id><published>`)
|
||||||
|
list.WriteString(data.PublishedTime.UTC().Format("Mon, 02 Jan 2006 15:04:05 -0700"))
|
||||||
|
list.WriteString(`</published>`)
|
||||||
|
list.WriteString(`<media:group><media:title>`)
|
||||||
|
list.WriteString(data.Title)
|
||||||
|
list.WriteString(`</media:title><media:thumbinal url="`)
|
||||||
|
list.WriteString(url)
|
||||||
|
list.WriteString(`"/></media:group><content type="xhtml"><div xmlns="http://www.w3.org/1999/xhtml"><a href="`)
|
||||||
|
list.WriteString(ConvertDeviantArtUrlToSkunkyArt(data.Url))
|
||||||
|
list.WriteString(`"><img src="`)
|
||||||
|
list.WriteString(url)
|
||||||
|
list.WriteString(`"/></a><p>`)
|
||||||
|
list.WriteString(ParseDescription(data.TextContent))
|
||||||
|
list.WriteString(`</p></div></content></entry>`)
|
||||||
|
} else {
|
||||||
|
list.WriteString(`<div class="block">`)
|
||||||
|
if url != "" {
|
||||||
|
list.WriteString(`<a title="open/download" href="`)
|
||||||
|
list.WriteString(url)
|
||||||
|
list.WriteString(`"><img loading="lazy" src="`)
|
||||||
|
list.WriteString(url)
|
||||||
|
list.WriteString(`" width="15%"></a>`)
|
||||||
|
} else {
|
||||||
|
list.WriteString(`<h1>[ TEXT ]</h1>`)
|
||||||
|
}
|
||||||
|
list.WriteString(`<br><a href="`)
|
||||||
|
list.WriteString(ConvertDeviantArtUrlToSkunkyArt(data.Url))
|
||||||
|
list.WriteString(`">`)
|
||||||
|
list.WriteString(data.Author.Username)
|
||||||
|
list.WriteString(" - ")
|
||||||
|
list.WriteString(data.Title)
|
||||||
|
|
||||||
list.WriteString(listContent.String())
|
// шильдики нсфв, аи и ежедневного поста
|
||||||
|
if data.NSFW {
|
||||||
|
list.WriteString(` [<span class="nsfw">NSFW</span>]`)
|
||||||
|
}
|
||||||
|
if data.AI {
|
||||||
|
list.WriteString(" [🤖]")
|
||||||
|
}
|
||||||
|
if data.DD {
|
||||||
|
list.WriteString(` [<span class="dd">DD</span>]`)
|
||||||
|
}
|
||||||
|
|
||||||
|
list.WriteString("</a></div>")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Atom {
|
||||||
|
list.WriteString("</feed>")
|
||||||
|
s.Writer.Write([]byte(list.String()))
|
||||||
|
return ""
|
||||||
|
} else {
|
||||||
list.WriteString("</div>")
|
list.WriteString("</div>")
|
||||||
if content != nil {
|
if content != nil {
|
||||||
list.WriteString(s.NavBase(content[0]))
|
list.WriteString(s.NavBase(content[0]))
|
||||||
|
@ -188,9 +176,8 @@ 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 {
|
||||||
l := len(tags)
|
l := len(tags)
|
||||||
for x := 0; x < l; x++ {
|
for x := 0; x < l; x++ {
|
||||||
|
@ -287,18 +274,16 @@ func ParseDescription(dscr devianter.Text) string {
|
||||||
|
|
||||||
switch x.Type {
|
switch x.Type {
|
||||||
case "atomic":
|
case "atomic":
|
||||||
if len(x.EntityRanges) != 0 {
|
d := entities[x.EntityRanges[0].Key]
|
||||||
d := entities[x.EntityRanges[0].Key]
|
parseddescription.WriteString(`<a href="`)
|
||||||
parsedDescription.WriteString(`<a href="`)
|
parseddescription.WriteString(ConvertDeviantArtUrlToSkunkyArt(d.Url))
|
||||||
parsedDescription.WriteString(ConvertDeviantArtUrlToSkunkyArt(d.Url))
|
parseddescription.WriteString(`"><img width="50%" src="`)
|
||||||
parsedDescription.WriteString(`"><img width="50%" src="`)
|
parseddescription.WriteString(ParseMedia(d.Media))
|
||||||
parsedDescription.WriteString(ParseMedia(d.Media))
|
parseddescription.WriteString(`" title="`)
|
||||||
parsedDescription.WriteString(`" title="`)
|
parseddescription.WriteString(d.Author.Username)
|
||||||
parsedDescription.WriteString(d.Author.Username)
|
parseddescription.WriteString(" - ")
|
||||||
parsedDescription.WriteString(" - ")
|
parseddescription.WriteString(d.Title)
|
||||||
parsedDescription.WriteString(d.Title)
|
parseddescription.WriteString(`"></a>`)
|
||||||
parsedDescription.WriteString(`"></a>`)
|
|
||||||
}
|
|
||||||
case "unstyled":
|
case "unstyled":
|
||||||
if l := len(Styles); l != 0 {
|
if l := len(Styles); l != 0 {
|
||||||
for n, r := range Styles {
|
for n, r := range Styles {
|
||||||
|
@ -307,31 +292,31 @@ func ParseDescription(dscr devianter.Text) string {
|
||||||
tag = "h2"
|
tag = "h2"
|
||||||
}
|
}
|
||||||
|
|
||||||
parsedDescription.WriteString(x.Text[:r.From])
|
parseddescription.WriteString(x.Text[:r.From])
|
||||||
if len(urls) != 0 && len(x.EntityRanges) != 0 {
|
if len(urls) != 0 && len(x.EntityRanges) != 0 {
|
||||||
ra := &x.EntityRanges[0]
|
ra := &x.EntityRanges[0]
|
||||||
|
|
||||||
parsedDescription.WriteString(`<a target="_blank" href="`)
|
parseddescription.WriteString(`<a target="_blank" href="`)
|
||||||
parsedDescription.WriteString(urls[ra.Key])
|
parseddescription.WriteString(urls[ra.Key])
|
||||||
parsedDescription.WriteString(`">`)
|
parseddescription.WriteString(`">`)
|
||||||
parsedDescription.WriteString(r.TXT)
|
parseddescription.WriteString(r.TXT)
|
||||||
parsedDescription.WriteString(`</a>`)
|
parseddescription.WriteString(`</a>`)
|
||||||
} else if l > n+1 {
|
} else if l > n+1 {
|
||||||
parsedDescription.WriteString(r.TXT)
|
parseddescription.WriteString(r.TXT)
|
||||||
}
|
}
|
||||||
parsedDescription.WriteString(TagBuilder(tag, x.Text[r.To:]))
|
parseddescription.WriteString(TagBuilder(tag, x.Text[r.To:]))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
parsedDescription.WriteString(x.Text)
|
parseddescription.WriteString(x.Text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
parsedDescription.WriteString("<br>")
|
parseddescription.WriteString("<br>")
|
||||||
}
|
}
|
||||||
} else if dl != 0 {
|
} else if dl != 0 {
|
||||||
for tt := html.NewTokenizer(strings.NewReader(dscr.Html.Markup)); ; {
|
for tt := html.NewTokenizer(strings.NewReader(dscr.Html.Markup)); ; {
|
||||||
switch tt.Next() {
|
switch tt.Next() {
|
||||||
case html.ErrorToken:
|
case html.ErrorToken:
|
||||||
return parsedDescription.String()
|
return parseddescription.String()
|
||||||
case html.StartTagToken, html.EndTagToken, html.SelfClosingTagToken:
|
case html.StartTagToken, html.EndTagToken, html.SelfClosingTagToken:
|
||||||
token := tt.Token()
|
token := tt.Token()
|
||||||
switch token.Data {
|
switch token.Data {
|
||||||
|
@ -339,11 +324,11 @@ func ParseDescription(dscr devianter.Text) string {
|
||||||
for _, a := range token.Attr {
|
for _, a := range token.Attr {
|
||||||
if a.Key == "href" {
|
if a.Key == "href" {
|
||||||
url := DeleteTrackingFromUrl(a.Val)
|
url := DeleteTrackingFromUrl(a.Val)
|
||||||
parsedDescription.WriteString(`<a target="_blank" href="`)
|
parseddescription.WriteString(`<a target="_blank" href="`)
|
||||||
parsedDescription.WriteString(url)
|
parseddescription.WriteString(url)
|
||||||
parsedDescription.WriteString(`">`)
|
parseddescription.WriteString(`">`)
|
||||||
parsedDescription.WriteString(GetValueOfTag(tt))
|
parseddescription.WriteString(GetValueOfTag(tt))
|
||||||
parsedDescription.WriteString("</a> ")
|
parseddescription.WriteString("</a> ")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case "img":
|
case "img":
|
||||||
|
@ -359,24 +344,24 @@ func ParseDescription(dscr devianter.Text) string {
|
||||||
}
|
}
|
||||||
if title != "" {
|
if title != "" {
|
||||||
for x := -1; x < b; x++ {
|
for x := -1; x < b; x++ {
|
||||||
parsedDescription.WriteString(`<img src="`)
|
parseddescription.WriteString(`<img src="`)
|
||||||
parsedDescription.WriteString(uri)
|
parseddescription.WriteString(uri)
|
||||||
parsedDescription.WriteString(`" title="`)
|
parseddescription.WriteString(`" title="`)
|
||||||
parsedDescription.WriteString(title)
|
parseddescription.WriteString(title)
|
||||||
parsedDescription.WriteString(`">`)
|
parseddescription.WriteString(`">`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case "br", "li", "ul", "p", "b":
|
case "br", "li", "ul", "p", "b":
|
||||||
parsedDescription.WriteString(token.String())
|
parseddescription.WriteString(token.String())
|
||||||
case "div":
|
case "div":
|
||||||
parsedDescription.WriteString("<p> ")
|
parseddescription.WriteString("<p> ")
|
||||||
}
|
}
|
||||||
case html.TextToken:
|
case html.TextToken:
|
||||||
parsedDescription.Write(tt.Text())
|
parseddescription.Write(tt.Text())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return parsedDescription.String()
|
return parseddescription.String()
|
||||||
}
|
}
|
||||||
|
|
93
app/router.go
Executable file → Normal file
93
app/router.go
Executable file → Normal file
|
@ -3,8 +3,7 @@ package app
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
url "net/url"
|
u "net/url"
|
||||||
"skunkyart/static"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
@ -13,7 +12,7 @@ var Host string
|
||||||
|
|
||||||
func Router() {
|
func Router() {
|
||||||
parsepath := func(path string) map[int]string {
|
parsepath := func(path string) map[int]string {
|
||||||
if l := len(CFG.URI); len(path) > l {
|
if l := len(CFG.BasePath); len(path) > l {
|
||||||
path = path[l-1:]
|
path = path[l-1:]
|
||||||
} else {
|
} else {
|
||||||
path = "/"
|
path = "/"
|
||||||
|
@ -43,61 +42,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)
|
if h := r.Header["Scheme"]; len(h) != 0 && h[0] == "https" {
|
||||||
Host = "http://" + r.Host
|
Host = h[0] + "://" + r.Host
|
||||||
if h := r.Header["X-Forwarded-Proto"]; len(h) != 0 && h[0] == "https" {
|
} else {
|
||||||
Host = "https://" + r.Host
|
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.BasePath = CFG.URI
|
skunky.Args = r.URL.Query()
|
||||||
|
skunky.BasePath = CFG.BasePath
|
||||||
|
|
||||||
|
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.BasePath)
|
||||||
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,43 +86,21 @@ 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"))
|
io.WriteString(w, Templates["skunky.css"])
|
||||||
case "favicon.ico":
|
|
||||||
w.Write(open("images/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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
http.HandleFunc("/", handle)
|
http.HandleFunc("/", handle)
|
||||||
println("SkunkyArt is listening on", CFG.Listen)
|
try_with_exitstatus(http.ListenAndServe(CFG.Listen, nil), 1)
|
||||||
|
|
||||||
tryWithExitStatus(http.ListenAndServe(CFG.Listen, nil), 1)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
286
app/util.go
Executable file → Normal file
286
app/util.go
Executable file → Normal file
|
@ -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)
|
||||||
|
@ -28,105 +26,18 @@ func try(e error) {
|
||||||
println(e.Error())
|
println(e.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func tryWithExitStatus(err error, code int) {
|
func try_with_exitstatus(err error, code int) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
exit(err.Error(), code)
|
exit(err.Error(), code)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func restore() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
recover()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
// 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())
|
||||||
}
|
}
|
||||||
|
@ -135,29 +46,16 @@ func UrlBuilder(strs ...string) string {
|
||||||
var str strings.Builder
|
var str strings.Builder
|
||||||
l := len(strs)
|
l := len(strs)
|
||||||
str.WriteString(Host)
|
str.WriteString(Host)
|
||||||
str.WriteString(CFG.URI)
|
str.WriteString(CFG.BasePath)
|
||||||
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,31 +71,23 @@ 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", "Mozilla/5.0 (X11; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0.0")
|
||||||
|
|
||||||
resp, e := cli.Do(req)
|
resp, e := cli.Do(req)
|
||||||
try(e)
|
try(e)
|
||||||
|
@ -211,29 +101,105 @@ func Download(urlString string) (d Downloaded) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
/* PARSING HELPERS */
|
// caching
|
||||||
func ParseMedia(media devianter.Media, thumb ...int) string {
|
func (s skunkyart) DownloadAndSendMedia(subdomain, path string) {
|
||||||
mediaUrl, filename := devianter.UrlFromMedia(media, thumb...)
|
var url strings.Builder
|
||||||
if len(mediaUrl) != 0 && CFG.Proxy {
|
url.WriteString("https://images-wixmp-")
|
||||||
mediaUrl = mediaUrl[21:]
|
url.WriteString(subdomain)
|
||||||
dot := strings.Index(mediaUrl, ".")
|
url.WriteString(".wixmp.com/")
|
||||||
if filename == "" {
|
url.WriteString(path)
|
||||||
filename = "image.gif"
|
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)
|
||||||
}
|
}
|
||||||
return UrlBuilder("media", "file", mediaUrl[:dot], mediaUrl[dot+11:], "&filename=", filename)
|
} else if CFG.Proxy {
|
||||||
} else if !CFG.Proxy {
|
dwnld := Download(url.String())
|
||||||
return mediaUrl
|
s.Writer.Write(dwnld.Body)
|
||||||
|
} else {
|
||||||
|
s.Writer.WriteHeader(403)
|
||||||
|
s.Writer.Write([]byte("Sorry, butt proxy on this instance are disabled."))
|
||||||
}
|
}
|
||||||
return ""
|
}
|
||||||
|
|
||||||
|
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 != 0 {
|
||||||
|
now := time.Now().UnixMilli()
|
||||||
|
|
||||||
|
f, _ := os.Stat(a)
|
||||||
|
stat := f.Sys().(*syscall.Stat_t)
|
||||||
|
time := time.Unix(stat.Ctim.Unix()).UnixMilli()
|
||||||
|
|
||||||
|
if time+c.Lifetime <= now {
|
||||||
|
try(os.RemoveAll(a))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c.MaxSize != 0 && stat.Size() > c.MaxSize {
|
||||||
|
try(os.RemoveAll(a))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dir.Close()
|
||||||
|
time.Sleep(time.Second * time.Duration(CFG.Cache.UpdateInterval))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CopyTemplatesToMemory() {
|
||||||
|
for _, dirname := range CFG.Dirs {
|
||||||
|
dir, e := os.ReadDir(dirname)
|
||||||
|
try_with_exitstatus(e, 1)
|
||||||
|
|
||||||
|
for _, x := range dir {
|
||||||
|
file, e := os.ReadFile(dirname + "/" + x.Name())
|
||||||
|
try_with_exitstatus(e, 1)
|
||||||
|
Templates[x.Name()] = string(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* PARSING HELPERS */
|
||||||
|
func ParseMedia(media devianter.Media) string {
|
||||||
|
url := devianter.UrlFromMedia(media)
|
||||||
|
if len(url) != 0 && CFG.Proxy {
|
||||||
|
url = url[21:]
|
||||||
|
dot := strings.Index(url, ".")
|
||||||
|
|
||||||
|
return UrlBuilder("media", "file", url[:dot], url[dot+11:])
|
||||||
|
}
|
||||||
|
return url
|
||||||
}
|
}
|
||||||
|
|
||||||
func ConvertDeviantArtUrlToSkunkyArt(url string) (output string) {
|
func ConvertDeviantArtUrlToSkunkyArt(url string) (output string) {
|
||||||
if len(url) > 32 && url[27:32] != "stash" {
|
if len(url) > 32 && url[27:32] != "stash" {
|
||||||
url = url[27:]
|
url = url[27:]
|
||||||
firstshash := strings.Index(url, "/")
|
toart := strings.Index(url, "/art/")
|
||||||
lastshash := firstshash + strings.Index(url[firstshash+1:], "/")
|
if toart != -1 {
|
||||||
if lastshash != -1 {
|
output = UrlBuilder("post", url[:toart], url[toart+5:])
|
||||||
output = UrlBuilder("post", url[:firstshash], url[lastshash+2:])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
@ -253,10 +219,11 @@ func BuildUserPlate(name string) string {
|
||||||
|
|
||||||
func GetValueOfTag(t *html.Tokenizer) string {
|
func GetValueOfTag(t *html.Tokenizer) string {
|
||||||
for tt := t.Next(); ; {
|
for tt := t.Next(); ; {
|
||||||
if tt == html.TextToken {
|
switch tt {
|
||||||
return string(t.Text())
|
default:
|
||||||
} else {
|
|
||||||
return ""
|
return ""
|
||||||
|
case html.TextToken:
|
||||||
|
return string(t.Text())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -269,14 +236,16 @@ type DeviationList struct {
|
||||||
|
|
||||||
// FIXME: на некоротрых артах первая страница может вызывать полное отсутствие панели навигации.
|
// FIXME: на некоротрых артах первая страница может вызывать полное отсутствие панели навигации.
|
||||||
func (s skunkyart) NavBase(c DeviationList) string {
|
func (s skunkyart) NavBase(c DeviationList) string {
|
||||||
|
// TODO: сделать понятнее
|
||||||
|
// навигация по страницам
|
||||||
var list strings.Builder
|
var list strings.Builder
|
||||||
|
|
||||||
list.WriteString("<br>")
|
list.WriteString("<br>")
|
||||||
|
p := s.Page
|
||||||
|
|
||||||
|
// функция для генерации ссылок
|
||||||
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=")
|
||||||
|
@ -299,26 +268,33 @@ func (s skunkyart) NavBase(c DeviationList) string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
p := s.Page
|
// вперёд-назад
|
||||||
|
|
||||||
if p > 1 {
|
if p > 1 {
|
||||||
prevrev("<= Prev |", p-1, false)
|
prevrev("<= Prev |", p-1, false)
|
||||||
} else {
|
} else {
|
||||||
p = 1
|
p = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, x := p-6, 0; (i <= c.Pages && i <= p+6) && x < 12; i++ {
|
if c.Pages > 0 {
|
||||||
if i > 0 {
|
// назад
|
||||||
var onPage bool
|
for x := p - 6; x < p && x > 0; x++ {
|
||||||
if i == p {
|
prevrev(strconv.Itoa(x), x, false)
|
||||||
onPage = true
|
}
|
||||||
|
|
||||||
|
// вперёд
|
||||||
|
for x := p; x <= p+6 && c.Pages > p+6; x++ {
|
||||||
|
if x == p {
|
||||||
|
prevrev("", x, true)
|
||||||
|
x++
|
||||||
}
|
}
|
||||||
|
|
||||||
prevrev(strconv.Itoa(i), i, onPage)
|
if x > p {
|
||||||
x++
|
prevrev(strconv.Itoa(x), x, false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// вперёд-назад
|
||||||
if c.More {
|
if c.More {
|
||||||
prevrev("| Next =>", p+1, false)
|
prevrev("| Next =>", p+1, false)
|
||||||
}
|
}
|
||||||
|
|
345
app/wrapper.go
Executable file → Normal file
345
app/wrapper.go
Executable file → Normal file
|
@ -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,77 @@ 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 []struct {
|
||||||
|
Title string
|
||||||
|
Country string
|
||||||
|
Urls []struct {
|
||||||
|
I2P string `json:"i2p"`
|
||||||
|
Ygg string
|
||||||
|
Tor string
|
||||||
|
Clearnet string
|
||||||
|
}
|
||||||
|
Settings struct {
|
||||||
|
Nsfw bool
|
||||||
|
Proxy bool
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SomeList string
|
||||||
|
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,16 +92,8 @@ 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
|
s.Templates.GroupUser.GR = g.GroupFunc()
|
||||||
s.Templates.GroupUser.GR, err, daError = g.Get()
|
|
||||||
try(err)
|
|
||||||
if daError.RAW != nil {
|
|
||||||
s.Error(daError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
group := &s.Templates.GroupUser
|
group := &s.Templates.GroupUser
|
||||||
|
|
||||||
switch s.Type {
|
switch s.Type {
|
||||||
|
@ -36,15 +103,17 @@ func (s skunkyart) GRUser() {
|
||||||
for _, x := range g.Gruser.Page.Modules {
|
for _, x := range g.Gruser.Page.Modules {
|
||||||
switch x.Name {
|
switch x.Name {
|
||||||
case "about", "group_about":
|
case "about", "group_about":
|
||||||
if g.Owner.Group {
|
switch g.Owner.Group {
|
||||||
|
case true:
|
||||||
var about = &x.ModuleData.GroupAbout
|
var about = &x.ModuleData.GroupAbout
|
||||||
group.Group = true
|
group.Group = true
|
||||||
group.CreationDate = x.ModuleData.GroupAbout.FoundatedAt.UTC().String()
|
group.CreationDate = x.ModuleData.GroupAbout.FoundatedAt.UTC().String()
|
||||||
group.About.DescriptionFormatted = ParseDescription(about.Description)
|
group.About.DescriptionFormatted = ParseDescription(about.Description)
|
||||||
} else if false {
|
case false:
|
||||||
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 {
|
||||||
|
@ -66,7 +135,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.CommentsFunc(
|
||||||
|
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
|
||||||
|
@ -81,77 +155,56 @@ 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 := g.Gallery(s.Page, folderid)
|
||||||
content, daError = g.Favourites(s.Page, all, folderid)
|
if folderid > 0 {
|
||||||
} else {
|
group.Gallery.List = s.DeviationList(gallery.Content.Results, DeviationList{
|
||||||
content, err, daError = g.Gallery(s.Page, folderid)
|
More: gallery.Content.HasMore,
|
||||||
try(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if daError.RAW != nil {
|
|
||||||
s.Error(daError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
if x.Name == "folder_deviations" {
|
if x.Name == "folder_deviations" {
|
||||||
group.Gallery.List = s.DeviationList(x.ModuleData.Folder.Deviations, true, DeviationList{
|
group.Gallery.List = s.DeviationList(x.ModuleData.Folder.Deviations, DeviationList{
|
||||||
Pages: x.ModuleData.Folder.Pages,
|
Pages: x.ModuleData.Folder.Pages,
|
||||||
More: x.ModuleData.Folder.HasMore,
|
More: x.ModuleData.Folder.HasMore,
|
||||||
})
|
})
|
||||||
|
@ -163,113 +216,77 @@ func (s skunkyart) GRUser() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !s.Atom {
|
if !s.Atom {
|
||||||
s.ExecuteTemplate("gruser.htm", "html", &s)
|
s.ExecuteTemplate("gruser.htm", &s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// посты
|
// посты
|
||||||
func (s skunkyart) Deviation(author, postname string) {
|
func (s skunkyart) Deviation(author, postname string) {
|
||||||
id_search := regexp.MustCompile("[0-9]+").FindAllString(postname, -1)
|
id_search := regexp.MustCompile("[0-9]+").FindAllString(postname, -1)
|
||||||
if len(id_search) < 1 {
|
if len(id_search) >= 1 {
|
||||||
s.ReturnHTTPError(400)
|
post := &s.Templates.Deviation
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var err devianter.Error
|
id := id_search[len(id_search)-1]
|
||||||
post := &s.Templates.Deviation
|
post.Post = devianter.DeviationFunc(id, author)
|
||||||
|
|
||||||
id := id_search[len(id_search)-1]
|
if post.Post.Deviation.TextContent.Excerpt != "" {
|
||||||
post.Post, err = devianter.GetDeviation(id, author)
|
post.Post.Description = ParseDescription(post.Post.Deviation.TextContent)
|
||||||
if err.RAW != nil {
|
} else {
|
||||||
s.Error(err)
|
post.Post.Description = ParseDescription(post.Post.Deviation.Extended.DescriptionText)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if post.Post.Deviation.NSFW && !CFG.Nsfw {
|
|
||||||
s.Writer.WriteHeader(403)
|
|
||||||
wr(s.Writer, `<html><link rel="stylesheet" href="`+
|
|
||||||
UrlBuilder("stylesheet")+
|
|
||||||
`" /><h1>NSFW content are disabled on this instance.</h1></html>`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if post.Post.Comments.Total <= 50 {
|
|
||||||
post.Post.Comments.Cursor = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if post.Post.Deviation.TextContent.Excerpt != "" {
|
|
||||||
post.Post.Description = ParseDescription(post.Post.Deviation.TextContent)
|
|
||||||
} else {
|
|
||||||
post.Post.Description = ParseDescription(post.Post.Deviation.Extended.DescriptionText)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, x := range post.Post.Deviation.Extended.RelatedContent {
|
|
||||||
if len(x.Deviations) != 0 {
|
|
||||||
post.Related += s.DeviationList(x.Deviations, false)
|
|
||||||
}
|
}
|
||||||
|
// время публикации
|
||||||
|
post.StringTime = post.Post.Deviation.PublishedTime.UTC().String()
|
||||||
|
post.Post.IMG = ParseMedia(post.Post.Deviation.Media)
|
||||||
|
for _, x := range post.Post.Deviation.Extended.RelatedContent {
|
||||||
|
if len(x.Deviations) != 0 {
|
||||||
|
post.Related += s.DeviationList(x.Deviations)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// хештэги
|
||||||
|
for _, x := range post.Post.Deviation.Extended.Tags {
|
||||||
|
var tag strings.Builder
|
||||||
|
tag.WriteString(` <a href="`)
|
||||||
|
tag.WriteString(UrlBuilder("search", "?q=", x.Name, "&type=tag"))
|
||||||
|
tag.WriteString(`">#`)
|
||||||
|
tag.WriteString(x.Name)
|
||||||
|
tag.WriteString("</a>")
|
||||||
|
|
||||||
|
post.Tags += tag.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
if post.Post.Comments.Total <= 50 {
|
||||||
|
post.Post.Comments.Cursor = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
post.Comments = s.ParseComments(devianter.CommentsFunc(id, post.Post.Comments.Cursor, s.Page, 1))
|
||||||
|
|
||||||
|
s.ExecuteTemplate("deviantion.htm", &s)
|
||||||
|
} else {
|
||||||
|
s.ReturnHTTPError(400)
|
||||||
}
|
}
|
||||||
|
|
||||||
// хештэги
|
|
||||||
for _, x := range post.Post.Deviation.Extended.Tags {
|
|
||||||
var tag strings.Builder
|
|
||||||
tag.WriteString(` <a href="`)
|
|
||||||
tag.WriteString(UrlBuilder("search", "?q=", x.Name, "&type=tag"))
|
|
||||||
tag.WriteString(`">#`)
|
|
||||||
tag.WriteString(x.Name)
|
|
||||||
tag.WriteString("</a>")
|
|
||||||
|
|
||||||
post.Tags += tag.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
post.Comments = s.ParseComments(devianter.GetComments(id, post.Post.Comments.Cursor, s.Page, 1))
|
|
||||||
post.StringTime = post.Post.Deviation.PublishedTime.UTC().String()
|
|
||||||
post.Post.IMG = ParseMedia(post.Post.Deviation.Media)
|
|
||||||
|
|
||||||
s.ExecuteTemplate("deviantion.htm", "html", &s)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s skunkyart) DD() {
|
func (s skunkyart) DD() {
|
||||||
dd, err := devianter.GetDailyDeviations(s.Page)
|
dd := devianter.DailyDeviationsFunc(s.Page)
|
||||||
if err.RAW != nil {
|
s.Templates.SomeList = s.DeviationList(dd.Deviations, DeviationList{
|
||||||
s.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var strips strings.Builder
|
|
||||||
for _, x := range dd.Strips {
|
|
||||||
strips.WriteString(`<h3 class="`)
|
|
||||||
strips.WriteString(x.Codename)
|
|
||||||
strips.WriteString(`"> <a href="#`)
|
|
||||||
strips.WriteString(x.Codename)
|
|
||||||
strips.WriteString(`"># </a>`)
|
|
||||||
strips.WriteString(x.Title)
|
|
||||||
strips.WriteString(`</h3>`)
|
|
||||||
|
|
||||||
strips.WriteString(s.DeviationList(x.Deviations, false))
|
|
||||||
}
|
|
||||||
s.Templates.DDStrips = strips.String()
|
|
||||||
s.Templates.SomeList = s.DeviationList(dd.Deviations, true, DeviationList{
|
|
||||||
Pages: 0,
|
Pages: 0,
|
||||||
More: dd.HasMore,
|
More: dd.HasMore,
|
||||||
})
|
})
|
||||||
if !s.Atom {
|
if !s.Atom {
|
||||||
s.ExecuteTemplate("daily.htm", "html", &s)
|
s.ExecuteTemplate("list.htm", &s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s skunkyart) Search() {
|
func (s skunkyart) Search() {
|
||||||
if s.Query == "" {
|
s.Atom = false
|
||||||
s.ReturnHTTPError(400)
|
var e error
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
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, e = devianter.SearchFunc(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, e = devianter.SearchFunc(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)
|
||||||
|
@ -315,34 +332,34 @@ func (s skunkyart) Search() {
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
s.ReturnHTTPError(400)
|
s.ReturnHTTPError(400)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
try(err)
|
try(e)
|
||||||
|
|
||||||
if s.Type != 'r' {
|
if s.Type != 'r' {
|
||||||
if daError.RAW != nil {
|
ss.List = s.DeviationList(ss.Content.Results, DeviationList{
|
||||||
s.Error(daError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
||||||
if name == "" || !(s.Type == 'a' || s.Type == 'e') {
|
if name != "" && (s.Type == 'a' || s.Type == 'e') {
|
||||||
|
ae, e := devianter.AEmedia(name, s.Type)
|
||||||
|
if e != nil {
|
||||||
|
s.ReturnHTTPError(404)
|
||||||
|
}
|
||||||
|
wr(s.Writer, ae)
|
||||||
|
} else {
|
||||||
s.ReturnHTTPError(400)
|
s.ReturnHTTPError(400)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
ae, e := devianter.AEmedia(name, s.Type)
|
|
||||||
if e != nil {
|
func (s skunkyart) About() {
|
||||||
s.ReturnHTTPError(404)
|
s.Templates.About.Nsfw = CFG.Nsfw
|
||||||
}
|
s.Templates.About.Proxy = CFG.Proxy
|
||||||
wr(s.Writer, ae)
|
try(json.Unmarshal([]byte(Templates["instances.json"]), &s.Templates.About))
|
||||||
|
s.ExecuteTemplate("about.htm", &s)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
services:
|
|
||||||
skunkyart:
|
|
||||||
container_name: skunkyart
|
|
||||||
restart: unless-stopped
|
|
||||||
build: .
|
|
||||||
ports:
|
|
||||||
- "127.0.0.1:3003:3003"
|
|
||||||
security_opt:
|
|
||||||
- no-new-privileges:true
|
|
||||||
volumes:
|
|
||||||
- ./config.json:/config.json:ro
|
|
||||||
- ./cache:/cache # Ensure cache folder has a 10000:10000 ownership.
|
|
15
config.example.json
Executable file → Normal file
15
config.example.json
Executable file → Normal file
|
@ -1,17 +1,18 @@
|
||||||
{
|
{
|
||||||
"listen": "0.0.0.0:3003",
|
"listen": "0.0.0.0:3003",
|
||||||
"uri": "/",
|
"base-path": "/",
|
||||||
"cache": {
|
"cache": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"path": "cache",
|
"path": "/home/skunk/projects/skunkyart/cache",
|
||||||
"lifetime": null,
|
"lifetime": null,
|
||||||
"max-size": 200,
|
"max-size": 100000,
|
||||||
"memcache": false,
|
|
||||||
"update-interval": 5
|
"update-interval": 5
|
||||||
},
|
},
|
||||||
"static-path": "static",
|
"dirs-to-memory": [
|
||||||
"download-proxy": "http://127.0.0.1:8080",
|
"/home/skunk/projects/skunkyart/html",
|
||||||
"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",
|
"/home/skunk/projects/skunkyart/css"
|
||||||
|
],
|
||||||
|
"download-proxy": null,
|
||||||
"proxy": true,
|
"proxy": true,
|
||||||
"nsfw": false
|
"nsfw": false
|
||||||
}
|
}
|
||||||
|
|
89
static/css/skunky.css → css/skunky.css
Executable file → Normal file
89
static/css/skunky.css → css/skunky.css
Executable file → Normal file
|
@ -1,6 +1,6 @@
|
||||||
/* TAGS */
|
/* TAGS */
|
||||||
html {
|
html {
|
||||||
font-family: ubuntu, system-ui;
|
font-family: Ubuntu;
|
||||||
background-color:black;
|
background-color:black;
|
||||||
color: rgb(234, 216, 216);
|
color: rgb(234, 216, 216);
|
||||||
}
|
}
|
||||||
|
@ -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 */
|
||||||
|
@ -45,22 +39,24 @@ input:focus {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
.block {
|
.block {
|
||||||
padding: 0px 0px 6px 0px;
|
max-width: 20%;
|
||||||
border: 3px solid #000;
|
height: 0%;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 3px solid #091f19;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
background-color: #091f19;
|
background-color: #091f19;
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
.block h1 {
|
|
||||||
padding: 8.5vh;
|
|
||||||
}
|
|
||||||
.block:hover {
|
.block:hover {
|
||||||
border: 3px solid #4d27d6;
|
border: 3px solid #4d27d6;
|
||||||
transition: 400ms;
|
transition: 400ms;
|
||||||
}
|
}
|
||||||
|
.block img, .plates .user-plate img {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
.block p {
|
.block p {
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
@ -135,77 +131,28 @@ input:focus {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* SCREEN OPTIMISATIONS */
|
/* SCREEN OPTIMISATIONS */
|
||||||
@media (orientation: portrait) {
|
@media screen and (orientation: portrait) {
|
||||||
* {
|
|
||||||
font-size: 120%
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
font-size: 80%
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
header {
|
||||||
margin-left: 3%;
|
scale: 155%;
|
||||||
text-align: center;
|
justify-content: center;
|
||||||
display: inline-block;
|
|
||||||
clear: both;
|
|
||||||
font-size: 200%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
form {
|
|
||||||
font-size: 60%;
|
|
||||||
border: solid #164e3e 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
display: inherit;
|
display: inherit;
|
||||||
scale: 100%;
|
scale: 100%;
|
||||||
}
|
}
|
||||||
.block {
|
.block {
|
||||||
margin-top: 10%;
|
max-width: 60%;
|
||||||
max-width: 200%;
|
|
||||||
}
|
|
||||||
.folder-item {
|
|
||||||
width: 25%
|
|
||||||
}
|
|
||||||
.folders {
|
|
||||||
display: flexbox;
|
|
||||||
justify-content: center
|
|
||||||
}
|
|
||||||
figure img {
|
|
||||||
width: 10%
|
|
||||||
}
|
|
||||||
figure a img {
|
|
||||||
width: 100%
|
|
||||||
}
|
|
||||||
.msg {
|
|
||||||
font-size: 60%;
|
|
||||||
max-width: 80%
|
|
||||||
}
|
|
||||||
.block img, .plates .user-plate img {
|
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (orientation: landscape) {
|
@media screen and (max-width: 1462px) {
|
||||||
.block {
|
|
||||||
width: 20%;
|
|
||||||
}
|
|
||||||
.block img, .plates .user-plate img {
|
|
||||||
width: 100%;
|
|
||||||
height: 30vh;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1462px) and (orientation: landscape) {
|
|
||||||
.block {
|
.block {
|
||||||
max-width: 30%;
|
max-width: 30%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 788px) and (max-width: 884px) {
|
@media screen and (min-width: 788px) and (max-width: 884px) {
|
||||||
.block {
|
.block {
|
||||||
max-width: 35%;
|
max-width: 35%;
|
||||||
}
|
}
|
4
go.mod
Executable file → Normal file
4
go.mod
Executable file → Normal file
|
@ -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.0
|
||||||
golang.org/x/net v0.27.0
|
golang.org/x/net v0.27.0
|
||||||
)
|
)
|
||||||
|
|
4
go.sum
Executable file → Normal file
4
go.sum
Executable file → Normal file
|
@ -1,4 +1,4 @@
|
||||||
git.macaw.me/skunky/devianter v0.2.6-0.20240904171839-b3c99749f133 h1:ziutYUyDmdbsptR8Lj4lNmZUxfgwGsNbHM1mO9ATph8=
|
git.macaw.me/skunky/devianter v0.2.0 h1:2vnMPb1Dax37CbAOfmHcSoK8+1goFkWHbtbh31Ytsww=
|
||||||
git.macaw.me/skunky/devianter v0.2.6-0.20240904171839-b3c99749f133/go.mod h1:ZLn527xBlnpXrUB1B8z/MhyeiWVK4nPWjyfnhWOE8Is=
|
git.macaw.me/skunky/devianter v0.2.0/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=
|
||||||
|
|
63
html/about.htm
Normal file
63
html/about.htm
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>SkunkyArt</title>
|
||||||
|
<link rel="stylesheet" href="{{.BasePath}}stylesheet">
|
||||||
|
</head>
|
||||||
|
<main>
|
||||||
|
<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>
|
||||||
|
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>
|
||||||
|
<h2>Instance settings:</h2>
|
||||||
|
<ul>
|
||||||
|
<li><b>NSFW</b>: <span class="about-{{.Templates.About.Nsfw}}">{{if .Templates.About.Nsfw}}YES{{else}}NO{{end}}</span></li>
|
||||||
|
<li><b>Proxyfing</b>: <span class="about-{{.Templates.About.Proxy}}">{{if .Templates.About.Proxy}}YES{{else}}NO{{end}}</span></li>
|
||||||
|
</ul>
|
||||||
|
<h2>Instances:</h2>
|
||||||
|
<ul>
|
||||||
|
{{range .Templates.About.Instances}}
|
||||||
|
<li><u><b>{{.Title}}</b></u>:
|
||||||
|
<ul>
|
||||||
|
<li><b>Country</b>: {{.Country}}</li>
|
||||||
|
<li><b>URLs</b>: </li>
|
||||||
|
<ul>
|
||||||
|
{{range .Urls}}
|
||||||
|
{{if ne .I2P ""}}
|
||||||
|
<li><b>I2P</b>: <a href="{{.I2P}}">Yes</a></li>
|
||||||
|
{{end}}
|
||||||
|
{{if ne .Ygg ""}}
|
||||||
|
<li><b>Ygg</b>: <a href="{{.Ygg}}">Yes</a></li>
|
||||||
|
{{end}}
|
||||||
|
{{if ne .Tor ""}}
|
||||||
|
<li><b>Tor</b>: <a href="{{.Tor}}">Yes</a></li>
|
||||||
|
{{end}}
|
||||||
|
{{if ne .Clearnet ""}}
|
||||||
|
<li><b>Clearnet</b>: <a href="{{.Clearnet}}">{{.Clearnet}}</a></li>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
<li><b>Settings</b>: </li>
|
||||||
|
<ul>
|
||||||
|
<li><b>NSFW</b>: <span class="about-{{.Settings.Nsfw}}">{{if .Settings.Nsfw}}YES{{else}}NO{{end}}</span></li>
|
||||||
|
<li><b>Proxyfing</b>: <span class="about-{{.Settings.Proxy}}">{{if .Settings.Proxy}}YES{{else}}NO{{end}}</span></li>
|
||||||
|
</ul>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
<p>Copyright <a href="https://go.kde.org/matrix/#/@softpigeones:ebloid.ru" target="_blank">lost+skunk</a>, X11. <a href="https://git.macaw.me/skunky/skunkyart/src/tag/v1.3" target="_blank">SkunkyArt v1.3</a></p>
|
||||||
|
</main>
|
||||||
|
</html>
|
19
static/html/deviantion.htm → html/deviantion.htm
Executable file → Normal file
19
static/html/deviantion.htm → html/deviantion.htm
Executable file → Normal file
|
@ -1,8 +1,23 @@
|
||||||
<!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" />
|
||||||
|
</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)}}
|
30
static/html/gruser.htm → html/gruser.htm
Executable file → Normal file
30
static/html/gruser.htm → html/gruser.htm
Executable file → Normal file
|
@ -1,27 +1,25 @@
|
||||||
<!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">
|
||||||
|
</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>
|
||||||
|
@ -30,7 +28,7 @@
|
||||||
</form> <h1>| {{.Templates.GroupUser.GR.Owner.Username}}</h1>
|
</form> <h1>| {{.Templates.GroupUser.GR.Owner.Username}}</h1>
|
||||||
</header>
|
</header>
|
||||||
{{if eq .Type 'a'}}
|
{{if eq .Type 'a'}}
|
||||||
{{if and (and (ne .Templates.About.Nsfw true) (ne .Templates.GroupUser.About.BGMeta.NSFW true)) (ne .Templates.GroupUser.About.BG "")}}
|
{{if ne .Templates.GroupUser.About.BG ""}}
|
||||||
<a href="{{.Templates.GroupUser.About.BGMeta.Url}}" class="ubg"><img title="{{if ne .Templates.GroupUser.GR.Owner.Username .Templates.GroupUser.About.BGMeta.Author.Username}}
|
<a href="{{.Templates.GroupUser.About.BGMeta.Url}}" class="ubg"><img title="{{if ne .Templates.GroupUser.GR.Owner.Username .Templates.GroupUser.About.BGMeta.Author.Username}}
|
||||||
{{.Templates.GroupUser.About.BGMeta.Author.Username}} - {{end}}{{.Templates.GroupUser.About.BGMeta.Title}}" src="{{.Templates.GroupUser.About.BG}}"></a>
|
{{.Templates.GroupUser.About.BGMeta.Author.Username}} - {{end}}{{.Templates.GroupUser.About.BGMeta.Title}}" src="{{.Templates.GroupUser.About.BG}}"></a>
|
||||||
{{end}}
|
{{end}}
|
21
html/index.htm
Normal file
21
html/index.htm
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>SkunkyArt</title>
|
||||||
|
<link rel="stylesheet" href="{{.}}stylesheet"/>
|
||||||
|
</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>
|
22
html/list.htm
Normal file
22
html/list.htm
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>SkunkyArt</title>
|
||||||
|
<link rel="stylesheet" href="{{.BasePath}}stylesheet">
|
||||||
|
</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>
|
||||||
|
{{.Templates.SomeList}}
|
||||||
|
</main>
|
||||||
|
</html>
|
29
html/search.htm
Normal file
29
html/search.htm
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>SkunkyArt | Search "{{.QueryRaw}}"</title>
|
||||||
|
<link rel="stylesheet" href="{{.BasePath}}stylesheet">
|
||||||
|
</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>
|
87
instances.json
Executable file → Normal file
87
instances.json
Executable file → Normal file
|
@ -1,81 +1,48 @@
|
||||||
{
|
{
|
||||||
"instances": [
|
"instances": [
|
||||||
{
|
{
|
||||||
"title": "lost-skunk.cc",
|
"title": "skunky.ebloid.ru",
|
||||||
"country": "Finland",
|
"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://lost-skunk.cc/skunkyart"
|
"clearnet": "https://skunky.ebloid.ru/art"
|
||||||
},
|
}],
|
||||||
"settings": {
|
"settings": {
|
||||||
"proxy": true,
|
"nsfw": false,
|
||||||
"nsfw": false
|
"proxy": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "orehus.club",
|
"title": "clovius.club",
|
||||||
"country": "Germany",
|
"country": "Sweden",
|
||||||
"urls": {
|
"urls": [{
|
||||||
"clearnet": "https://sa.orehus.club"
|
"clearnet": "https://skunky.clovius.club"
|
||||||
},
|
}],
|
||||||
"settings": {
|
"settings": {
|
||||||
"proxy": false,
|
"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": "lumaeris.com",
|
"title": "frontendfriendly.xyz",
|
||||||
"country": "Germany",
|
"country": "Finland",
|
||||||
"urls": {
|
"urls": [{
|
||||||
"clearnet": "https://skunkyart.lumaeris.com"
|
"clearnet": "https://skunkyart.frontendfriendly.xyz"
|
||||||
},
|
}],
|
||||||
"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
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "dc09.ru",
|
|
||||||
"country": "Russia",
|
|
||||||
"urls": {
|
|
||||||
"clearnet": "https://sa.dc09.ru"
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"proxy": true,
|
|
||||||
"nsfw": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "opnxng.com",
|
|
||||||
"country": "Singapore",
|
|
||||||
"urls": {
|
|
||||||
"clearnet": "https://da.opnxng.com"
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"proxy": true,
|
|
||||||
"nsfw": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
23
main.go
Executable file → Normal file
23
main.go
Executable file → Normal file
|
@ -2,30 +2,17 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"skunkyart/app"
|
"skunkyart/app"
|
||||||
"skunkyart/static"
|
|
||||||
"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()
|
|
||||||
|
|
||||||
app.ExecuteCommandLineArguments()
|
|
||||||
app.ExecuteConfig()
|
app.ExecuteConfig()
|
||||||
static.CopyTemplatesToMemory()
|
app.CopyTemplatesToMemory()
|
||||||
|
err := devianter.UpdateCSRF()
|
||||||
go func() {
|
if err != nil {
|
||||||
for {
|
println(err.Error())
|
||||||
err := devianter.UpdateCSRF()
|
}
|
||||||
if err != nil {
|
|
||||||
println(err.Error())
|
|
||||||
}
|
|
||||||
time.Sleep(12 * time.Hour)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
app.Router()
|
app.Router()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
#!/sbin/openrc-run
|
|
||||||
supervisor=supervise-daemon
|
|
||||||
user=skunkyart:skunkyart
|
|
||||||
name=SkunkyArt
|
|
||||||
directory=/opt/skunkyart
|
|
||||||
command=$directory/bin/skunkyart
|
|
||||||
description="Privacy-oriented frontend for DeviantArt"
|
|
||||||
error_logger=logger
|
|
||||||
output_logger=logger
|
|
||||||
no_new_privs=true
|
|
||||||
umask=0077
|
|
||||||
|
|
||||||
# if you use old version openrc, uncomment function lower
|
|
||||||
# start_pre() {
|
|
||||||
# cd $directory
|
|
||||||
# }
|
|
||||||
|
|
||||||
depend() {
|
|
||||||
need net localmount bootmisc
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
# Note: i didn't use systemd, so it can be not works :)
|
|
||||||
|
|
||||||
[Unit]
|
|
||||||
Description=Privacy-oriented frontend for DeviantArt
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Directory=<path-to-dir-with-skunkyart>
|
|
||||||
ExecStart=<path-to-dir-skunkyart>
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
11
skunkyart.example.openrc
Executable file
11
skunkyart.example.openrc
Executable file
|
@ -0,0 +1,11 @@
|
||||||
|
#!/sbin/openrc-run
|
||||||
|
name="SkunkyArt"
|
||||||
|
description="Privacy frontend for deviantart.com"
|
||||||
|
supervisor=supervise-daemon
|
||||||
|
command=<path_to_skunkyart>
|
||||||
|
command_args="-c <path_to_config>"
|
||||||
|
directory="<path_to_dir_with_skunkyart>"
|
||||||
|
|
||||||
|
depend() {
|
||||||
|
need net
|
||||||
|
}
|
|
@ -1,49 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
{{template "head" .}}
|
|
||||||
<main>
|
|
||||||
{{template "header" .}}
|
|
||||||
<p>
|
|
||||||
SkunkyArt is an alternative frontend for deviantart.com, written in Go.
|
|
||||||
</p>
|
|
||||||
<h3><a href="https://go.kde.org/matrix/#/#skunkyart:gnulinux.club" 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>
|
|
||||||
<li><b>Proxyfing</b>: <span class="about-{{.Templates.About.Proxy}}">{{if .Templates.About.Proxy}}YES{{else}}NO{{end}}</span></li>
|
|
||||||
</ul>
|
|
||||||
<details>
|
|
||||||
<summary><b>Instances:</b></summary>
|
|
||||||
<ul>
|
|
||||||
{{range .Templates.About.Instances}}
|
|
||||||
<li><u><b>{{.Title}}</b></u>:
|
|
||||||
<ul>
|
|
||||||
<li><b>Country</b>: {{.Country}}</li>
|
|
||||||
<li><b>URLs</b>: </li>
|
|
||||||
<ul>
|
|
||||||
{{if ne .Urls.I2P ""}}
|
|
||||||
<li><b>I2P</b>: <a href="{{.Urls.I2P}}">Yes</a></li>
|
|
||||||
{{end}}
|
|
||||||
{{if ne .Urls.Ygg ""}}
|
|
||||||
<li><b>Ygg</b>: <a href="{{.Urls.Ygg}}">Yes</a></li>
|
|
||||||
{{end}}
|
|
||||||
{{if ne .Urls.Tor ""}}
|
|
||||||
<li><b>Tor</b>: <a href="{{.Urls.Tor}}">Yes</a></li>
|
|
||||||
{{end}}
|
|
||||||
{{if ne .Urls.Clearnet ""}}
|
|
||||||
<li><b>Clearnet</b>: <a href="{{.Urls.Clearnet}}">{{.Urls.Clearnet}}</a></li>
|
|
||||||
{{end}}
|
|
||||||
</ul>
|
|
||||||
<li><b>Settings</b>: </li>
|
|
||||||
<ul>
|
|
||||||
<li><b>NSFW</b>: <span class="about-{{.Settings.Nsfw}}">{{if .Settings.Nsfw}}YES{{else}}NO{{end}}</span></li>
|
|
||||||
<li><b>Proxyfing</b>: <span class="about-{{.Settings.Proxy}}">{{if .Settings.Proxy}}YES{{else}}NO{{end}}</span></li>
|
|
||||||
</ul>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
{{end}}
|
|
||||||
</ul>
|
|
||||||
</details>
|
|
||||||
<p>Copyright <a href="https://go.kde.org/matrix/#/@ls:gnulinux.club" 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>
|
|
||||||
</main>
|
|
||||||
</html>
|
|
|
@ -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>
|
|
Binary file not shown.
Before Width: | Height: | Size: 398 KiB |
|
@ -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…
Add table
Add a link
Reference in a new issue