diff --git a/.dockerignore b/.dockerignore
new file mode 100755
index 0000000..6833fbe
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,6 @@
+cache
+compose.yaml
+*.json
+LICENSE
+*.md
+services
diff --git a/.gitignore b/.gitignore
old mode 100644
new mode 100755
index 4686488..63ca398
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
**/cache
+**/compose.yaml
**/config.json
**/skunkyart
+**/skunkyart-*
diff --git a/Dockerfile b/Dockerfile
new file mode 100755
index 0000000..fdc1919
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,23 @@
+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"]
diff --git a/INSTANCES.md b/INSTANCES.md
new file mode 100755
index 0000000..38cd5bb
--- /dev/null
+++ b/INSTANCES.md
@@ -0,0 +1,11 @@
+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 |
diff --git a/LICENSE b/LICENSE
old mode 100644
new mode 100755
diff --git a/README.md b/README.md
old mode 100644
new mode 100755
index 9e30da0..e5f404d
--- a/README.md
+++ b/README.md
@@ -1,78 +1,51 @@
-[](https://go.kde.org/matrix/#/#skunkyart:ebloid.ru)
-# Instances
-|Инстанс|Yggdrasil|I2P|Tor|NSFW|Proxifying|Country|
-|:-----:|:-------:|:-:|:-:|:--:|:--------:|:-----:|
-|[skunky.ebloid.ru](https://skunky.ebloid.ru/art)|[Yes](http://[201:eba5:d1fc:bf7b:cfcb:a811:4b8b:7ea3]/art)|No|No| No | No | Russia |
-|[clovius.club](https://skunky.clovius.club)|No|No|No| Yes | Yes | Sweden |
-|[bloat.cat](https://skunky.bloat.cat)|No|No|No| Yes | Yes | Romania |
-|[frontendfriendly.xyz](https://skunkyart.frontendfriendly.xyz)|No|No|No| Yes | Yes | Finland |
+> [!NOTE]
+> Currently, due to school, I cannot actively develop this project :(
+> However, this does not mean that development has stopped. Just wait for the summer. For questions, write either to the Matrix room or to me in DM.
+
+
+
+[](https://go.kde.org/matrix/#/#skunkyart:gnulinux.club)
+
+Instances: [`INSTANCES.md`](/skunky/SkunkyArt/src/branch/master/INSTANCES.md)
# EN 🇺🇸
## Description
-SkunkyArt 🦨 -- alternative frontend to DeviantArt, which will work without problems even on quite old hardware, due to the lack of JavaScript.
-## Config
-The sample config is in the `config.example.json` file. To specify your own path to the config, use the CLI argument `-c` or `--config`.
-* `listen` -- the address and port on which SkunkyArt will listen
-* `base-path` -- the path to the instance. Example: "`base-path`:"/art/" -> https://skunky.ebloid.ru/art/
-* `cache` -- caching system; default is off.
-* * `path` -- the path to the cache
-* * `lifetime` -- cache file lifetime; measured in Unix milliseconds.
-* * `max-size` -- maximum file size in bytes.
-* `dirs-to-memory` -- this setting determines which directories will be copied to RAM when SkunkyArt is started. Required
-* `download-proxy` -- proxy address for downloading files.
-## Examples of reverse proxies
-Nginx:
-```apache
-server {
- listen 443 ssl;
- server_name skunky.example.com;
-
- location ((BASE URL)) { # if you have a separate subdomain for the frontend, insert '/' instead of '((BASE URL))'.
- proxy_set_header Scheme $scheme;
- proxy_set_header Host $host;
- proxy_http_version 1.1;
- proxy_pass http://((IP)):((PORT));
- }
-}
-```
-## How do I add my instance to the list?
-To do this, you must either make a PR by adding your instance to the `instances.json` file, or report it to the room in Matrix. I don't think it needs any description. However, be aware, this list has a couple rules:
-1. the instance must not use Cloudflare.
+SkunkyArt 🦨 — alternative frontend for DevianArt, which works without JS.
+## Build (translated via DeepL)
+It is recommended to build with the 'embed' tag because it embeds the presets in the binary. If you plan to modify the templates, then do not use this tag. You can also add the `-ldflags "-w -s"` argument (GCCGO has a different name for it — `gccgoflags`) to reduce the size of the output file. Here is an example:
+
+`go build -tags embed -ldflags "-w -s"`
+
+Pre-compiled binaries can be found in the [Releases](https://git.macaw.me/skunky/skunkyart/releases) tab.
+## Setup
+The sample config is in the `config.example.json` file. For custom config, use `--config` option.
+See the [`SETUP.md`](/skunky/SkunkyArt/src/branch/master/SETUP.md) file for more info about directives.
+## 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.
2. If your instance has modified source code, you need to publish it to any free platform. For example, Github and Gitlab are not.
## Acknowledgements
-* [Лис⚛](https://go.kde.org/matrix/#/@fox:matrix.org) -- helped me understand Go and gave me a lot of useful advice on this language.
+* [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.
+* [meoww](https://codeberg.org/meoww) — translated some sentences into English and wrote a service for openrc
# RU 🇷🇺
## Описание
-SkunkyArt 🦨 -- альтернативный фронтенд к DeviantArt, который будет работать без проблем даже на довольно старом оборудовании, за счёт отсутствия JavaScript.
-## Конфиг
-Пример конфига находится в файле `config.example.json`. Чтобы указать свой путь до конфига, используйте CLI-аргумент `-c` или `--config`.
-* `listen` -- адрес и порт, на котором будет слушать SkunkyArt
-* `base-path` -- путь к инстансу. Пример: "base-path": "/art/" -> https://skunky.ebloid.ru/art/
-* `cache` -- система кеширования; по умолчанию - выкл.
-* * `path` -- путь до кеша
-* * `lifetime` -- время жизни файла в кеше; измеряется в Unix-миллисекундах
-* * `max-size` -- максимальный размер файла в байтах
-* `dirs-to-memory` -- данная настройка определяет какие каталоги будут скопированы в ОЗУ при запуске SkunkyArt. Обязательна
-* `download-proxy` -- адрес прокси для загрузки файлов
-## Примеры reverse-прокси
-Nginx:
-```apache
-server {
- listen 443 ssl;
- server_name skunky.example.com;
-
- location ((BASE URL)) { # если у вас отдельный поддомен для фронтенда, вместо '((BASE URL))' вставляйте '/'
- proxy_set_header Scheme $scheme;
- proxy_set_header Host $host;
- proxy_http_version 1.1;
- proxy_pass http://((IP)):((PORT));
- }
-}
-```
-## Как добавить свой инстанс в список?
-Чтобы это сделать, вы должны либо сделать PR, добавив в файл `instances.json` свой инстанс, либо сообщить о нём в комнате в Matrix. Думаю, он не нуждается в описании. Однако учтите, у этого списка есть пара правил:
-1. Инстанс не должен использовать Cloudflare.
+SkunkyArt 🦨 — альтернативный фронтенд к DeviantArt, который полностью работает без JS (JavaScript).
+## Сборка
+Рекомендуется производить сборку с тегом 'embed', поскольку он встраивает заготовки в бинарный файл. Если вы планируете изменять заготовки, то не используйте этот тег. Также вы можете добавить аргумент `-ldflags "-w -s"` (у GCCGO он называется по-другому — `gccgoflags`) для уменьшения размера выходного файла. Вот пример:
+
+`go build -tags embed -ldflags "-w -s"`
+
+Готовые бинари находятся во вкладке [Releases](https://git.macaw.me/skunky/skunkyart/releases).
+## Настройка
+Пример конфига находится в файле `config.example.json`. Чтобы указать свой конфиг, используйте cli-аргумент `--config`.
+См. [`SETUP-RU.md`](/skunky/SkunkyArt/src/branch/master/SETUP-RU.md) для информации о настройки фронтенда.
+## Добавление инстанса в список
+Чтобы это сделать, вы должны либо сделать PR, добавив в файлы `instances.json` и `INSTANCES.md` свой инстанс (можете воспользоваться cli-аргументом `--add-instance`, который автоматически это сделает), либо создать Issue, или сообщить о нём в комнате в Matrix. Учтите, что ваш инстанс должен соблюсти следущие правила:
+1. Инстанс не должен использовать Cloudflare итп.
2. Если ваш инстанс имеет модифицированный исходный код, то вам нужно опубликовать его на любую свободную площадку. Например, Github и Gitlab таковыми не являются.
## Благодарности
-* [Лис⚛](https://go.kde.org/matrix/#/@fox:matrix.org) -- помог разобраться в Go и много чего полезного посоветовал по этому языку.
\ No newline at end of file
+* [vlnst](https://git.bloat.cat/vlnst) — написал Docker-файл.
+* [Лис⚛](https://go.kde.org/matrix/#/@fox:matrix.org) — помог разобраться в Go и много чего полезного посоветовал по этому языку.
+* [meoww](https://codeberg.org/meoww) — перевела некоторые предложения на английский язык и написала сервис для openrc
\ No newline at end of file
diff --git a/REDIRECTS.md b/REDIRECTS.md
new file mode 100755
index 0000000..2469977
--- /dev/null
+++ b/REDIRECTS.md
@@ -0,0 +1,13 @@
+# Search
+* `deviantart.com/search?q=$QUERY` => `/search?q=$QUERY&type=all`
+# Daily Deviations
+* `deviantart.com` => `/dd`
+# Deviations
+* (`$USER_GROUP.deviantart.com/art/$ID`|`deviantart.com/$USER_GROUP/art/$ID`) => `/post/$USER_GROUP/$ID`
+# Groups and users
+## Main user page
+* (`$USER_GROUP.deviantart.com`|`deviantart.com/$USER_GROUP`) => `/group_user?type=about&q=$USER_GROUP`
+## Gallery
+* (`$USER_GROUP.deviantart.com/gallery`|`deviantart.com/$USER_GROUP/gallery`) => `/group_user?type=gallery&q=$USER_GROUP`
+## Favourites
+* (`$USER_GROUP.deviantart.com/favourites`|`deviantart.com/$USER_GROUP/favourites`) => `/group_user?type=favourites&q=$USER_GROUP`
diff --git a/SETUP-RU.md b/SETUP-RU.md
new file mode 100755
index 0000000..7d7d04c
--- /dev/null
+++ b/SETUP-RU.md
@@ -0,0 +1,42 @@
+[English version 🇬🇧](/skunky/SkunkyArt/src/branch/master/SETUP.md)
+
+# Единицы измерения
+Размер файла в кеше измеряется в мегабайтах.
+Единицы времени:
+* `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));
+ }
+}
+```
\ No newline at end of file
diff --git a/SETUP.md b/SETUP.md
new file mode 100755
index 0000000..cc89118
--- /dev/null
+++ b/SETUP.md
@@ -0,0 +1,42 @@
+[Версия на русском языке 🇷🇺](/skunky/SkunkyArt/src/branch/master/SETUP-RU.md)
+
+# Units
+Maximum file size in megabytes, requires numeric value.
+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));
+ }
+}
+```
\ No newline at end of file
diff --git a/TODO.md b/TODO.md
old mode 100644
new mode 100755
index 2334695..01bc524
--- a/TODO.md
+++ b/TODO.md
@@ -1,7 +1,23 @@
# v1.3.x
-* Доделать парсинг описания
-* Реализовать миниатюры и оптимизировать CSS под маленькие экраны
+* Почистить говнокод
+* Добавить фильтры поиска
+* ~~Сделать порт под FreeBSD~~ ✔️
+* **Доделать парсинг описания**
+* ~~Реализовать стрипы в ежедневных артах~~ ✔️
+* ~~Исправить баг с навигацией по страницам~~ ✔️
+* ~~Сделать нормальное отображение ошибок~~ ✔️
+* ~~Сделать единицы в конфиге более понятными~~ ✔️
+* Добавить чекер инстанса на работоспособность
+* ~~Добавить просмотр понравившихся артов пользователю~~ ✔️
+* Добавить возможность включить темплейты в бинарник [P]
+* ~~Реализовать миниатюры и оптимизировать CSS под маленькие экраны~~ ✔️
+* Написать Makefile и скрипт для автоматического развёртывания инстанса
+* Исправить баг с эмоджи, когда некоторые кастомные эмоции могут не отображаться
+* ~~Добавить аргумент &filename, который будет выдавать файл с нормально выглядещем именем~~ ✔️
+* ~~Улучшить систему кеширования: добавить рейтинг для удаления и копирование изображений в ОЗУ~~ ✔️
# v1.4
+* Реализовать API
* Реализовать темы
+* Перейти на арены в кеше
* Реализовать многоязычный интерфейс
-* Реализовать API
\ No newline at end of file
+
diff --git a/app/api.go b/app/api.go
new file mode 100755
index 0000000..d2a5655
--- /dev/null
+++ b/app/api.go
@@ -0,0 +1,82 @@
+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
+ }
+}
diff --git a/app/cache.go b/app/cache.go
new file mode 100755
index 0000000..e03db67
--- /dev/null
+++ b/app/cache.go
@@ -0,0 +1,152 @@
+// 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))
+ }
+}
diff --git a/app/cli.go b/app/cli.go
new file mode 100755
index 0000000..0480bdb
--- /dev/null
+++ b/app/cli.go
@@ -0,0 +1,166 @@
+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)
+}
diff --git a/app/config.go b/app/config.go
old mode 100644
new mode 100755
index c848b9c..813453c
--- a/app/config.go
+++ b/app/config.go
@@ -3,87 +3,101 @@ package app
import (
"encoding/json"
"os"
+ "regexp"
+ "skunkyart/static"
+ "strconv"
"time"
+
+ "git.macaw.me/skunky/devianter"
)
+var Release struct {
+ Version string
+ Description string
+}
+
type cache_config struct {
Enabled bool
+ MemCache bool `json:"memcache"`
Path string
MaxSize int64 `json:"max-size"`
- Lifetime int64
+ Lifetime string
UpdateInterval int64 `json:"update-interval"`
}
type config struct {
cfg string
Listen string
- BasePath string `json:"base-path"`
+ URI string `json:"uri"`
Cache cache_config
Proxy, Nsfw bool
- DownloadProxy string `json:"download-proxy"`
- Dirs []string `json:"dirs-to-memory"`
+ UserAgent string `json:"user-agent"`
+ DownloadProxy string `json:"download-proxy"`
+ StaticPath string `json:"static-path"`
}
var CFG = config{
- cfg: "config.json",
- Listen: "127.0.0.1:3003",
- BasePath: "/",
+ cfg: "config.json",
+ Listen: "127.0.0.1:3003",
+ URI: "/",
Cache: cache_config{
- Enabled: true,
+ Enabled: false,
Path: "cache",
UpdateInterval: 1,
},
- Dirs: []string{"html", "css"},
- Proxy: true,
- Nsfw: true,
+ StaticPath: "static",
+ 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,
+ Nsfw: true,
}
+var lifetimeParsed int64
+
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 != "" {
f, err := os.ReadFile(CFG.cfg)
- try_with_exitstatus(err, 1)
-
- try_with_exitstatus(json.Unmarshal(f, &CFG), 1)
+ tryWithExitStatus(err, 1)
+ tryWithExitStatus(json.Unmarshal(f, &CFG), 1)
if CFG.Cache.Enabled && !CFG.Proxy {
exit("Incompatible settings detected: cannot use caching media content without proxy", 1)
}
- if CFG.Cache.MaxSize != 0 || CFG.Cache.Lifetime != 0 {
+ if CFG.Cache.Enabled {
+ 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()
}
+
+ About = instanceAbout{
+ Proxy: CFG.Proxy,
+ Nsfw: CFG.Nsfw,
+ }
+
+ static.StaticPath = CFG.StaticPath
+ devianter.UserAgent = CFG.UserAgent
}
}
diff --git a/app/parsers.go b/app/parsers.go
old mode 100644
new mode 100755
index 0665476..192a88d
--- a/app/parsers.go
+++ b/app/parsers.go
@@ -9,7 +9,11 @@ import (
"golang.org/x/net/html"
)
-func (s skunkyart) ParseComments(c devianter.Comments) string {
+func (s skunkyart) ParseComments(c devianter.Comments, daError devianter.Error) string {
+ if daError.RAW != nil {
+ return "Failed to fetch comments :("
+ }
+
var cmmts strings.Builder
replied := make(map[int]string)
@@ -41,7 +45,9 @@ func (s skunkyart) ParseComments(c devianter.Comments) string {
cmmts.WriteString(" ")
if x.Parent > 0 {
- cmmts.WriteString(` In reply to `)
if replied[x.Parent] == "" {
@@ -70,17 +76,82 @@ func (s skunkyart) ParseComments(c devianter.Comments) string {
return cmmts.String()
}
-func (s skunkyart) DeviationList(devs []devianter.Deviation, content ...DeviationList) string {
- var list strings.Builder
+func (s skunkyart) DeviationList(devs []devianter.Deviation, allowAtom bool, content ...DeviationList) string {
if s.Atom && s.Page > 1 {
s.ReturnHTTPError(400)
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(``)
+ listContent.WriteString(data.Author.Username)
+ listContent.WriteString(``)
+ listContent.WriteString(data.Title)
+ listContent.WriteString(``)
+ listContent.WriteString(id)
+ listContent.WriteString(``)
+ listContent.WriteString(data.PublishedTime.UTC().Format("Mon, 02 Jan 2006 15:04:05 -0700"))
+ listContent.WriteString(``)
+ listContent.WriteString(``)
+ listContent.WriteString(data.Title)
+ listContent.WriteString(`