mirror of
https://codeberg.org/video-prize-ranch/rimgo.git
synced 2026-02-15 04:55:59 +00:00
Compare commits
51 Commits
v1.2.2
...
permissive
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b78ed44730 | ||
|
|
c208a55f40 | ||
|
|
4441d25d38 | ||
|
|
975ffa0b9c | ||
|
|
7b1314fae3 | ||
|
|
fd704f53e7 | ||
|
|
bf849e1cbc | ||
|
|
cd4a36c9f7 | ||
|
|
04fbc7f5f4 | ||
|
|
189ebeefde | ||
|
|
3f40c25b04 | ||
|
|
33fa04e98d | ||
|
|
e5b87dc924 | ||
|
|
8cb2524924 | ||
|
|
23b66cba47 | ||
|
|
eabb7d9917 | ||
|
|
3b8ad3f360 | ||
|
|
e8ed026962 | ||
|
|
107a45e58b | ||
|
|
751616d967 | ||
|
|
09222a116f | ||
|
|
42174c052d | ||
|
|
c22365100d | ||
|
|
26edb6a385 | ||
|
|
dc57521bfa | ||
|
|
44ce694b5b | ||
|
|
2cef3880c7 | ||
|
|
061f9b0ebe | ||
|
|
7811070f6d | ||
|
|
248782a236 | ||
|
|
1a0016cec3 | ||
|
|
af919226ed | ||
|
|
3d2b0eeb2a | ||
|
|
2cb5bcebf3 | ||
|
|
4e3022ce1a | ||
|
|
65e4a5f08e | ||
|
|
0f1fbc2401 | ||
|
|
4afb7cefe7 | ||
|
|
20715f53af | ||
|
|
ac9582df0a | ||
|
|
0ce85ed2bd | ||
|
|
09a76779c9 | ||
|
|
337796b9be | ||
|
|
927ea20fad | ||
|
|
7433265991 | ||
|
|
fb82afc7dd | ||
|
|
1579e59dca | ||
|
|
6d528c7d0a | ||
|
|
9a6173a662 | ||
|
|
c366f95f6b | ||
|
|
f5e1669a61 |
@@ -2,6 +2,7 @@ ADDRESS=0.0.0.0
|
||||
PORT=3000
|
||||
FIBER_PREFORK=false
|
||||
IMGUR_CLIENT_ID=546c25a59c58ad7
|
||||
SECURE=true
|
||||
|
||||
# Instance privacy
|
||||
# For more information, see https://codeberg.org/librarian/librarian/wiki/Instance-privacy
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,3 +2,4 @@
|
||||
tmp
|
||||
static/app.css
|
||||
dist/
|
||||
node_modules
|
||||
@@ -1,6 +1,7 @@
|
||||
version: 2
|
||||
before:
|
||||
hooks:
|
||||
- tailwindcss -i static/tailwind.css -o static/app.css -m
|
||||
- pnpm run build
|
||||
- go mod tidy
|
||||
project_name: rimgo
|
||||
builds:
|
||||
@@ -16,24 +17,26 @@ builds:
|
||||
ldflags:
|
||||
- -X codeberg.org/rimgo/rimgo/pages.VersionInfo={{.Version}}
|
||||
archives:
|
||||
- format: tar.gz
|
||||
- formats: tar.gz
|
||||
name_template: >-
|
||||
{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}{{- if .Arm }}v{{ .Arm }}{{ end }}
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
formats: zip
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
kos:
|
||||
- repository: codeberg.org/rimgo/rimgo
|
||||
- repositories:
|
||||
- codeberg.org/rimgo/rimgo
|
||||
- codeberg.org/video-prize-ranch/rimgo
|
||||
tags:
|
||||
- '{{.Version}}'
|
||||
- latest
|
||||
- '{{.Version}}'
|
||||
- latest
|
||||
bare: true
|
||||
preserve_import_paths: false
|
||||
platforms:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
sbom: none
|
||||
gitea_urls:
|
||||
api: https://codeberg.org/api/v1
|
||||
|
||||
@@ -3,14 +3,14 @@ FROM --platform=$BUILDPLATFORM golang:alpine AS build
|
||||
ARG TARGETARCH
|
||||
|
||||
WORKDIR /src
|
||||
RUN apk --no-cache add ca-certificates git nodejs npm
|
||||
RUN apk --no-cache add ca-certificates git nodejs pnpm
|
||||
COPY . .
|
||||
|
||||
RUN npx tailwindcss -i static/tailwind.css -o static/app.css -m
|
||||
RUN pnpm install && pnpm run build
|
||||
RUN go mod download
|
||||
RUN GOOS=linux GOARCH=$TARGETARCH CGO_ENABLED=0 go build -ldflags "-X codeberg.org/rimgo/rimgo/pages.VersionInfo=$(date '+%Y-%m-%d')-$(git rev-list --abbrev-commit -1 HEAD)"
|
||||
|
||||
FROM scratch as bin
|
||||
FROM scratch AS bin
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
|
||||
13
Justfile
13
Justfile
@@ -1,7 +1,14 @@
|
||||
build:
|
||||
tailwindcss -i static/tailwind.css -o static/app.css -m
|
||||
pnpm run build
|
||||
CGO_ENABLED=0 go build -o rimgo -ldflags "-X codeberg.org/rimgo/rimgo/pages.VersionInfo=$(date '+%Y-%m-%d')-$(git rev-list --abbrev-commit -1 HEAD)"
|
||||
|
||||
dev-css:
|
||||
pnpm run watch
|
||||
|
||||
dev:
|
||||
tailwindcss -i static/tailwind.css -o static/app.css -m -w &
|
||||
go run github.com/cosmtrek/air@latest -c .air.toml
|
||||
go run github.com/air-verse/air@latest -c .air.toml
|
||||
|
||||
tag-vpr:
|
||||
podman pull codeberg.org/rimgo/rimgo:latest
|
||||
podman image tag codeberg.org/rimgo/rimgo:latest codeberg.org/video-prize-ranch/rimgo:latest
|
||||
podman push codeberg.org/video-prize-ranch/rimgo:latest
|
||||
@@ -17,8 +17,6 @@ An alternative frontend for Imgur. Originally based on [rimgu](https://codeberg.
|
||||
- [Privacy](#privacy)
|
||||
- [Usage](#usage)
|
||||
- [Instances](#instances)
|
||||
- [Clearnet](#clearnet)
|
||||
- [Tor](#tor)
|
||||
- [Contributing](#contributing)
|
||||
- [License](#license)
|
||||
|
||||
@@ -28,7 +26,7 @@ Our new documentation is now available at [https://rimgo.codeberg.page/docs/](ht
|
||||
|
||||
- [Install](https://rimgo.codeberg.page/docs/getting-started/install/)
|
||||
- [Configuration](https://rimgo.codeberg.page/docs/usage/configuration/)
|
||||
- [Redirection](https://rimgo.codeberg.page/docs/usage/configuration/)
|
||||
- [Redirection](https://rimgo.codeberg.page/docs/usage/redirection/)
|
||||
- [Instance privacy](https://rimgo.codeberg.page/docs/usage/instance-privacy/)
|
||||
|
||||
## Features
|
||||
@@ -52,12 +50,12 @@ Tested using [Google PageSpeed Insights](https://pagespeed.web.dev/).
|
||||
| Time to Interactive | 1.6s | 23.8s |
|
||||
|
||||
### Privacy
|
||||
Imgur collects information about your device and uses tracking cookies for advertising, this is mentioned in their [privacy policy](https://imgur.com/privacy/). [Blacklight](https://themarkup.org/blacklight) found 31 trackers and 87 third-party cookies.
|
||||
Imgur collects information about your device and uses tracking cookies for advertising, this is mentioned in their [privacy policy](https://imgur.com/privacy/). [Blacklight](https://themarkup.org/blacklight) found 84 trackers and 264 third-party cookies.
|
||||
|
||||
See what cookies and trackers Imgur uses and where your data gets sent: https://themarkup.org/blacklight?url=imgur.com
|
||||
|
||||
## Usage
|
||||
Replace imgur.com or i.imgur.com with the instance domain. For i.stack.imgur.com, replace i.stack.imgur.com with the instance domain and add stack/ before the media ID. You can use a browser extension to do this [automatically](#automatically-redirect-links).
|
||||
Replace imgur.com or i.imgur.com with the instance domain. For i.stack.imgur.com, replace i.stack.imgur.com with the instance domain and add stack/ before the media ID.
|
||||
|
||||
Imgur: `https://imgur.com/gallery/j2sOQkJ` -> `https://rimgo.bcow.xyz/gallery/j2sOQkJ`
|
||||
Stack Overflow: `https://i.stack.imgur.com/KnO3v.jpg?s=64&g=1` -> `https://rimgo.bcow.xyz/stack/KnO3v.jpg?s=64&g=1`
|
||||
|
||||
@@ -7,15 +7,15 @@ import (
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
ClientID string
|
||||
Cache *cache.Cache
|
||||
ClientID string
|
||||
Cache *cache.Cache
|
||||
}
|
||||
|
||||
func NewClient(clientId string) (*Client) {
|
||||
client := Client{
|
||||
ClientID: clientId,
|
||||
Cache: cache.New(15*time.Minute, 15*time.Minute),
|
||||
}
|
||||
|
||||
return &client
|
||||
}
|
||||
func NewClient(clientId string) *Client {
|
||||
client := Client{
|
||||
ClientID: clientId,
|
||||
Cache: cache.New(15*time.Minute, 15*time.Minute),
|
||||
}
|
||||
|
||||
return &client
|
||||
}
|
||||
|
||||
@@ -11,19 +11,19 @@ import (
|
||||
)
|
||||
|
||||
type SearchResult struct {
|
||||
Id string
|
||||
Url string
|
||||
ImageUrl string
|
||||
Title string
|
||||
User string
|
||||
Points string
|
||||
Views string
|
||||
RelTime string
|
||||
Id string
|
||||
Url string
|
||||
ImageUrl string
|
||||
Title string
|
||||
User string
|
||||
Points string
|
||||
Views string
|
||||
RelTime string
|
||||
}
|
||||
|
||||
func (client *Client) Search(query string, page string) ([]SearchResult, error) {
|
||||
query = url.QueryEscape(query)
|
||||
req, err := http.NewRequest("GET", "https://imgur.com/search/all/page/" + page + "?scrolled&q_size_is_mpx=off&qs=list&q=" + query, nil)
|
||||
req, err := http.NewRequest("GET", "https://imgur.com/search/all/page/"+page+"?scrolled&q_size_is_mpx=off&qs=list&q="+query, nil)
|
||||
if err != nil {
|
||||
return []SearchResult{}, err
|
||||
}
|
||||
@@ -35,16 +35,16 @@ func (client *Client) Search(query string, page string) ([]SearchResult, error)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != 200 {
|
||||
return []SearchResult{}, fmt.Errorf("invalid status code, got %d", res.StatusCode)
|
||||
}
|
||||
return []SearchResult{}, fmt.Errorf("invalid status code, got %d", res.StatusCode)
|
||||
}
|
||||
|
||||
doc, err := goquery.NewDocumentFromReader(res.Body)
|
||||
if err != nil {
|
||||
return []SearchResult{}, err
|
||||
}
|
||||
if err != nil {
|
||||
return []SearchResult{}, err
|
||||
}
|
||||
|
||||
results := []SearchResult{}
|
||||
doc.Find(".post-list").Each(func(i int, s *goquery.Selection) {
|
||||
doc.Find(".post-list").Each(func(i int, s *goquery.Selection) {
|
||||
url, _ := s.Find("a").Attr("href")
|
||||
imageUrl, _ := s.Find("img").Attr("src")
|
||||
|
||||
@@ -55,18 +55,18 @@ func (client *Client) Search(query string, page string) ([]SearchResult, error)
|
||||
views = strings.TrimSuffix(views, " views")
|
||||
|
||||
result := SearchResult{
|
||||
Id: strings.Split(url, "/")[2],
|
||||
Url: url,
|
||||
Id: strings.Split(url, "/")[2],
|
||||
Url: url,
|
||||
ImageUrl: strings.ReplaceAll(imageUrl, "//i.imgur.com", ""),
|
||||
Title: s.Find(".search-item-title a").Text(),
|
||||
User: s.Find(".account").Text(),
|
||||
Views: views,
|
||||
Points: points,
|
||||
RelTime: strings.TrimSpace(postInfo[2]),
|
||||
Title: s.Find(".search-item-title a").Text(),
|
||||
User: s.Find(".account").Text(),
|
||||
Views: views,
|
||||
Points: points,
|
||||
RelTime: strings.TrimSpace(postInfo[2]),
|
||||
}
|
||||
|
||||
results = append(results, result)
|
||||
})
|
||||
|
||||
return results, nil
|
||||
}
|
||||
}
|
||||
|
||||
68
api/tag.go
68
api/tag.go
@@ -1,11 +1,11 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
|
||||
"github.com/patrickmn/go-cache"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
@@ -21,6 +21,9 @@ type Tag struct {
|
||||
}
|
||||
|
||||
func (client *Client) FetchTag(tag string, sort string, page string) (Tag, error) {
|
||||
// Dots are automatically removed on Imgur, so more cache hits
|
||||
tag = strings.ReplaceAll(tag, ".", "")
|
||||
|
||||
cacheData, found := client.Cache.Get(tag + sort + page + "-tag")
|
||||
if found {
|
||||
return cacheData.(Tag), nil
|
||||
@@ -57,54 +60,51 @@ func (client *Client) FetchTag(tag string, sort string, page string) (Tag, error
|
||||
return Tag{}, err
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return Tag{}, err
|
||||
}
|
||||
|
||||
data := gjson.Parse(string(body))
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
posts := make([]Submission, 0)
|
||||
data.Get("posts").ForEach(
|
||||
func(key, value gjson.Result) bool {
|
||||
wg.Add(1)
|
||||
url, _ := url.Parse(strings.ReplaceAll(value.Get("url").String(), "https://imgur.com", ""))
|
||||
q := url.Query()
|
||||
q.Add("tag", tag+"."+sort+"."+page+"."+key.String())
|
||||
url.RawQuery = q.Encode()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
posts = append(posts, Submission{
|
||||
Id: value.Get("id").String(),
|
||||
Title: value.Get("title").String(),
|
||||
Link: strings.ReplaceAll(value.Get("url").String(), "https://imgur.com", ""),
|
||||
Cover: Media{
|
||||
Id: value.Get("cover_id").String(),
|
||||
Type: value.Get("cover.type").String(),
|
||||
Url: strings.ReplaceAll(value.Get("cover.url").String(), "https://i.imgur.com", ""),
|
||||
},
|
||||
Points: value.Get("point_count").Int(),
|
||||
Upvotes: value.Get("upvote_count").Int(),
|
||||
Downvotes: value.Get("downvote_count").Int(),
|
||||
Comments: value.Get("comment_count").Int(),
|
||||
Views: value.Get("view_count").Int(),
|
||||
IsAlbum: value.Get("is_album").Bool(),
|
||||
})
|
||||
}()
|
||||
posts = append(posts, Submission{
|
||||
Id: value.Get("id").String(),
|
||||
Title: value.Get("title").String(),
|
||||
Link: url.String(),
|
||||
Cover: Media{
|
||||
Id: value.Get("cover_id").String(),
|
||||
Type: value.Get("cover.type").String(),
|
||||
Url: strings.ReplaceAll(value.Get("cover.url").String(), "https://i.imgur.com", ""),
|
||||
},
|
||||
Points: value.Get("point_count").Int(),
|
||||
Upvotes: value.Get("upvote_count").Int(),
|
||||
Downvotes: value.Get("downvote_count").Int(),
|
||||
Comments: value.Get("comment_count").Int(),
|
||||
Views: value.Get("view_count").Int(),
|
||||
IsAlbum: value.Get("is_album").Bool(),
|
||||
})
|
||||
|
||||
return true
|
||||
},
|
||||
)
|
||||
|
||||
wg.Wait()
|
||||
|
||||
tagData := Tag{
|
||||
Tag: tag,
|
||||
Display: data.Get("display").String(),
|
||||
Sort: sort,
|
||||
PostCount: data.Get("post_count").Int(),
|
||||
Posts: posts,
|
||||
Background: "/" + data.Get("background_id").String() + ".webp",
|
||||
Tag: tag,
|
||||
Display: data.Get("display").String(),
|
||||
Sort: sort,
|
||||
PostCount: data.Get("post_count").Int(),
|
||||
Posts: posts,
|
||||
Background: "/" + data.Get("background_id").String() + ".webp",
|
||||
}
|
||||
|
||||
client.Cache.Set(tag + sort + page + "-tag", tagData, cache.DefaultExpiration)
|
||||
client.Cache.Set(tag+sort+page+"-tag", tagData, 4*cache.DefaultExpiration)
|
||||
return tagData, nil
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
rimgo:
|
||||
image: codeberg.org/rimgo/rimgo # Official image
|
||||
|
||||
41
go.mod
41
go.mod
@@ -1,43 +1,28 @@
|
||||
module codeberg.org/rimgo/rimgo
|
||||
|
||||
go 1.21
|
||||
|
||||
toolchain go1.21.4
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/PuerkitoBio/goquery v1.8.1
|
||||
github.com/aymerick/raymond v2.0.2+incompatible
|
||||
github.com/PuerkitoBio/goquery v1.11.0
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
github.com/gofiber/fiber/v2 v2.51.0
|
||||
github.com/gofiber/template/handlebars/v2 v2.1.6
|
||||
github.com/gorilla/feeds v1.2.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/microcosm-cc/bluemonday v1.0.26
|
||||
github.com/mailgun/raymond/v2 v2.0.48
|
||||
github.com/microcosm-cc/bluemonday v1.0.27
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/tidwall/gjson v1.17.0
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
gitlab.com/golang-commonmark/linkify v0.0.0-20200225224916-64bca66f6ad3
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.0.6 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.2 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/gofiber/template v1.8.2 // indirect
|
||||
github.com/gofiber/utils v1.1.0 // indirect
|
||||
github.com/google/uuid v1.4.0 // indirect
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/klauspost/compress v1.17.3 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/philhofer/fwd v1.1.2 // indirect
|
||||
github.com/rivo/uniseg v0.4.4 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/stretchr/testify v1.10.0 // indirect
|
||||
github.com/tidwall/match v1.2.0 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tinylib/msgp v1.1.9 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.51.0 // indirect
|
||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||
golang.org/x/net v0.18.0 // indirect
|
||||
golang.org/x/sys v0.14.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
)
|
||||
|
||||
138
go.sum
138
go.sum
@@ -1,110 +1,128 @@
|
||||
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
|
||||
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
|
||||
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
|
||||
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
||||
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
|
||||
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
||||
github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw=
|
||||
github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ=
|
||||
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/aymerick/raymond v2.0.2+incompatible h1:VEp3GpgdAnv9B2GFyTvqgcKvY+mfKMjPOA3SbKLtnU0=
|
||||
github.com/aymerick/raymond v2.0.2+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/gofiber/fiber/v2 v2.51.0 h1:JNACcZy5e2tGApWB2QrRpenTWn0fq0hkFm6k0C86gKQ=
|
||||
github.com/gofiber/fiber/v2 v2.51.0/go.mod h1:xaQRZQJGqnKOQnbQw+ltvku3/h8QxvNi8o6JiJ7Ll0U=
|
||||
github.com/gofiber/template v1.8.2 h1:PIv9s/7Uq6m+Fm2MDNd20pAFFKt5wWs7ZBd8iV9pWwk=
|
||||
github.com/gofiber/template v1.8.2/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8=
|
||||
github.com/gofiber/template/handlebars/v2 v2.1.6 h1:+z9S2/L3RZueHpRtjxyMGpNHKIMUYkvyVEGHQrau+Po=
|
||||
github.com/gofiber/template/handlebars/v2 v2.1.6/go.mod h1:Kes9qTj4iD73xj1bq94HjJHNqhUhuyWpLvs9fyH5aMs=
|
||||
github.com/gofiber/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM=
|
||||
github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0=
|
||||
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
|
||||
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||
github.com/gorilla/feeds v1.2.0 h1:O6pBiXJ5JHhPvqy53NsjKOThq+dNFm8+DFrxBEdzSCc=
|
||||
github.com/gorilla/feeds v1.2.0/go.mod h1:WMib8uJP3BbY+X8Szd1rA5Pzhdfh+HCCAYT2z7Fza6Y=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/klauspost/compress v1.17.3 h1:qkRjuerhUU1EmXLYGkSH6EZL+vPSxIrYjLNAK4slzwA=
|
||||
github.com/klauspost/compress v1.17.3/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
|
||||
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw=
|
||||
github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
|
||||
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM=
|
||||
github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
|
||||
github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tinylib/msgp v1.1.9 h1:SHf3yoO2sGA0veCJeCBYLHuttAVFHGm2RHgNodW7wQU=
|
||||
github.com/tinylib/msgp v1.1.9/go.mod h1:BCXGB54lDD8qUEPmiG0cQQUANC4IUQyB2ItS2UDlO/k=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
|
||||
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
|
||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
gitlab.com/golang-commonmark/linkify v0.0.0-20200225224916-64bca66f6ad3 h1:1Coh5BsUBlXoEJmIEaNzVAWrtg9k7/eJzailMQr1grw=
|
||||
gitlab.com/golang-commonmark/linkify v0.0.0-20200225224916-64bca66f6ad3/go.mod h1:Gn+LZmCrhPECMD3SOKlE+BOHwhOYD9j7WT9NUtkCrC8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg=
|
||||
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
|
||||
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
222
main.go
222
main.go
@@ -3,23 +3,37 @@ package main
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
"strings"
|
||||
|
||||
"codeberg.org/rimgo/rimgo/pages"
|
||||
"codeberg.org/rimgo/rimgo/render"
|
||||
"codeberg.org/rimgo/rimgo/static"
|
||||
"codeberg.org/rimgo/rimgo/utils"
|
||||
"codeberg.org/rimgo/rimgo/views"
|
||||
"github.com/aymerick/raymond"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/cache"
|
||||
"github.com/gofiber/fiber/v2/middleware/filesystem"
|
||||
"github.com/gofiber/fiber/v2/middleware/recover"
|
||||
"github.com/gofiber/template/handlebars/v2"
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
// a handler that returns error if it can't respond
|
||||
type handler func(w http.ResponseWriter, r *http.Request) error
|
||||
|
||||
func wrapHandler(h handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
if v := recover(); v != nil {
|
||||
utils.RenderError(w, r, 500, fmt.Sprint(v))
|
||||
}
|
||||
}()
|
||||
err := h(w, r)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
utils.RenderError(w, r, 500, err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func main() {
|
||||
envPath := flag.String("c", ".env", "Path to env file")
|
||||
godotenv.Load(*envPath)
|
||||
@@ -27,113 +41,111 @@ func main() {
|
||||
|
||||
pages.InitializeApiClient()
|
||||
|
||||
views := http.FS(views.GetFiles())
|
||||
if os.Getenv("ENV") == "dev" {
|
||||
views = http.Dir("./views")
|
||||
}
|
||||
engine := handlebars.NewFileSystem(views, ".hbs")
|
||||
views := views.GetFiles()
|
||||
static := static.GetFiles()
|
||||
render.Initialize(views)
|
||||
|
||||
engine.AddFunc("noteq", func(a interface{}, b interface{}, options *raymond.Options) interface{} {
|
||||
if raymond.Str(a) != raymond.Str(b) {
|
||||
return options.Fn()
|
||||
}
|
||||
return ""
|
||||
})
|
||||
app := http.NewServeMux()
|
||||
|
||||
app := fiber.New(fiber.Config{
|
||||
Views: engine,
|
||||
Prefork: utils.Config.FiberPrefork,
|
||||
UnescapePath: true,
|
||||
StreamRequestBody: true,
|
||||
ErrorHandler: func(ctx *fiber.Ctx, err error) error {
|
||||
code := fiber.StatusInternalServerError
|
||||
|
||||
if e, ok := err.(*fiber.Error); ok {
|
||||
code = e.Code
|
||||
}
|
||||
|
||||
err = ctx.Status(code).Render("errors/error", fiber.Map{
|
||||
"err": err,
|
||||
})
|
||||
if err != nil {
|
||||
return ctx.Status(fiber.StatusInternalServerError).SendString("Internal Server Error")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
app.Use(recover.New(recover.Config{
|
||||
EnableStackTrace: true,
|
||||
StackTraceHandler: func(c *fiber.Ctx, e interface{}) {
|
||||
fmt.Println(e)
|
||||
},
|
||||
app.Handle("GET /static/", http.StripPrefix("/static/", http.FileServerFS(static)))
|
||||
app.Handle("GET /robots.txt", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
file, _ := static.Open("robots.txt")
|
||||
defer file.Close()
|
||||
io.Copy(w, file)
|
||||
}))
|
||||
app.Handle("GET /favicon.ico", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
file, _ := static.Open("favicon/favicon.ico")
|
||||
defer file.Close()
|
||||
io.Copy(w, file)
|
||||
}))
|
||||
|
||||
if os.Getenv("ENV") == "dev" {
|
||||
app.Use("/static", filesystem.New(filesystem.Config{
|
||||
Root: http.Dir("./static"),
|
||||
app.Handle("GET /errors/429", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
utils.RenderError(w, r, 429)
|
||||
}))
|
||||
app.Get("/errors/429", func(c *fiber.Ctx) error {
|
||||
return c.Render("errors/429", nil)
|
||||
})
|
||||
app.Get("/errors/404", func(c *fiber.Ctx) error {
|
||||
return c.Render("errors/404", nil)
|
||||
})
|
||||
app.Get("/errors/error", func(c *fiber.Ctx) error {
|
||||
return c.Render("errors/error", fiber.Map{
|
||||
"err": "Test error",
|
||||
})
|
||||
})
|
||||
} else {
|
||||
app.Use("/static", filesystem.New(filesystem.Config{
|
||||
MaxAge: 2592000,
|
||||
Root: http.FS(static.GetFiles()),
|
||||
app.Handle("GET /errors/429/img", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Location", "/static/img/error-429.png")
|
||||
w.WriteHeader(302)
|
||||
}))
|
||||
app.Use(cache.New(cache.Config{
|
||||
Expiration: 30 * time.Minute,
|
||||
MaxBytes: 25000000,
|
||||
KeyGenerator: func(c *fiber.Ctx) string {
|
||||
return c.OriginalURL()
|
||||
},
|
||||
CacheControl: true,
|
||||
StoreResponseHeaders: true,
|
||||
app.Handle("GET /errors/404", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
utils.RenderError(w, r, 404)
|
||||
}))
|
||||
app.Handle("GET /errors/404/img", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Location", "/static/img/error-404.png")
|
||||
w.WriteHeader(302)
|
||||
}))
|
||||
app.Handle("GET /errors/error", wrapHandler(func(w http.ResponseWriter, r *http.Request) error {
|
||||
return fmt.Errorf("Test error")
|
||||
}))
|
||||
app.Handle("GET /errors/panic", wrapHandler(func(w http.ResponseWriter, r *http.Request) error {
|
||||
panic("Test error")
|
||||
}))
|
||||
}
|
||||
|
||||
app.Get("/robots.txt", func(c *fiber.Ctx) error {
|
||||
file, _ := static.GetFiles().ReadFile("robots.txt")
|
||||
_, err := c.Write(file)
|
||||
app.Handle("GET /{$}", wrapHandler(pages.HandleFrontpage))
|
||||
app.Handle("GET /a/{postID}", wrapHandler(pages.HandlePost))
|
||||
app.Handle("GET /a/{postID}/embed", wrapHandler(pages.HandleEmbed))
|
||||
app.Handle("GET /t/{tag}", wrapHandler(func(w http.ResponseWriter, r *http.Request) error {
|
||||
name, ext := utils.SplitNameExt(r.PathValue("tag"))
|
||||
if ext != "" {
|
||||
r.SetPathValue("tag", name[0:len(name)-1])
|
||||
r.SetPathValue("type", ext)
|
||||
return pages.HandleTagRSS(w, r)
|
||||
}
|
||||
return pages.HandleTag(w, r)
|
||||
}))
|
||||
app.Handle("GET /t/{tag}/{postID}", wrapHandler(pages.HandlePost))
|
||||
app.Handle("GET /r/{sub}/{postID}", wrapHandler(pages.HandlePost))
|
||||
app.Handle("GET /user/{userID}", wrapHandler(func(w http.ResponseWriter, r *http.Request) error {
|
||||
name, ext := utils.SplitNameExt(r.PathValue("userID"))
|
||||
if ext != "" {
|
||||
r.SetPathValue("userID", name[0:len(name)-1])
|
||||
r.SetPathValue("type", ext)
|
||||
return pages.HandleUserRSS(w, r)
|
||||
}
|
||||
return pages.HandleUser(w, r)
|
||||
}))
|
||||
app.Handle("GET /user/{userID}/favorites", wrapHandler(pages.HandleUserFavorites))
|
||||
app.Handle("GET /user/{userID}/comments", wrapHandler(pages.HandleUserComments))
|
||||
app.Handle("GET /user/{userID}/cover", wrapHandler(pages.HandleUserCover))
|
||||
app.Handle("GET /user/{userID}/avatar", wrapHandler(pages.HandleUserAvatar))
|
||||
app.Handle("GET /gallery/{postID}", wrapHandler(pages.HandlePost))
|
||||
app.Handle("GET /gallery/{postID}/embed", wrapHandler(pages.HandleEmbed))
|
||||
app.Handle("GET /{component}", wrapHandler(func(w http.ResponseWriter, r *http.Request) error {
|
||||
component := r.PathValue("component")
|
||||
switch {
|
||||
case component == "about":
|
||||
return pages.HandleAbout(w, r)
|
||||
case component == "privacy":
|
||||
return pages.HandlePrivacy(w, r)
|
||||
case component == "search":
|
||||
return pages.HandleSearch(w, r)
|
||||
case component == "trending":
|
||||
return pages.HandleTrending(w, r)
|
||||
case strings.HasPrefix(component, "trending."):
|
||||
_, ext := utils.SplitNameExt(component)
|
||||
r.SetPathValue("type", ext)
|
||||
return pages.HandleTrendingRSS(w, r)
|
||||
case strings.HasSuffix(component, ".gifv"):
|
||||
r.SetPathValue("postID", component)
|
||||
return pages.HandleGifv(w, r)
|
||||
case strings.Contains(component, "."):
|
||||
return pages.HandleMedia(w, r)
|
||||
default:
|
||||
r.SetPathValue("postID", component)
|
||||
return pages.HandlePost(w, r)
|
||||
}
|
||||
}))
|
||||
app.Handle("GET /stack/{component}", wrapHandler(pages.HandleMedia))
|
||||
// matches anything with no more specific route
|
||||
app.Handle("GET /", wrapHandler(func(w http.ResponseWriter, r *http.Request) error {
|
||||
err := render.Render(w, "errors/404", nil)
|
||||
return err
|
||||
})
|
||||
app.Get("/favicon.ico", func(c *fiber.Ctx) error {
|
||||
file, _ := static.GetFiles().ReadFile("favicon/favicon.ico")
|
||||
_, err := c.Write(file)
|
||||
return err
|
||||
})
|
||||
}))
|
||||
|
||||
app.Get("/", pages.HandleFrontpage)
|
||||
app.Get("/about", pages.HandleAbout)
|
||||
app.Get("/privacy", pages.HandlePrivacy)
|
||||
app.Get("/search", pages.HandleSearch)
|
||||
app.Get("/trending", pages.HandleTrending)
|
||||
app.Get("/a/:postID", pages.HandlePost)
|
||||
app.Get("/a/:postID/embed", pages.HandleEmbed)
|
||||
app.Get("/t/:tag", pages.HandleTag)
|
||||
app.Get("/t/:tag/:postID", pages.HandlePost)
|
||||
app.Get("/r/:sub/:postID", pages.HandlePost)
|
||||
app.Get("/user/:userID", pages.HandleUser)
|
||||
app.Get("/user/:userID/favorites", pages.HandleUserFavorites)
|
||||
app.Get("/user/:userID/comments", pages.HandleUserComments)
|
||||
app.Get("/user/:userID/cover", pages.HandleUserCover)
|
||||
app.Get("/user/:userID/avatar", pages.HandleUserAvatar)
|
||||
app.Get("/gallery/:postID", pages.HandlePost)
|
||||
app.Get("/gallery/:postID/embed", pages.HandleEmbed)
|
||||
app.Get("/:postID.gifv", pages.HandleGifv)
|
||||
app.Get("/:baseName.:extension", pages.HandleMedia)
|
||||
app.Get("/stack/:baseName.:extension", pages.HandleMedia)
|
||||
app.Get("/:postID", pages.HandlePost)
|
||||
app.Get("/:postID/embed", pages.HandleEmbed)
|
||||
|
||||
app.Listen(utils.Config.Addr + ":" + utils.Config.Port)
|
||||
addr := utils.Config.Addr + ":" + utils.Config.Port
|
||||
fmt.Println("listening on " + addr)
|
||||
err := http.ListenAndServe(addr, app)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
10
package.json
Normal file
10
package.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"@tailwindcss/cli": "^4.1.5",
|
||||
"tailwindcss": "^4.1.5"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tailwindcss -i static/tailwind.css -o static/app.css",
|
||||
"watch": "tailwindcss -i static/tailwind.css -o static/app.css --watch"
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,21 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"os"
|
||||
"net/http"
|
||||
|
||||
"codeberg.org/rimgo/rimgo/render"
|
||||
"codeberg.org/rimgo/rimgo/utils"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func HandleAbout(w http.ResponseWriter, r *http.Request) error {
|
||||
utils.SetHeaders(w)
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
w.Header().Set("Cache-Control", "public,max-age=31557600")
|
||||
w.Header().Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; style-src 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
|
||||
|
||||
func HandleAbout(c *fiber.Ctx) error {
|
||||
utils.SetHeaders(c)
|
||||
c.Set("X-Frame-Options", "DENY")
|
||||
c.Set("Cache-Control", "public,max-age=31557600")
|
||||
c.Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; style-src 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
|
||||
|
||||
return c.Render("about", fiber.Map{
|
||||
"proto": c.Protocol(),
|
||||
"domain": c.Hostname(),
|
||||
"force_webp": os.Getenv("FORCE_WEBP"),
|
||||
return render.Render(w, "about", map[string]any{
|
||||
"proto": r.Proto,
|
||||
"domain": r.Host,
|
||||
"force_webp": utils.Config.ForceWebp,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@ import (
|
||||
var ApiClient *api.Client
|
||||
|
||||
func InitializeApiClient() {
|
||||
ApiClient = api.NewClient(utils.Config.ImgurId)
|
||||
}
|
||||
ApiClient = api.NewClient(utils.Config.ImgurId)
|
||||
}
|
||||
|
||||
@@ -1,48 +1,49 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"codeberg.org/rimgo/rimgo/api"
|
||||
"codeberg.org/rimgo/rimgo/render"
|
||||
"codeberg.org/rimgo/rimgo/utils"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func HandleEmbed(c *fiber.Ctx) error {
|
||||
utils.SetHeaders(c)
|
||||
c.Set("Cache-Control", "public,max-age=31557600")
|
||||
c.Set("Content-Security-Policy", "default-src 'none'; base-uri 'none'; form-action 'none'; media-src 'self'; style-src 'self'; img-src 'self'; block-all-mixed-content")
|
||||
func HandleEmbed(w http.ResponseWriter, r *http.Request) error {
|
||||
utils.SetHeaders(w)
|
||||
w.Header().Set("Cache-Control", "public,max-age=31557600")
|
||||
w.Header().Set("Content-Security-Policy", "default-src 'none'; base-uri 'none'; form-action 'none'; media-src 'self'; style-src 'self'; img-src 'self'; block-all-mixed-content")
|
||||
|
||||
post, err := api.Album{}, error(nil)
|
||||
switch {
|
||||
case strings.HasPrefix(c.Path(), "/a"):
|
||||
post, err = ApiClient.FetchAlbum(c.Params("postID"))
|
||||
case strings.HasPrefix(c.Path(), "/gallery"):
|
||||
post, err = ApiClient.FetchPosts(c.Params("postID"))
|
||||
case strings.HasPrefix(r.URL.Path, "/a"):
|
||||
post, err = ApiClient.FetchAlbum(r.PathValue("postID"))
|
||||
case strings.HasPrefix(r.URL.Path, "/gallery"):
|
||||
post, err = ApiClient.FetchPosts(r.PathValue("postID"))
|
||||
default:
|
||||
post, err = ApiClient.FetchMedia(c.Params("postID"))
|
||||
post, err = ApiClient.FetchMedia(r.PathValue("postID"))
|
||||
}
|
||||
if err != nil && err.Error() == "ratelimited by imgur" {
|
||||
return c.Status(429).Render("errors/429", nil)
|
||||
return utils.RenderError(w, r, 429)
|
||||
}
|
||||
if err != nil && post.Id == "" && strings.Contains(err.Error(), "404") {
|
||||
return c.Status(404).Render("errors/404", nil)
|
||||
return utils.RenderError(w, r, 404)
|
||||
}
|
||||
if err != nil {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Render("embed", fiber.Map{
|
||||
return render.Render(w, "embed", map[string]any{
|
||||
"post": post,
|
||||
})
|
||||
}
|
||||
|
||||
func HandleGifv(c *fiber.Ctx) error {
|
||||
utils.SetHeaders(c)
|
||||
c.Set("Cache-Control", "public,max-age=31557600")
|
||||
c.Set("Content-Security-Policy", "default-src 'none'; base-uri 'none'; form-action 'none'; media-src 'self'; style-src 'self'; img-src 'self'; block-all-mixed-content")
|
||||
func HandleGifv(w http.ResponseWriter, r *http.Request) error {
|
||||
utils.SetHeaders(w)
|
||||
w.Header().Set("Cache-Control", "public,max-age=31557600")
|
||||
w.Header().Set("Content-Security-Policy", "default-src 'none'; base-uri 'none'; form-action 'none'; media-src 'self'; style-src 'self'; img-src 'self'; block-all-mixed-content")
|
||||
|
||||
return c.Render("gifv", fiber.Map{
|
||||
"id": c.Params("postID"),
|
||||
return render.Render(w, "gifv", map[string]any{
|
||||
"id": r.PathValue("postID"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"codeberg.org/rimgo/rimgo/render"
|
||||
"codeberg.org/rimgo/rimgo/utils"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
var VersionInfo string
|
||||
|
||||
func HandleFrontpage(c *fiber.Ctx) error {
|
||||
utils.SetHeaders(c)
|
||||
c.Set("X-Frame-Options", "DENY")
|
||||
c.Set("Cache-Control", "public,max-age=31557600")
|
||||
c.Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; style-src 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
|
||||
func HandleFrontpage(w http.ResponseWriter, r *http.Request) error {
|
||||
utils.SetHeaders(w)
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
w.Header().Set("Cache-Control", "public,max-age=31557600")
|
||||
w.Header().Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; style-src 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
|
||||
|
||||
return c.Render("frontpage", fiber.Map{
|
||||
return render.Render(w, "frontpage", map[string]any{
|
||||
"config": utils.Config,
|
||||
"version": VersionInfo,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,55 +1,73 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"codeberg.org/rimgo/rimgo/utils"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func HandleMedia(c *fiber.Ctx) error {
|
||||
c.Set("Cache-Control", "public,max-age=31557600")
|
||||
if strings.HasPrefix(c.Path(), "/stack") {
|
||||
return handleMedia(c, "https://i.stack.imgur.com/" + strings.ReplaceAll(c.Params("baseName"), "stack/", "") + "." + c.Params("extension"))
|
||||
func HandleMedia(w http.ResponseWriter, r *http.Request) error {
|
||||
w.Header().Set("Cache-Control", "public,max-age=31557600")
|
||||
w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'self'; img-src 'self'")
|
||||
splitName := strings.SplitN(r.PathValue("component"), ".", 2)
|
||||
baseName, extension := splitName[0], splitName[1]
|
||||
if strings.HasPrefix(r.URL.Path, "/stack") {
|
||||
return handleMedia(w, r, "https://i.stack.imgur.com/"+strings.ReplaceAll(baseName, "stack/", "")+"."+extension)
|
||||
} else {
|
||||
return handleMedia(c, "https://i.imgur.com/" + c.Params("baseName") + "." + c.Params("extension"))
|
||||
return handleMedia(w, r, "https://i.imgur.com/"+baseName+"."+extension)
|
||||
}
|
||||
}
|
||||
|
||||
func HandleUserCover(c *fiber.Ctx) error {
|
||||
c.Set("Cache-Control", "public,max-age=604800")
|
||||
return handleMedia(c, "https://imgur.com/user/" + c.Params("userID") + "/cover?maxwidth=2560")
|
||||
};
|
||||
func HandleUserCover(w http.ResponseWriter, r *http.Request) error {
|
||||
w.Header().Set("Cache-Control", "public,max-age=604800")
|
||||
w.Header().Set("Content-Security-Policy", "default-src 'none'")
|
||||
return handleMedia(w, r, "https://imgur.com/user/"+r.PathValue("userID")+"/cover?maxwidth=2560")
|
||||
}
|
||||
|
||||
func HandleUserAvatar(c *fiber.Ctx) error {
|
||||
c.Set("Cache-Control", "public,max-age=604800")
|
||||
return handleMedia(c, "https://imgur.com/user/" + c.Params("userID") + "/avatar")
|
||||
};
|
||||
func HandleUserAvatar(w http.ResponseWriter, r *http.Request) error {
|
||||
w.Header().Set("Cache-Control", "public,max-age=604800")
|
||||
w.Header().Set("Content-Security-Policy", "default-src 'none'")
|
||||
return handleMedia(w, r, "https://imgur.com/user/"+r.PathValue("userID")+"/avatar")
|
||||
}
|
||||
|
||||
func handleMedia(c *fiber.Ctx, url string) error {
|
||||
utils.SetHeaders(c)
|
||||
func handleMedia(w http.ResponseWriter, r *http.Request, url string) error {
|
||||
utils.SetHeaders(w)
|
||||
if !utils.Config.RestrictiveCORS {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
}
|
||||
path := r.URL.Path
|
||||
|
||||
if os.Getenv("FORCE_WEBP") == "1" && c.Query("no_webp") == "" && c.Accepts("image/webp") == "image/webp" && !strings.HasPrefix(c.Path(), "/stack") {
|
||||
if utils.Config.ForceWebp &&
|
||||
!strings.HasSuffix(path, ".webp") &&
|
||||
r.Header.Get("Sec-Fetch-Dest") == "image" &&
|
||||
r.URL.Query().Get("no_webp") == "" &&
|
||||
utils.Accepts(r, "image/webp") &&
|
||||
!strings.HasPrefix(path, "/stack") {
|
||||
url = strings.ReplaceAll(url, ".png", ".webp")
|
||||
url = strings.ReplaceAll(url, ".jpg", ".webp")
|
||||
url = strings.ReplaceAll(url, ".jpeg", ".webp")
|
||||
filename := strings.TrimPrefix(path, "/")
|
||||
w.Header().Set("Content-Disposition", mime.FormatMediaType("attachment", map[string]string{"filename*": filename}))
|
||||
}
|
||||
|
||||
if strings.HasPrefix(c.Path(), "/stack") && strings.Contains(c.OriginalURL(), "?") {
|
||||
url = url + "?" + strings.Split(c.OriginalURL(), "?")[1]
|
||||
queryStr := r.URL.Query().Encode()
|
||||
if strings.HasPrefix(path, "/stack") && queryStr != "" {
|
||||
url = url + "?" + queryStr
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
utils.SetReqHeaders(req)
|
||||
|
||||
if c.Get("Range") != "" {
|
||||
req.Header.Set("Range", c.Get("Range"))
|
||||
rng := r.URL.Query().Get("Range")
|
||||
if rng != "" {
|
||||
req.Header.Set("Range", rng)
|
||||
}
|
||||
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
@@ -57,23 +75,19 @@ func handleMedia(c *fiber.Ctx, url string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Status(res.StatusCode)
|
||||
if res.StatusCode == 404 {
|
||||
return c.Render("errors/404", fiber.Map{
|
||||
"path": c.Path(),
|
||||
})
|
||||
if res.StatusCode == 404 || strings.Contains(res.Request.URL.String(), "error/404") {
|
||||
return utils.RenderError(w, r, 404)
|
||||
} else if res.StatusCode == 429 {
|
||||
return c.Render("errors/429", fiber.Map{
|
||||
"path": c.Path(),
|
||||
})
|
||||
return utils.RenderError(w, r, 429)
|
||||
}
|
||||
|
||||
c.Set("Accept-Ranges", "bytes")
|
||||
c.Set("Content-Type", res.Header.Get("Content-Type"));
|
||||
c.Set("Content-Length", res.Header.Get("Content-Length"))
|
||||
|
||||
w.Header().Set("Accept-Ranges", "bytes")
|
||||
w.Header().Set("Content-Type", res.Header.Get("Content-Type"))
|
||||
w.Header().Set("Content-Length", res.Header.Get("Content-Length"))
|
||||
if res.Header.Get("Content-Range") != "" {
|
||||
c.Set("Content-Range", res.Header.Get("Content-Range"))
|
||||
w.Header().Set("Content-Range", res.Header.Get("Content-Range"))
|
||||
}
|
||||
|
||||
return c.SendStream(res.Body)
|
||||
}
|
||||
|
||||
_, err = io.Copy(w, res.Body)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -3,49 +3,76 @@ package pages
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"codeberg.org/rimgo/rimgo/api"
|
||||
"codeberg.org/rimgo/rimgo/render"
|
||||
"codeberg.org/rimgo/rimgo/utils"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func HandlePost(c *fiber.Ctx) error {
|
||||
utils.SetHeaders(c)
|
||||
c.Set("X-Frame-Options", "DENY")
|
||||
// Cursed function
|
||||
func nextInTag(client *api.Client, tagname, sort, page, I string) string {
|
||||
i, err := strconv.Atoi(I)
|
||||
if err != nil || i < 0 {
|
||||
return ""
|
||||
}
|
||||
tag, err := client.FetchTag(tagname, sort, page)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
if i >= len(tag.Posts)-1 {
|
||||
pageNumber, _ := strconv.Atoi(page)
|
||||
tagn, err := client.FetchTag(tagname, sort, strconv.Itoa(pageNumber+1))
|
||||
// Check length - Imgur will not return an error if there are no more posts and you request the next page
|
||||
if err != nil || len(tagn.Posts) < 1 {
|
||||
return ""
|
||||
}
|
||||
return tagn.Posts[0].Link
|
||||
}
|
||||
return tag.Posts[i+1].Link
|
||||
}
|
||||
|
||||
func HandlePost(w http.ResponseWriter, r *http.Request) error {
|
||||
utils.SetHeaders(w)
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
|
||||
postId := r.PathValue("postID")
|
||||
if strings.Contains(postId, "-") {
|
||||
postId = postId[len(postId)-7:]
|
||||
}
|
||||
|
||||
post, err := api.Album{}, error(nil)
|
||||
switch {
|
||||
case strings.HasPrefix(c.Path(), "/a"):
|
||||
post, err = ApiClient.FetchAlbum(c.Params("postID"))
|
||||
case strings.HasPrefix(c.Path(), "/gallery"):
|
||||
post, err = ApiClient.FetchPosts(c.Params("postID"))
|
||||
case strings.HasPrefix(c.Path(), "/t"):
|
||||
post, err = ApiClient.FetchPosts(c.Params("postID"))
|
||||
case strings.HasPrefix(r.URL.Path, "/a"):
|
||||
post, err = ApiClient.FetchAlbum(postId)
|
||||
case strings.HasPrefix(r.URL.Path, "/gallery"):
|
||||
post, err = ApiClient.FetchPosts(postId)
|
||||
case strings.HasPrefix(r.URL.Path, "/t"):
|
||||
post, err = ApiClient.FetchPosts(postId)
|
||||
default:
|
||||
post, err = ApiClient.FetchMedia(c.Params("postID"))
|
||||
post, err = ApiClient.FetchMedia(postId)
|
||||
}
|
||||
if err != nil && err.Error() == "ratelimited by imgur" {
|
||||
return c.Status(429).Render("errors/429", fiber.Map{
|
||||
"path": c.Path(),
|
||||
})
|
||||
return utils.RenderError(w, r, 429)
|
||||
}
|
||||
if err != nil && post.Id == "" && strings.Contains(err.Error(), "404") {
|
||||
return c.Status(404).Render("errors/404", nil)
|
||||
return utils.RenderError(w, r, 404)
|
||||
}
|
||||
if err != nil {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
comments := []api.Comment{}
|
||||
if post.SharedWithCommunity {
|
||||
c.Set("Cache-Control", "public,max-age=604800")
|
||||
comments, err = ApiClient.FetchComments(c.Params("postID"))
|
||||
w.Header().Set("Cache-Control", "public,max-age=604800")
|
||||
comments, err = ApiClient.FetchComments(postId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
c.Set("Cache-Control", "public,max-age=31557600")
|
||||
w.Header().Set("Cache-Control", "public,max-age=31557600")
|
||||
}
|
||||
|
||||
nonce := ""
|
||||
@@ -56,10 +83,18 @@ func HandlePost(c *fiber.Ctx) error {
|
||||
nonce = fmt.Sprintf("%x", b)
|
||||
csp = csp + " 'nonce-" + nonce + "'"
|
||||
}
|
||||
c.Set("Content-Security-Policy", csp)
|
||||
w.Header().Set("Content-Security-Policy", csp)
|
||||
|
||||
return c.Render("post", fiber.Map{
|
||||
var next string
|
||||
tagParam := strings.Split(r.URL.Query().Get("tag"), ".")
|
||||
if len(tagParam) == 4 {
|
||||
tag, sort, page, index := tagParam[0], tagParam[1], tagParam[2], tagParam[3]
|
||||
next = nextInTag(ApiClient, tag, sort, page, index)
|
||||
}
|
||||
|
||||
return render.Render(w, "post", map[string]any{
|
||||
"post": post,
|
||||
"next": next,
|
||||
"comments": comments,
|
||||
"nonce": nonce,
|
||||
})
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"net/http"
|
||||
|
||||
"codeberg.org/rimgo/rimgo/render"
|
||||
"codeberg.org/rimgo/rimgo/utils"
|
||||
)
|
||||
|
||||
func HandlePrivacy(c *fiber.Ctx) error {
|
||||
utils.SetHeaders(c)
|
||||
c.Set("X-Frame-Options", "DENY")
|
||||
func HandlePrivacy(w http.ResponseWriter, r *http.Request) error {
|
||||
utils.SetHeaders(w)
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
w.Header().Set("Content-Security-Policy", "default-src 'none'; form-action 'self'; style-src 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
|
||||
|
||||
return c.Render("privacy", fiber.Map{
|
||||
return render.Render(w, "privacy", map[string]any{
|
||||
"config": utils.Config,
|
||||
"version": VersionInfo,
|
||||
})
|
||||
|
||||
143
pages/rss.go
Normal file
143
pages/rss.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"mime"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"codeberg.org/rimgo/rimgo/api"
|
||||
"codeberg.org/rimgo/rimgo/utils"
|
||||
"github.com/gorilla/feeds"
|
||||
)
|
||||
|
||||
func HandleTagRSS(w http.ResponseWriter, r *http.Request) error {
|
||||
utils.SetHeaders(w)
|
||||
|
||||
tag, err := ApiClient.FetchTag(r.PathValue("tag"), r.URL.Query().Get("sort"), "1")
|
||||
if err != nil && err.Error() == "ratelimited by imgur" {
|
||||
w.WriteHeader(429)
|
||||
_, err := w.Write([]byte("rate limited by imgur"))
|
||||
return err
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tag.Display == "" {
|
||||
w.WriteHeader(404)
|
||||
_, err := w.Write([]byte("tag not found"))
|
||||
return err
|
||||
}
|
||||
|
||||
instance := utils.GetInstanceUrl(r)
|
||||
|
||||
feed := &feeds.Feed{
|
||||
Title: tag.Display + " on Imgur",
|
||||
Link: &feeds.Link{Href: instance + "/t/" + r.PathValue("tag")},
|
||||
Created: time.Now(),
|
||||
}
|
||||
|
||||
return handleFeed(w, r, instance, feed, tag.Posts)
|
||||
}
|
||||
|
||||
func HandleTrendingRSS(w http.ResponseWriter, r *http.Request) error {
|
||||
utils.SetHeaders(w)
|
||||
|
||||
section := r.URL.Query().Get("section")
|
||||
switch section {
|
||||
case "hot", "new", "top":
|
||||
default:
|
||||
section = "hot"
|
||||
}
|
||||
sort := r.URL.Query().Get("sort")
|
||||
switch sort {
|
||||
case "newest", "best", "popular":
|
||||
default:
|
||||
sort = "popular"
|
||||
}
|
||||
|
||||
results, err := ApiClient.FetchTrending(section, sort, "1")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
instance := utils.GetInstanceUrl(r)
|
||||
|
||||
feed := &feeds.Feed{
|
||||
Title: "Trending on Imgur",
|
||||
Link: &feeds.Link{Href: instance + "/trending"},
|
||||
Created: time.Now(),
|
||||
}
|
||||
|
||||
return handleFeed(w, r, instance, feed, results)
|
||||
}
|
||||
|
||||
func HandleUserRSS(w http.ResponseWriter, r *http.Request) error {
|
||||
utils.SetHeaders(w)
|
||||
|
||||
user := r.PathValue("userID")
|
||||
|
||||
submissions, err := ApiClient.FetchSubmissions(user, "newest", "1")
|
||||
if err != nil && err.Error() == "ratelimited by imgur" {
|
||||
return utils.RenderError(w, r, 429)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
instance := utils.GetInstanceUrl(r)
|
||||
|
||||
feed := &feeds.Feed{
|
||||
Title: user + " on Imgur",
|
||||
Link: &feeds.Link{Href: instance + "/user/" + user},
|
||||
Created: time.Now(),
|
||||
}
|
||||
|
||||
return handleFeed(w, r, instance, feed, submissions)
|
||||
}
|
||||
|
||||
func handleFeed(w http.ResponseWriter, r *http.Request, instance string, feed *feeds.Feed, posts []api.Submission) error {
|
||||
feed.Items = []*feeds.Item{}
|
||||
|
||||
for _, post := range posts {
|
||||
link := instance + post.Link
|
||||
|
||||
item := &feeds.Item{
|
||||
Title: post.Title,
|
||||
Link: &feeds.Link{Href: link},
|
||||
Description: "<a href=\"" + link + "\"><img width=\"480\" src=\"" + instance + "/" + post.Cover.Id + ".jpeg" + "\"></a>",
|
||||
}
|
||||
|
||||
if post.Cover.Type == "video" {
|
||||
item.Description = "🎞️ Video<br><br>" + item.Description
|
||||
}
|
||||
|
||||
feed.Items = append(feed.Items, item)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", mime.TypeByExtension("."+r.PathValue("type")))
|
||||
switch r.PathValue("type") {
|
||||
case "atom":
|
||||
body, err := feed.ToAtom()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.Write([]byte(body))
|
||||
case "json":
|
||||
body, err := feed.ToJSON()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.Write([]byte(body))
|
||||
case "rss":
|
||||
body, err := feed.ToRss()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.Write([]byte(body))
|
||||
default:
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.WriteHeader(400)
|
||||
w.Write([]byte("invalid type"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,30 +1,33 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"codeberg.org/rimgo/rimgo/render"
|
||||
"codeberg.org/rimgo/rimgo/utils"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func HandleSearch(c *fiber.Ctx) error {
|
||||
utils.SetHeaders(c)
|
||||
c.Set("X-Frame-Options", "DENY")
|
||||
c.Set("Cache-Control", "public,max-age=604800")
|
||||
c.Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; style-src 'unsafe-inline' 'self'; media-src 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
|
||||
func HandleSearch(w http.ResponseWriter, r *http.Request) error {
|
||||
utils.SetHeaders(w)
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
w.Header().Set("Cache-Control", "public,max-age=604800")
|
||||
w.Header().Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; style-src 'unsafe-inline' 'self'; media-src 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
|
||||
|
||||
query := c.Query("q")
|
||||
query := r.URL.Query().Get("q")
|
||||
|
||||
if utils.ImgurRe.MatchString(query) {
|
||||
return c.Redirect(utils.ImgurRe.ReplaceAllString(query, ""))
|
||||
w.Header().Set("Location", utils.ImgurRe.ReplaceAllString(query, ""))
|
||||
w.WriteHeader(302)
|
||||
return nil
|
||||
}
|
||||
|
||||
page := "0"
|
||||
if c.Query("page") != "" {
|
||||
page = c.Query("page")
|
||||
page := r.URL.Query().Get("page")
|
||||
if page == "" {
|
||||
page = "0"
|
||||
}
|
||||
|
||||
pageNumber, err := strconv.Atoi(c.Query("page"))
|
||||
pageNumber, err := strconv.Atoi(page)
|
||||
if err != nil {
|
||||
pageNumber = 0
|
||||
}
|
||||
@@ -34,11 +37,11 @@ func HandleSearch(c *fiber.Ctx) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Render("search", fiber.Map{
|
||||
"query": query,
|
||||
"results": results,
|
||||
"page": pageNumber,
|
||||
"nextPage": pageNumber + 1,
|
||||
"prevPage": pageNumber - 1,
|
||||
return render.Render(w, "search", map[string]any{
|
||||
"query": query,
|
||||
"results": results,
|
||||
"page": pageNumber,
|
||||
"nextPage": pageNumber + 1,
|
||||
"prevPage": pageNumber - 1,
|
||||
})
|
||||
}
|
||||
|
||||
39
pages/tag.go
39
pages/tag.go
@@ -1,45 +1,44 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"codeberg.org/rimgo/rimgo/render"
|
||||
"codeberg.org/rimgo/rimgo/utils"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func HandleTag(c *fiber.Ctx) error {
|
||||
utils.SetHeaders(c)
|
||||
c.Set("X-Frame-Options", "DENY")
|
||||
c.Set("Cache-Control", "public,max-age=604800")
|
||||
c.Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; style-src 'unsafe-inline' 'self'; media-src 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
|
||||
func HandleTag(w http.ResponseWriter, r *http.Request) error {
|
||||
utils.SetHeaders(w)
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
w.Header().Set("Cache-Control", "public,max-age=604800")
|
||||
w.Header().Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; style-src 'unsafe-inline' 'self'; media-src 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
|
||||
|
||||
page := "1"
|
||||
if c.Query("page") != "" {
|
||||
page = c.Query("page")
|
||||
page := r.URL.Query().Get("page")
|
||||
if page == "" {
|
||||
page = "1"
|
||||
}
|
||||
|
||||
pageNumber, err := strconv.Atoi(c.Query("page"))
|
||||
pageNumber, err := strconv.Atoi(page)
|
||||
if err != nil {
|
||||
pageNumber = 0
|
||||
}
|
||||
|
||||
tag, err := ApiClient.FetchTag(c.Params("tag"), c.Query("sort"), page)
|
||||
tag, err := ApiClient.FetchTag(r.PathValue("tag"), r.URL.Query().Get("sort"), page)
|
||||
if err != nil && err.Error() == "ratelimited by imgur" {
|
||||
return c.Status(429).Render("errors/429", fiber.Map{
|
||||
"path": c.Path(),
|
||||
})
|
||||
return utils.RenderError(w, r, 429)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tag.Display == "" {
|
||||
return c.Status(404).Render("errors/404", nil)
|
||||
return utils.RenderError(w, r, 404)
|
||||
}
|
||||
|
||||
return c.Render("tag", fiber.Map{
|
||||
"tag": tag,
|
||||
"page": page,
|
||||
"nextPage": pageNumber + 1,
|
||||
"prevPage": pageNumber - 1,
|
||||
return render.Render(w, "tag", map[string]any{
|
||||
"tag": tag,
|
||||
"page": page,
|
||||
"nextPage": pageNumber + 1,
|
||||
"prevPage": pageNumber - 1,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,35 +1,36 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"codeberg.org/rimgo/rimgo/render"
|
||||
"codeberg.org/rimgo/rimgo/utils"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func HandleTrending(c *fiber.Ctx) error {
|
||||
utils.SetHeaders(c)
|
||||
c.Set("X-Frame-Options", "DENY")
|
||||
c.Set("Cache-Control", "public,max-age=604800")
|
||||
c.Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; style-src 'unsafe-inline' 'self'; media-src 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
|
||||
func HandleTrending(w http.ResponseWriter, r *http.Request) error {
|
||||
utils.SetHeaders(w)
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
w.Header().Set("Cache-Control", "public,max-age=604800")
|
||||
w.Header().Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; style-src 'unsafe-inline' 'self'; media-src 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
|
||||
|
||||
page := "1"
|
||||
if c.Query("page") != "" {
|
||||
page = c.Query("page")
|
||||
page := r.URL.Query().Get("page")
|
||||
if page == "" {
|
||||
page = "1"
|
||||
}
|
||||
|
||||
pageNumber, err := strconv.Atoi(c.Query("page"))
|
||||
pageNumber, err := strconv.Atoi(page)
|
||||
if err != nil {
|
||||
pageNumber = 1
|
||||
}
|
||||
|
||||
section := c.Query("section")
|
||||
section := r.URL.Query().Get("section")
|
||||
switch section {
|
||||
case "hot", "new", "top":
|
||||
default:
|
||||
section = "hot"
|
||||
}
|
||||
sort := c.Query("sort")
|
||||
sort := r.URL.Query().Get("sort")
|
||||
switch sort {
|
||||
case "newest", "best", "popular":
|
||||
default:
|
||||
@@ -41,12 +42,12 @@ func HandleTrending(c *fiber.Ctx) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Render("trending", fiber.Map{
|
||||
"results": results,
|
||||
"section": section,
|
||||
"sort": sort,
|
||||
"page": pageNumber,
|
||||
"nextPage": pageNumber + 1,
|
||||
"prevPage": pageNumber - 1,
|
||||
return render.Render(w, "trending", map[string]any{
|
||||
"results": results,
|
||||
"section": section,
|
||||
"sort": sort,
|
||||
"page": pageNumber,
|
||||
"nextPage": pageNumber + 1,
|
||||
"prevPage": pageNumber - 1,
|
||||
})
|
||||
}
|
||||
|
||||
100
pages/user.go
100
pages/user.go
@@ -1,53 +1,49 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"codeberg.org/rimgo/rimgo/render"
|
||||
"codeberg.org/rimgo/rimgo/utils"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func HandleUser(c *fiber.Ctx) error {
|
||||
utils.SetHeaders(c)
|
||||
c.Set("X-Frame-Options", "DENY")
|
||||
c.Set("Cache-Control", "public,max-age=604800")
|
||||
c.Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; media-src 'self'; style-src 'unsafe-inline' 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
|
||||
func HandleUser(w http.ResponseWriter, r *http.Request) error {
|
||||
utils.SetHeaders(w)
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
w.Header().Set("Cache-Control", "public,max-age=604800")
|
||||
w.Header().Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; media-src 'self'; style-src 'unsafe-inline' 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
|
||||
|
||||
page := "0"
|
||||
if c.Query("page") != "" {
|
||||
page = c.Query("page")
|
||||
page := r.URL.Query().Get("page")
|
||||
if page == "" {
|
||||
page = "0"
|
||||
}
|
||||
|
||||
pageNumber, err := strconv.Atoi(c.Query("page"))
|
||||
pageNumber, err := strconv.Atoi(page)
|
||||
if err != nil {
|
||||
pageNumber = 0
|
||||
}
|
||||
|
||||
user, err := ApiClient.FetchUser(c.Params("userID"))
|
||||
user, err := ApiClient.FetchUser(r.PathValue("userID"))
|
||||
if err != nil && err.Error() == "ratelimited by imgur" {
|
||||
return c.Status(429).Render("errors/429", fiber.Map{
|
||||
"path": c.Path(),
|
||||
})
|
||||
return utils.RenderError(w, r, 429)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if user.Username == "" {
|
||||
return c.Status(404).Render("errors/404", nil)
|
||||
return utils.RenderError(w, r, 404)
|
||||
}
|
||||
|
||||
submissions, err := ApiClient.FetchSubmissions(c.Params("userID"), "newest", page)
|
||||
submissions, err := ApiClient.FetchSubmissions(r.PathValue("userID"), "newest", page)
|
||||
if err != nil && err.Error() == "ratelimited by imgur" {
|
||||
c.Status(429)
|
||||
return c.Render("errors/429", fiber.Map{
|
||||
"path": c.Path(),
|
||||
})
|
||||
return utils.RenderError(w, r, 429)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Render("user", fiber.Map{
|
||||
return render.Render(w, "user", map[string]any{
|
||||
"user": user,
|
||||
"submissions": submissions,
|
||||
"page": page,
|
||||
@@ -56,83 +52,73 @@ func HandleUser(c *fiber.Ctx) error {
|
||||
})
|
||||
}
|
||||
|
||||
func HandleUserComments(c *fiber.Ctx) error {
|
||||
utils.SetHeaders(c)
|
||||
c.Set("X-Frame-Options", "DENY")
|
||||
c.Set("Cache-Control", "public,max-age=604800")
|
||||
c.Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; media-src 'self'; style-src 'unsafe-inline' 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
|
||||
func HandleUserComments(w http.ResponseWriter, r *http.Request) error {
|
||||
utils.SetHeaders(w)
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
w.Header().Set("Cache-Control", "public,max-age=604800")
|
||||
w.Header().Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; media-src 'self'; style-src 'unsafe-inline' 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
|
||||
|
||||
user, err := ApiClient.FetchUser(c.Params("userID"))
|
||||
user, err := ApiClient.FetchUser(r.PathValue("userID"))
|
||||
if err != nil && err.Error() == "ratelimited by imgur" {
|
||||
return c.Status(429).Render("errors/429", fiber.Map{
|
||||
"path": c.Path(),
|
||||
})
|
||||
return utils.RenderError(w, r, 429)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if user.Username == "" {
|
||||
return c.Status(404).Render("errors/404", nil)
|
||||
return utils.RenderError(w, r, 404)
|
||||
}
|
||||
|
||||
comments, err := ApiClient.FetchUserComments(c.Params("userID"))
|
||||
comments, err := ApiClient.FetchUserComments(r.PathValue("userID"))
|
||||
if err != nil && err.Error() == "ratelimited by imgur" {
|
||||
c.Status(429)
|
||||
return c.Render("errors/429", fiber.Map{
|
||||
"path": c.Path(),
|
||||
})
|
||||
return utils.RenderError(w, r, 429)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Render("userComments", fiber.Map{
|
||||
return render.Render(w, "userComments", map[string]any{
|
||||
"user": user,
|
||||
"comments": comments,
|
||||
})
|
||||
}
|
||||
|
||||
func HandleUserFavorites(c *fiber.Ctx) error {
|
||||
utils.SetHeaders(c)
|
||||
c.Set("X-Frame-Options", "DENY")
|
||||
c.Set("Cache-Control", "public,max-age=604800")
|
||||
c.Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; media-src 'self'; style-src 'unsafe-inline' 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
|
||||
func HandleUserFavorites(w http.ResponseWriter, r *http.Request) error {
|
||||
utils.SetHeaders(w)
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
w.Header().Set("Cache-Control", "public,max-age=604800")
|
||||
w.Header().Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; media-src 'self'; style-src 'unsafe-inline' 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
|
||||
|
||||
page := "0"
|
||||
if c.Query("page") != "" {
|
||||
page = c.Query("page")
|
||||
page := r.URL.Query().Get("page")
|
||||
if page == "" {
|
||||
page = "0"
|
||||
}
|
||||
|
||||
pageNumber, err := strconv.Atoi(c.Query("page"))
|
||||
pageNumber, err := strconv.Atoi(page)
|
||||
if err != nil {
|
||||
pageNumber = 0
|
||||
}
|
||||
|
||||
user, err := ApiClient.FetchUser(c.Params("userID"))
|
||||
user, err := ApiClient.FetchUser(r.PathValue("userID"))
|
||||
if err != nil && err.Error() == "ratelimited by imgur" {
|
||||
return c.Status(429).Render("errors/429", fiber.Map{
|
||||
"path": c.Path(),
|
||||
})
|
||||
return utils.RenderError(w, r, 429)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if user.Username == "" {
|
||||
return c.Status(404).Render("errors/404", nil)
|
||||
return utils.RenderError(w, r, 404)
|
||||
}
|
||||
|
||||
favorites, err := ApiClient.FetchUserFavorites(c.Params("userID"), "newest", page)
|
||||
favorites, err := ApiClient.FetchUserFavorites(r.PathValue("userID"), "newest", page)
|
||||
if err != nil && err.Error() == "ratelimited by imgur" {
|
||||
c.Status(429)
|
||||
return c.Render("errors/429", fiber.Map{
|
||||
"path": c.Path(),
|
||||
})
|
||||
return utils.RenderError(w, r, 429)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Render("userFavorites", fiber.Map{
|
||||
return render.Render(w, "userFavorites", map[string]any{
|
||||
"user": user,
|
||||
"favorites": favorites,
|
||||
"page": page,
|
||||
|
||||
547
pnpm-lock.yaml
generated
Normal file
547
pnpm-lock.yaml
generated
Normal file
@@ -0,0 +1,547 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
devDependencies:
|
||||
'@tailwindcss/cli':
|
||||
specifier: ^4.1.5
|
||||
version: 4.1.5
|
||||
tailwindcss:
|
||||
specifier: ^4.1.5
|
||||
version: 4.1.5
|
||||
|
||||
packages:
|
||||
|
||||
'@parcel/watcher-android-arm64@2.5.1':
|
||||
resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@parcel/watcher-darwin-arm64@2.5.1':
|
||||
resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@parcel/watcher-darwin-x64@2.5.1':
|
||||
resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@parcel/watcher-freebsd-x64@2.5.1':
|
||||
resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@parcel/watcher-linux-arm-glibc@2.5.1':
|
||||
resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@parcel/watcher-linux-arm-musl@2.5.1':
|
||||
resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@parcel/watcher-linux-arm64-glibc@2.5.1':
|
||||
resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@parcel/watcher-linux-arm64-musl@2.5.1':
|
||||
resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@parcel/watcher-linux-x64-glibc@2.5.1':
|
||||
resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@parcel/watcher-linux-x64-musl@2.5.1':
|
||||
resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@parcel/watcher-win32-arm64@2.5.1':
|
||||
resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@parcel/watcher-win32-ia32@2.5.1':
|
||||
resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@parcel/watcher-win32-x64@2.5.1':
|
||||
resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@parcel/watcher@2.5.1':
|
||||
resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
|
||||
'@tailwindcss/cli@4.1.5':
|
||||
resolution: {integrity: sha512-Kr567rDwDjY1VUnfqh5/+DCpRf4B8lPs5O9flP4kri7n4AM2aubrIxGSh5GN8s+awUKw/U4+6kNlEnZbBNfUeg==}
|
||||
hasBin: true
|
||||
|
||||
'@tailwindcss/node@4.1.5':
|
||||
resolution: {integrity: sha512-CBhSWo0vLnWhXIvpD0qsPephiaUYfHUX3U9anwDaHZAeuGpTiB3XmsxPAN6qX7bFhipyGBqOa1QYQVVhkOUGxg==}
|
||||
|
||||
'@tailwindcss/oxide-android-arm64@4.1.5':
|
||||
resolution: {integrity: sha512-LVvM0GirXHED02j7hSECm8l9GGJ1RfgpWCW+DRn5TvSaxVsv28gRtoL4aWKGnXqwvI3zu1GABeDNDVZeDPOQrw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@tailwindcss/oxide-darwin-arm64@4.1.5':
|
||||
resolution: {integrity: sha512-//TfCA3pNrgnw4rRJOqavW7XUk8gsg9ddi8cwcsWXp99tzdBAZW0WXrD8wDyNbqjW316Pk2hiN/NJx/KWHl8oA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@tailwindcss/oxide-darwin-x64@4.1.5':
|
||||
resolution: {integrity: sha512-XQorp3Q6/WzRd9OalgHgaqgEbjP3qjHrlSUb5k1EuS1Z9NE9+BbzSORraO+ecW432cbCN7RVGGL/lSnHxcd+7Q==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@tailwindcss/oxide-freebsd-x64@4.1.5':
|
||||
resolution: {integrity: sha512-bPrLWbxo8gAo97ZmrCbOdtlz/Dkuy8NK97aFbVpkJ2nJ2Jo/rsCbu0TlGx8joCuA3q6vMWTSn01JY46iwG+clg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@tailwindcss/oxide-linux-arm-gnueabihf@4.1.5':
|
||||
resolution: {integrity: sha512-1gtQJY9JzMAhgAfvd/ZaVOjh/Ju/nCoAsvOVJenWZfs05wb8zq+GOTnZALWGqKIYEtyNpCzvMk+ocGpxwdvaVg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-gnu@4.1.5':
|
||||
resolution: {integrity: sha512-dtlaHU2v7MtdxBXoqhxwsWjav7oim7Whc6S9wq/i/uUMTWAzq/gijq1InSgn2yTnh43kR+SFvcSyEF0GCNu1PQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-musl@4.1.5':
|
||||
resolution: {integrity: sha512-fg0F6nAeYcJ3CriqDT1iVrqALMwD37+sLzXs8Rjy8Z1ZHshJoYceodfyUwGJEsQoTyWbliFNRs2wMQNXtT7MVA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-gnu@4.1.5':
|
||||
resolution: {integrity: sha512-SO+F2YEIAHa1AITwc8oPwMOWhgorPzzcbhWEb+4oLi953h45FklDmM8dPSZ7hNHpIk9p/SCZKUYn35t5fjGtHA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-musl@4.1.5':
|
||||
resolution: {integrity: sha512-6UbBBplywkk/R+PqqioskUeXfKcBht3KU7juTi1UszJLx0KPXUo10v2Ok04iBJIaDPkIFkUOVboXms5Yxvaz+g==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@tailwindcss/oxide-wasm32-wasi@4.1.5':
|
||||
resolution: {integrity: sha512-hwALf2K9FHuiXTPqmo1KeOb83fTRNbe9r/Ixv9ZNQ/R24yw8Ge1HOWDDgTdtzntIaIUJG5dfXCf4g9AD4RiyhQ==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
cpu: [wasm32]
|
||||
bundledDependencies:
|
||||
- '@napi-rs/wasm-runtime'
|
||||
- '@emnapi/core'
|
||||
- '@emnapi/runtime'
|
||||
- '@tybys/wasm-util'
|
||||
- '@emnapi/wasi-threads'
|
||||
- tslib
|
||||
|
||||
'@tailwindcss/oxide-win32-arm64-msvc@4.1.5':
|
||||
resolution: {integrity: sha512-oDKncffWzaovJbkuR7/OTNFRJQVdiw/n8HnzaCItrNQUeQgjy7oUiYpsm9HUBgpmvmDpSSbGaCa2Evzvk3eFmA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@tailwindcss/oxide-win32-x64-msvc@4.1.5':
|
||||
resolution: {integrity: sha512-WiR4dtyrFdbb+ov0LK+7XsFOsG+0xs0PKZKkt41KDn9jYpO7baE3bXiudPVkTqUEwNfiglCygQHl2jklvSBi7Q==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@tailwindcss/oxide@4.1.5':
|
||||
resolution: {integrity: sha512-1n4br1znquEvyW/QuqMKQZlBen+jxAbvyduU87RS8R3tUSvByAkcaMTkJepNIrTlYhD+U25K4iiCIxE6BGdRYA==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
braces@3.0.3:
|
||||
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
detect-libc@1.0.3:
|
||||
resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==}
|
||||
engines: {node: '>=0.10'}
|
||||
hasBin: true
|
||||
|
||||
detect-libc@2.0.4:
|
||||
resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
enhanced-resolve@5.18.1:
|
||||
resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
||||
fill-range@7.1.1:
|
||||
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
graceful-fs@4.2.11:
|
||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||
|
||||
is-extglob@2.1.1:
|
||||
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
is-glob@4.0.3:
|
||||
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
is-number@7.0.0:
|
||||
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
|
||||
engines: {node: '>=0.12.0'}
|
||||
|
||||
jiti@2.4.2:
|
||||
resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
|
||||
hasBin: true
|
||||
|
||||
lightningcss-darwin-arm64@1.29.2:
|
||||
resolution: {integrity: sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
lightningcss-darwin-x64@1.29.2:
|
||||
resolution: {integrity: sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
lightningcss-freebsd-x64@1.29.2:
|
||||
resolution: {integrity: sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
lightningcss-linux-arm-gnueabihf@1.29.2:
|
||||
resolution: {integrity: sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
lightningcss-linux-arm64-gnu@1.29.2:
|
||||
resolution: {integrity: sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
lightningcss-linux-arm64-musl@1.29.2:
|
||||
resolution: {integrity: sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
lightningcss-linux-x64-gnu@1.29.2:
|
||||
resolution: {integrity: sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
lightningcss-linux-x64-musl@1.29.2:
|
||||
resolution: {integrity: sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.29.2:
|
||||
resolution: {integrity: sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
lightningcss-win32-x64-msvc@1.29.2:
|
||||
resolution: {integrity: sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
lightningcss@1.29.2:
|
||||
resolution: {integrity: sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
|
||||
micromatch@4.0.8:
|
||||
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
|
||||
engines: {node: '>=8.6'}
|
||||
|
||||
mri@1.2.0:
|
||||
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
node-addon-api@7.1.1:
|
||||
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
|
||||
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
|
||||
picomatch@2.3.1:
|
||||
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
|
||||
engines: {node: '>=8.6'}
|
||||
|
||||
tailwindcss@4.1.5:
|
||||
resolution: {integrity: sha512-nYtSPfWGDiWgCkwQG/m+aX83XCwf62sBgg3bIlNiiOcggnS1x3uVRDAuyelBFL+vJdOPPCGElxv9DjHJjRHiVA==}
|
||||
|
||||
tapable@2.2.1:
|
||||
resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
to-regex-range@5.0.1:
|
||||
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||
engines: {node: '>=8.0'}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@parcel/watcher-android-arm64@2.5.1':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-darwin-arm64@2.5.1':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-darwin-x64@2.5.1':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-freebsd-x64@2.5.1':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-linux-arm-glibc@2.5.1':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-linux-arm-musl@2.5.1':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-linux-arm64-glibc@2.5.1':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-linux-arm64-musl@2.5.1':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-linux-x64-glibc@2.5.1':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-linux-x64-musl@2.5.1':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-win32-arm64@2.5.1':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-win32-ia32@2.5.1':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-win32-x64@2.5.1':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher@2.5.1':
|
||||
dependencies:
|
||||
detect-libc: 1.0.3
|
||||
is-glob: 4.0.3
|
||||
micromatch: 4.0.8
|
||||
node-addon-api: 7.1.1
|
||||
optionalDependencies:
|
||||
'@parcel/watcher-android-arm64': 2.5.1
|
||||
'@parcel/watcher-darwin-arm64': 2.5.1
|
||||
'@parcel/watcher-darwin-x64': 2.5.1
|
||||
'@parcel/watcher-freebsd-x64': 2.5.1
|
||||
'@parcel/watcher-linux-arm-glibc': 2.5.1
|
||||
'@parcel/watcher-linux-arm-musl': 2.5.1
|
||||
'@parcel/watcher-linux-arm64-glibc': 2.5.1
|
||||
'@parcel/watcher-linux-arm64-musl': 2.5.1
|
||||
'@parcel/watcher-linux-x64-glibc': 2.5.1
|
||||
'@parcel/watcher-linux-x64-musl': 2.5.1
|
||||
'@parcel/watcher-win32-arm64': 2.5.1
|
||||
'@parcel/watcher-win32-ia32': 2.5.1
|
||||
'@parcel/watcher-win32-x64': 2.5.1
|
||||
|
||||
'@tailwindcss/cli@4.1.5':
|
||||
dependencies:
|
||||
'@parcel/watcher': 2.5.1
|
||||
'@tailwindcss/node': 4.1.5
|
||||
'@tailwindcss/oxide': 4.1.5
|
||||
enhanced-resolve: 5.18.1
|
||||
mri: 1.2.0
|
||||
picocolors: 1.1.1
|
||||
tailwindcss: 4.1.5
|
||||
|
||||
'@tailwindcss/node@4.1.5':
|
||||
dependencies:
|
||||
enhanced-resolve: 5.18.1
|
||||
jiti: 2.4.2
|
||||
lightningcss: 1.29.2
|
||||
tailwindcss: 4.1.5
|
||||
|
||||
'@tailwindcss/oxide-android-arm64@4.1.5':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-darwin-arm64@4.1.5':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-darwin-x64@4.1.5':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-freebsd-x64@4.1.5':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-linux-arm-gnueabihf@4.1.5':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-gnu@4.1.5':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-linux-arm64-musl@4.1.5':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-gnu@4.1.5':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-linux-x64-musl@4.1.5':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-wasm32-wasi@4.1.5':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-win32-arm64-msvc@4.1.5':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide-win32-x64-msvc@4.1.5':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/oxide@4.1.5':
|
||||
optionalDependencies:
|
||||
'@tailwindcss/oxide-android-arm64': 4.1.5
|
||||
'@tailwindcss/oxide-darwin-arm64': 4.1.5
|
||||
'@tailwindcss/oxide-darwin-x64': 4.1.5
|
||||
'@tailwindcss/oxide-freebsd-x64': 4.1.5
|
||||
'@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.5
|
||||
'@tailwindcss/oxide-linux-arm64-gnu': 4.1.5
|
||||
'@tailwindcss/oxide-linux-arm64-musl': 4.1.5
|
||||
'@tailwindcss/oxide-linux-x64-gnu': 4.1.5
|
||||
'@tailwindcss/oxide-linux-x64-musl': 4.1.5
|
||||
'@tailwindcss/oxide-wasm32-wasi': 4.1.5
|
||||
'@tailwindcss/oxide-win32-arm64-msvc': 4.1.5
|
||||
'@tailwindcss/oxide-win32-x64-msvc': 4.1.5
|
||||
|
||||
braces@3.0.3:
|
||||
dependencies:
|
||||
fill-range: 7.1.1
|
||||
|
||||
detect-libc@1.0.3: {}
|
||||
|
||||
detect-libc@2.0.4: {}
|
||||
|
||||
enhanced-resolve@5.18.1:
|
||||
dependencies:
|
||||
graceful-fs: 4.2.11
|
||||
tapable: 2.2.1
|
||||
|
||||
fill-range@7.1.1:
|
||||
dependencies:
|
||||
to-regex-range: 5.0.1
|
||||
|
||||
graceful-fs@4.2.11: {}
|
||||
|
||||
is-extglob@2.1.1: {}
|
||||
|
||||
is-glob@4.0.3:
|
||||
dependencies:
|
||||
is-extglob: 2.1.1
|
||||
|
||||
is-number@7.0.0: {}
|
||||
|
||||
jiti@2.4.2: {}
|
||||
|
||||
lightningcss-darwin-arm64@1.29.2:
|
||||
optional: true
|
||||
|
||||
lightningcss-darwin-x64@1.29.2:
|
||||
optional: true
|
||||
|
||||
lightningcss-freebsd-x64@1.29.2:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-arm-gnueabihf@1.29.2:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-arm64-gnu@1.29.2:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-arm64-musl@1.29.2:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-x64-gnu@1.29.2:
|
||||
optional: true
|
||||
|
||||
lightningcss-linux-x64-musl@1.29.2:
|
||||
optional: true
|
||||
|
||||
lightningcss-win32-arm64-msvc@1.29.2:
|
||||
optional: true
|
||||
|
||||
lightningcss-win32-x64-msvc@1.29.2:
|
||||
optional: true
|
||||
|
||||
lightningcss@1.29.2:
|
||||
dependencies:
|
||||
detect-libc: 2.0.4
|
||||
optionalDependencies:
|
||||
lightningcss-darwin-arm64: 1.29.2
|
||||
lightningcss-darwin-x64: 1.29.2
|
||||
lightningcss-freebsd-x64: 1.29.2
|
||||
lightningcss-linux-arm-gnueabihf: 1.29.2
|
||||
lightningcss-linux-arm64-gnu: 1.29.2
|
||||
lightningcss-linux-arm64-musl: 1.29.2
|
||||
lightningcss-linux-x64-gnu: 1.29.2
|
||||
lightningcss-linux-x64-musl: 1.29.2
|
||||
lightningcss-win32-arm64-msvc: 1.29.2
|
||||
lightningcss-win32-x64-msvc: 1.29.2
|
||||
|
||||
micromatch@4.0.8:
|
||||
dependencies:
|
||||
braces: 3.0.3
|
||||
picomatch: 2.3.1
|
||||
|
||||
mri@1.2.0: {}
|
||||
|
||||
node-addon-api@7.1.1: {}
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
picomatch@2.3.1: {}
|
||||
|
||||
tailwindcss@4.1.5: {}
|
||||
|
||||
tapable@2.2.1: {}
|
||||
|
||||
to-regex-range@5.0.1:
|
||||
dependencies:
|
||||
is-number: 7.0.0
|
||||
17
render/helpers.go
Normal file
17
render/helpers.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package render
|
||||
|
||||
import "github.com/mailgun/raymond/v2"
|
||||
|
||||
func (r *renderer) registerHelpers() {
|
||||
funcmap := map[string]any{
|
||||
"noteq": noteq,
|
||||
}
|
||||
raymond.RegisterHelpers(funcmap)
|
||||
}
|
||||
|
||||
func noteq(a, b any, options *raymond.Options) any {
|
||||
if raymond.Str(a) != raymond.Str(b) {
|
||||
return options.Fn()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
76
render/render.go
Normal file
76
render/render.go
Normal file
@@ -0,0 +1,76 @@
|
||||
// stolen from gofiber/template but simpler
|
||||
package render
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/mailgun/raymond/v2"
|
||||
)
|
||||
|
||||
var Renderer *renderer
|
||||
|
||||
func Render(out io.Writer, name string, bind map[string]any) error {
|
||||
return Renderer.Render(out, name, bind)
|
||||
}
|
||||
|
||||
type renderer struct {
|
||||
templates map[string]*raymond.Template
|
||||
}
|
||||
|
||||
const ext = ".hbs"
|
||||
|
||||
func Initialize(views fs.FS) {
|
||||
r := new(renderer)
|
||||
r.templates = make(map[string]*raymond.Template)
|
||||
r.registerHelpers()
|
||||
fs.WalkDir(views, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil || d.IsDir() {
|
||||
return err
|
||||
}
|
||||
name, hasExt := strings.CutSuffix(path, ext)
|
||||
if !hasExt {
|
||||
return nil
|
||||
}
|
||||
path = filepath.ToSlash(path)
|
||||
file, err := views.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
buf, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tmpl, err := raymond.Parse(string(buf))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.templates[name] = tmpl
|
||||
return nil
|
||||
})
|
||||
for j := range r.templates {
|
||||
for n, template := range r.templates {
|
||||
r.templates[j].RegisterPartialTemplate(n, template)
|
||||
}
|
||||
}
|
||||
Renderer = r
|
||||
}
|
||||
|
||||
func (r *renderer) Render(out io.Writer, name string, bind map[string]any) error {
|
||||
tmpl := r.templates[name]
|
||||
if tmpl == nil {
|
||||
return fmt.Errorf("render: template %s does not exist", name)
|
||||
}
|
||||
parsed, err := tmpl.Exec(bind)
|
||||
if err != nil {
|
||||
return fmt.Errorf("render: %w", err)
|
||||
}
|
||||
if _, err = out.Write([]byte(parsed)); err != nil {
|
||||
return fmt.Errorf("render: %w", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -7,4 +7,4 @@ var files embed.FS
|
||||
|
||||
func GetFiles() embed.FS {
|
||||
return files
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "rimgo",
|
||||
"short_name": "rimgo",
|
||||
"start_url": "/",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/static/favicon/android-chrome-192x192.png",
|
||||
|
||||
1
static/icons/PhRss.svg
Normal file
1
static/icons/PhRss.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 256 256"><path fill="currentColor" d="M106.91 149.09A71.53 71.53 0 0 1 128 200a8 8 0 0 1-16 0a56 56 0 0 0-56-56a8 8 0 0 1 0-16a71.53 71.53 0 0 1 50.91 21.09M56 80a8 8 0 0 0 0 16a104 104 0 0 1 104 104a8 8 0 0 0 16 0A120 120 0 0 0 56 80m118.79 1.21A166.9 166.9 0 0 0 56 32a8 8 0 0 0 0 16a151 151 0 0 1 107.48 44.52A151 151 0 0 1 208 200a8 8 0 0 0 16 0a166.9 166.9 0 0 0-49.21-118.79M60 184a12 12 0 1 0 12 12a12 12 0 0 0-12-12"/></svg>
|
||||
|
After Width: | Height: | Size: 508 B |
BIN
static/img/error-404.png
Normal file
BIN
static/img/error-404.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
static/img/error-429.png
Normal file
BIN
static/img/error-429.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
BIN
static/img/error-generic.png
Normal file
BIN
static/img/error-generic.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@@ -1,6 +1,4 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@import "tailwindcss" source("../views");
|
||||
|
||||
body {
|
||||
margin: 0 24vw;
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
module.exports = {
|
||||
content: ["./views/*.hbs", "./views/**/*.hbs"],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
variants: {},
|
||||
plugins: [],
|
||||
};
|
||||
22
utils/accepts.go
Normal file
22
utils/accepts.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Accepts(r *http.Request, format string) bool {
|
||||
format = strings.ToLower(format)
|
||||
group := strings.Split(format, "/")[0] + "/*"
|
||||
header := r.Header.Get("Accept")
|
||||
if header == "" {
|
||||
return false
|
||||
}
|
||||
for _, mime := range strings.Split(header, ",") {
|
||||
mime = strings.ToLower(strings.TrimSpace(strings.SplitN(mime, ";", 2)[0]))
|
||||
if mime == "*/*" || mime == format || mime == group {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -6,60 +6,53 @@ import (
|
||||
)
|
||||
|
||||
type config struct {
|
||||
Port string
|
||||
Addr string
|
||||
ImgurId string
|
||||
FiberPrefork bool
|
||||
ImageCache bool
|
||||
CleanupInterval time.Duration
|
||||
CacheDir string
|
||||
Privacy map[string]interface{}
|
||||
Port string
|
||||
Addr string
|
||||
ImgurId string
|
||||
ProtocolDetection bool
|
||||
Secure bool
|
||||
ForceWebp bool
|
||||
RestrictiveCORS bool
|
||||
ImageCache bool
|
||||
CleanupInterval time.Duration
|
||||
CacheDir string
|
||||
Privacy map[string]interface{}
|
||||
}
|
||||
|
||||
var Config config
|
||||
|
||||
func envString(name, def string) string {
|
||||
env := os.Getenv(name)
|
||||
if env != "" {
|
||||
return env
|
||||
}
|
||||
return def
|
||||
}
|
||||
func envBool(name string) bool {
|
||||
return os.Getenv(name) == "true" || os.Getenv(name) == "1"
|
||||
}
|
||||
|
||||
func LoadConfig() {
|
||||
port := "3000"
|
||||
if os.Getenv("PORT") != "" {
|
||||
port = os.Getenv("PORT")
|
||||
}
|
||||
if os.Getenv("RIMGU_PORT") != "" {
|
||||
port = os.Getenv("RIMGU_PORT")
|
||||
}
|
||||
|
||||
addr := "0.0.0.0"
|
||||
if os.Getenv("ADDRESS") != "" {
|
||||
addr = os.Getenv("ADDRESS")
|
||||
}
|
||||
if os.Getenv("RIMGU_ADDRESS") != "" {
|
||||
addr = os.Getenv("RIMGU_ADDRESS")
|
||||
}
|
||||
|
||||
imgurId := "546c25a59c58ad7"
|
||||
if os.Getenv("IMGUR_CLIENT_ID") != "" {
|
||||
imgurId = os.Getenv("IMGUR_CLIENT_ID")
|
||||
}
|
||||
if os.Getenv("RIMGU_IMGUR_CLIENT_ID") != "" {
|
||||
imgurId = os.Getenv("RIMGU_IMGUR_CLIENT_ID")
|
||||
}
|
||||
|
||||
Config = config{
|
||||
Port: port,
|
||||
Addr: addr,
|
||||
ImgurId: imgurId,
|
||||
FiberPrefork: os.Getenv("FIBER_PREFORK") == "true",
|
||||
Port: envString("PORT", "3000"),
|
||||
Addr: envString("ADDR", "0.0.0.0"),
|
||||
ImgurId: envString("IMGUR_CLIENT_ID", "546c25a59c58ad7"),
|
||||
ProtocolDetection: envBool("PROTOCOL_DETECTION"),
|
||||
Secure: envBool("SECURE"),
|
||||
ForceWebp: envBool("FORCE_WEBP"),
|
||||
RestrictiveCORS: envBool("RESTRICTIVE_CORS"),
|
||||
Privacy: map[string]interface{}{
|
||||
"set": os.Getenv("PRIVACY_NOT_COLLECTED") != "",
|
||||
"policy": os.Getenv("PRIVACY_POLICY"),
|
||||
"message": os.Getenv("PRIVACY_MESSAGE"),
|
||||
"country": os.Getenv("PRIVACY_COUNTRY"),
|
||||
"provider": os.Getenv("PRIVACY_PROVIDER"),
|
||||
"cloudflare": os.Getenv("PRIVACY_CLOUDFLARE") == "true",
|
||||
"not_collected": os.Getenv("PRIVACY_NOT_COLLECTED") == "true",
|
||||
"ip": os.Getenv("PRIVACY_IP") == "true",
|
||||
"url": os.Getenv("PRIVACY_URL") == "true",
|
||||
"device": os.Getenv("PRIVACY_DEVICE") == "true",
|
||||
"diagnostics": os.Getenv("PRIVACY_DIAGNOSTICS") == "true",
|
||||
"cloudflare": envBool("PRIVACY_CLOUDFLARE"),
|
||||
"not_collected": envBool("PRIVACY_NOT_COLLECTED"),
|
||||
"ip": envBool("PRIVACY_IP"),
|
||||
"url": envBool("PRIVACY_URL"),
|
||||
"device": envBool("PRIVACY_DEVICE"),
|
||||
"diagnostics": envBool("PRIVACY_DIAGNOSTICS"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
43
utils/error.go
Normal file
43
utils/error.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"codeberg.org/rimgo/rimgo/render"
|
||||
"codeberg.org/rimgo/rimgo/static"
|
||||
)
|
||||
|
||||
func RenderError(w http.ResponseWriter, r *http.Request, code int, str ...string) (err error) {
|
||||
if len(str) != 1 {
|
||||
str = []string{""}
|
||||
}
|
||||
codeStr := "generic"
|
||||
if code == 0 {
|
||||
code = 500
|
||||
}
|
||||
if code != 500 {
|
||||
codeStr = strconv.Itoa(code)
|
||||
}
|
||||
if !Accepts(r, "text/html") && r.PathValue("extension") != "" {
|
||||
w.Header().Set("Content-Type", "image/png")
|
||||
w.WriteHeader(code)
|
||||
file, _ := static.GetFiles().Open("img/error-" + codeStr + ".png")
|
||||
defer file.Close()
|
||||
_, err = io.Copy(w, file)
|
||||
|
||||
} else {
|
||||
w.WriteHeader(code)
|
||||
err = render.Render(w, "errors/"+codeStr, map[string]any{
|
||||
"path": r.URL.Path,
|
||||
"err": str[0],
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
// don't panic or return error, it will loop
|
||||
fmt.Println("error in RenderError: " + err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -9,4 +9,4 @@ func FormatDate(date string) (string, error) {
|
||||
}
|
||||
|
||||
return time.Format("Jan 2, 2006 3:04 PM"), nil
|
||||
}
|
||||
}
|
||||
|
||||
21
utils/getUrl.go
Normal file
21
utils/getUrl.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package utils
|
||||
|
||||
import "net/http"
|
||||
|
||||
func GetInstanceProtocol(r *http.Request) string {
|
||||
proto := "https"
|
||||
if !Config.Secure {
|
||||
proto = "http"
|
||||
}
|
||||
if Config.ProtocolDetection {
|
||||
xproto := r.Header.Get("X-Forwarded-Proto")
|
||||
if xproto != "" {
|
||||
proto = xproto
|
||||
}
|
||||
}
|
||||
return proto
|
||||
}
|
||||
|
||||
func GetInstanceUrl(r *http.Request) string {
|
||||
return GetInstanceProtocol(r) + "://" + r.Host
|
||||
}
|
||||
@@ -2,4 +2,4 @@ package utils
|
||||
|
||||
import "regexp"
|
||||
|
||||
var ImgurRe = regexp.MustCompile(`https?://(i\.)?imgur\.com`)
|
||||
var ImgurRe = regexp.MustCompile(`https?://(i\.)?imgur\.com`)
|
||||
|
||||
@@ -35,7 +35,7 @@ func GetJSON(url string) (gjson.Result, error) {
|
||||
return gjson.Result{}, err
|
||||
}
|
||||
|
||||
switch (res.StatusCode) {
|
||||
switch res.StatusCode {
|
||||
case 200:
|
||||
return gjson.Parse(string(body)), nil
|
||||
case 429:
|
||||
@@ -43,4 +43,4 @@ func GetJSON(url string) (gjson.Result, error) {
|
||||
default:
|
||||
return gjson.Result{}, fmt.Errorf("received status %s, expected 200 OK.\n%s", res.Status, string(body))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,14 @@ package utils
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func SetHeaders(c *fiber.Ctx) {
|
||||
c.Set("Referrer-Policy", "no-referrer")
|
||||
c.Set("X-Content-Type-Options", "nosniff")
|
||||
c.Set("X-Robots-Tag", "noindex, noimageindex, nofollow")
|
||||
c.Set("Strict-Transport-Security", "max-age=31557600")
|
||||
c.Set("Permissions-Policy", "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(self), geolocation=(), gyroscope=(), interest-cohort=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()")
|
||||
func SetHeaders(w http.ResponseWriter) {
|
||||
w.Header().Set("Referrer-Policy", "no-referrer")
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.Header().Set("X-Robots-Tag", "noindex, noimageindex, nofollow")
|
||||
w.Header().Set("Strict-Transport-Security", "max-age=31557600")
|
||||
w.Header().Set("Permissions-Policy", "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(self), geolocation=(), gyroscope=(), interest-cohort=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()")
|
||||
}
|
||||
|
||||
func SetReqHeaders(req *http.Request) {
|
||||
@@ -25,4 +23,4 @@ func SetReqHeaders(req *http.Request) {
|
||||
req.Header.Set("Sec-Fetch-Mode", "cors")
|
||||
req.Header.Set("Sec-Fetch-Site", "same-site")
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; rv:102.0) Gecko/20100101 Firefox/102.0")
|
||||
}
|
||||
}
|
||||
|
||||
16
utils/splitNameExt.go
Normal file
16
utils/splitNameExt.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package utils
|
||||
|
||||
func SplitNameExt(path string) (name, ext string) {
|
||||
name, ext = path, ""
|
||||
for range 5 {
|
||||
if len(name) == 0 || name[len(name)-1] == '.' || name[len(name)-1] == '/' {
|
||||
break
|
||||
}
|
||||
name = name[:len(name)-1]
|
||||
ext = path[len(name):]
|
||||
}
|
||||
if len(name) == 0 || name[len(name)-1] != '.' {
|
||||
return path, ""
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -7,4 +7,4 @@ var files embed.FS
|
||||
|
||||
func GetFiles() embed.FS {
|
||||
return files
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="flex flex-col gap-2 bg-slate-600 p-4 rounded-lg rounded-t-none sm:rounded-t-lg w-full">
|
||||
<div class="flex flex-col h-full">
|
||||
<p class="md-container">{{{this.Comment}}}</p>
|
||||
<div class="flex-grow"></div>
|
||||
<div class="grow"></div>
|
||||
<div class="flex gap-2">
|
||||
<span title="{{this.CreatedAt}}">{{this.RelTime}}</span>
|
||||
{{#if this.DeletedAt}}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<head>
|
||||
<title>
|
||||
{{#if post.Title}}
|
||||
{{post.Title}} -
|
||||
{{post.Title}} -
|
||||
{{/if}}
|
||||
rimgo
|
||||
</title>
|
||||
@@ -23,33 +23,40 @@
|
||||
<h1 class="text-3xl font-bold">{{post.Title}}</h1>
|
||||
<p>{{post.CreatedAt}}</p>
|
||||
</header>
|
||||
|
||||
|
||||
<main>
|
||||
<div class="flex flex-col gap-2 md:flex-row md:gap-4 md:items-center my-4">
|
||||
{{#if post.User.Username}}
|
||||
<a href="/user/{{post.User.Username}}" class="flex gap-2 items-center">
|
||||
<img src="{{post.User.Avatar}}" class="rounded-full" width="36" height="36" />
|
||||
<p>
|
||||
<b>{{post.User.Username}}</b>
|
||||
</p>
|
||||
</a>
|
||||
{{/if}}
|
||||
<div class="flex gap-2 items-center">
|
||||
<div class="flex flex-center gap-2">
|
||||
<img class="icon invert" src="/static/icons/PhEye.svg" alt="Views" width="24px" height="24px">
|
||||
<p>{{post.Views}}</p>
|
||||
</div>
|
||||
{{#if post.SharedWithCommunity}}
|
||||
<div class="flex flex-center gap-2">
|
||||
<img class="icon invert" src="/static/icons/PhArrowFatUp.svg" alt="Likes" width="24px" height="24px">
|
||||
<p>{{post.Upvotes}}</p>
|
||||
</div>
|
||||
<div class="flex flex-center gap-2">
|
||||
<img class="icon invert" src="/static/icons/PhArrowFatDown.svg" alt="Dislikes" width="24px" height="24px">
|
||||
<p>{{post.Downvotes}}</p>
|
||||
</div>
|
||||
<div class="flex flex-col sm:flex-row my-4 w-full justify-between">
|
||||
<div class="flex flex-col gap-2 md:flex-row md:gap-4 md:items-center">
|
||||
{{#if post.User.Username}}
|
||||
<a href="/user/{{post.User.Username}}" class="flex gap-2 items-center">
|
||||
<img src="{{post.User.Avatar}}" class="rounded-full" width="36" height="36" />
|
||||
<p>
|
||||
<b>{{post.User.Username}}</b>
|
||||
</p>
|
||||
</a>
|
||||
{{/if}}
|
||||
<div class="flex gap-2 items-center">
|
||||
<div class="flex flex-center gap-2">
|
||||
<img class="icon invert" src="/static/icons/PhEye.svg" alt="Views" width="24px" height="24px">
|
||||
<p>{{post.Views}}</p>
|
||||
</div>
|
||||
{{#if post.SharedWithCommunity}}
|
||||
<div class="flex flex-center gap-2">
|
||||
<img class="icon invert" src="/static/icons/PhArrowFatUp.svg" alt="Likes" width="24px" height="24px">
|
||||
<p>{{post.Upvotes}}</p>
|
||||
</div>
|
||||
<div class="flex flex-center gap-2">
|
||||
<img class="icon invert" src="/static/icons/PhArrowFatDown.svg" alt="Dislikes" width="24px" height="24px">
|
||||
<p>{{post.Downvotes}}</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{#noteq next ""}}
|
||||
<a href="{{next}}" class="self-end">
|
||||
<button class="p-2 rounded-lg bg-slate-600">Next ></button>
|
||||
</a>
|
||||
{{/noteq}}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-center flex-col break-words">
|
||||
@@ -70,7 +77,7 @@
|
||||
{{#if this.Description}}
|
||||
<p>{{{this.Description}}}</p>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
{{#if post.tags}}
|
||||
@@ -78,7 +85,7 @@
|
||||
<style nonce="{{nonce}}">
|
||||
{{#each post.tags}}
|
||||
.{{this.BackgroundId}} { background-image: url('{{this.Background}}') }
|
||||
{{/each}}
|
||||
{{/each}}
|
||||
</style>
|
||||
{{#each post.tags}}
|
||||
<a href="/t/{{this.Tag}}">
|
||||
@@ -101,7 +108,8 @@
|
||||
{{#if comments}}
|
||||
<div>
|
||||
<input id="comments__expandBtn" type="checkbox" checked>
|
||||
<label class="comments__expandBtn__label my-2 py-4 border-solid border-t-2 border-slate-400" for="comments__expandBtn">
|
||||
<label class="comments__expandBtn__label my-2 py-4 border-solid border-t-2 border-slate-400"
|
||||
for="comments__expandBtn">
|
||||
<h3 class="text-xl font-bold">Comments ({{post.Comments}})</h3>
|
||||
<span class="text-xl font-bold"></span>
|
||||
</label>
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
<title>{{tag.Display}} - rimgo</title>
|
||||
|
||||
{{> partials/head }}
|
||||
<link href="/t/{{tag.Tag}}.rss" rel="alternate" title={{tag.Display}} type="application/rss+xml"
|
||||
<link href="/t/{{tag.Tag}}.atom" rel="alternate" title={{tag.Display}} type="application/atom+xml">
|
||||
<link href="/t/{{tag.Tag}}.json" rel="alternate" title={{tag.Display}} type="application/feed+json">
|
||||
</head>
|
||||
|
||||
<body class="font-sans text-lg bg-slate-800 text-white">
|
||||
@@ -16,7 +19,12 @@
|
||||
|
||||
<header class="p-4 rounded-xl text-white mb-4" style="background-image: url('{{tag.Background}}');">
|
||||
<div class="flex flex-col items-center justify-center text-center">
|
||||
<h2 class="text-2xl font-bold">{{tag.Display}}</h2>
|
||||
<div class="flex gap-2 items-center">
|
||||
<h2 class="text-2xl font-bold">{{tag.Display}}</h2>
|
||||
<a href="/t/{{tag.Tag}}.rss" label="RSS">
|
||||
<img src="/static/icons/PhRss.svg" width="24px" height="24px" class="invert" />
|
||||
</a>
|
||||
</div>
|
||||
<p>{{tag.PostCount}} posts</p>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
@@ -37,7 +45,7 @@
|
||||
{{/equal}}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
<main>
|
||||
<div class="posts">
|
||||
{{#each tag.Posts}}
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
<title>Trending - rimgo</title>
|
||||
|
||||
{{> partials/head }}
|
||||
<link href="/trending.rss" rel="alternate" title="Trending" type="application/rss+xml">
|
||||
<link href="/trending.atom" rel="alternate" title="Trending" type="application/atom+xml">
|
||||
<link href="/trending.json" rel="alternate" title="Trending" type="application/feed+json">
|
||||
</head>
|
||||
|
||||
<body class="font-sans text-lg bg-slate-800 text-white">
|
||||
@@ -15,8 +18,11 @@
|
||||
</section>
|
||||
|
||||
<header class="p-4 rounded-xl text-white mb-4 bg-gradient-to-r from-blue-400 to-emerald-400">
|
||||
<div class="flex flex-col items-center justify-center text-center">
|
||||
<div class="flex items-center justify-center text-center gap-2">
|
||||
<h2 class="text-2xl font-extrabold">Trending</h2>
|
||||
<a href="/trending.rss" label="RSS">
|
||||
<img src="/static/icons/PhRss.svg" width="24px" height="24px" class="invert" />
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex flex-col sm:flex-row sm:justify-between">
|
||||
<div class="flex flex-col">
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
<title>{{user.Username}} - rimgo</title>
|
||||
|
||||
{{> partials/head }}
|
||||
<link href="/user/{{user.Username}}.rss" rel="alternate" title={{user.Username}} type="application/rss+xml">
|
||||
<link href="/user/{{user.Username}}.atom" rel="alternate" title={{user.Username}} type="application/atom+xml">
|
||||
<link href="/user/{{user.Username}}.json" rel="alternate" title={{user.Username}} type="application/feed+json">
|
||||
</head>
|
||||
|
||||
<body class="font-sans text-lg bg-slate-800 text-white">
|
||||
@@ -18,10 +21,15 @@
|
||||
<div class="flex flex-col sm:flex-row items-center gap-2">
|
||||
<img class="rounded-full" src="{{user.Avatar}}" width="72" height="72">
|
||||
<div class="items-center sm:items-start text-center sm:text-left">
|
||||
<h2 class="font-bold text-2xl">{{user.Username}}</h2>
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<h2 class="font-bold text-2xl">{{user.Username}}</h2>
|
||||
<a href="/user/{{user.Username}}.rss" label="RSS">
|
||||
<img src="/static/icons/PhRss.svg" width="24px" height="24px" class="invert" />
|
||||
</a>
|
||||
</div>
|
||||
<p>{{user.Points}} pts · {{user.CreatedAt}}</p>
|
||||
</div>
|
||||
<hr class="sm:border-0 flex-grow">
|
||||
<hr class="sm:border-0 grow">
|
||||
<div class="flex flex-col items-center sm:items-end">
|
||||
<a href="/user/{{user.Username}}"><b>Submissions</b></a>
|
||||
<a href="/user/{{user.Username}}/favorites">Favorites</a>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<h2 class="font-bold text-2xl">{{user.Username}}</h2>
|
||||
<p>{{user.Points}} pts · {{user.CreatedAt}}</p>
|
||||
</div>
|
||||
<hr class="sm:border-0 flex-grow">
|
||||
<hr class="sm:border-0 grow">
|
||||
<div class="flex flex-col items-center sm:items-end">
|
||||
<a href="/user/{{user.Username}}">Submissions</a>
|
||||
<a href="/user/{{user.Username}}/favorites">Favorites</a>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<h2 class="font-bold text-2xl">{{user.Username}}</h2>
|
||||
<p>{{user.Points}} pts · {{user.CreatedAt}}</p>
|
||||
</div>
|
||||
<hr class="sm:border-0 flex-grow">
|
||||
<hr class="sm:border-0 grow">
|
||||
<div class="flex flex-col items-center sm:items-end">
|
||||
<a href="/user/{{user.Username}}">Submissions</a>
|
||||
<a href="/user/{{user.Username}}/favorites"><b>Favorites</b></a>
|
||||
|
||||
Reference in New Issue
Block a user