3 Commits

Author SHA1 Message Date
video-prize-ranch
5438365e50 Fix previous/next page buttons 2023-08-29 18:58:11 -04:00
orangix
3d707c561e cache per page 2023-08-29 23:58:30 +02:00
orangix
9fd38bed03 added user favorites 2023-08-20 07:58:38 +02:00
64 changed files with 899 additions and 1877 deletions

View File

@@ -2,7 +2,6 @@ ADDRESS=0.0.0.0
PORT=3000 PORT=3000
FIBER_PREFORK=false FIBER_PREFORK=false
IMGUR_CLIENT_ID=546c25a59c58ad7 IMGUR_CLIENT_ID=546c25a59c58ad7
SECURE=true
# Instance privacy # Instance privacy
# For more information, see https://codeberg.org/librarian/librarian/wiki/Instance-privacy # For more information, see https://codeberg.org/librarian/librarian/wiki/Instance-privacy

1
.gitignore vendored
View File

@@ -2,4 +2,3 @@
tmp tmp
static/app.css static/app.css
dist/ dist/
node_modules

View File

@@ -1,7 +1,6 @@
version: 2
before: before:
hooks: hooks:
- pnpm run build - tailwindcss -i static/tailwind.css -o static/app.css -m
- go mod tidy - go mod tidy
project_name: rimgo project_name: rimgo
builds: builds:
@@ -17,26 +16,24 @@ builds:
ldflags: ldflags:
- -X codeberg.org/rimgo/rimgo/pages.VersionInfo={{.Version}} - -X codeberg.org/rimgo/rimgo/pages.VersionInfo={{.Version}}
archives: archives:
- formats: tar.gz - format: tar.gz
name_template: >- name_template: >-
{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}{{- if .Arm }}v{{ .Arm }}{{ end }} {{ .ProjectName }}-{{ .Os }}-{{ .Arch }}{{- if .Arm }}v{{ .Arm }}{{ end }}
format_overrides: format_overrides:
- goos: windows - goos: windows
formats: zip format: zip
checksum: checksum:
name_template: 'checksums.txt' name_template: 'checksums.txt'
kos: kos:
- repositories: - repository: codeberg.org/rimgo/rimgo
- codeberg.org/rimgo/rimgo
- codeberg.org/video-prize-ranch/rimgo
tags: tags:
- '{{.Version}}' - '{{.Version}}'
- latest - latest
bare: true bare: true
preserve_import_paths: false preserve_import_paths: false
platforms: platforms:
- linux/amd64 - linux/amd64
- linux/arm64 - linux/arm64
sbom: none sbom: none
gitea_urls: gitea_urls:
api: https://codeberg.org/api/v1 api: https://codeberg.org/api/v1

View File

@@ -3,14 +3,14 @@ FROM --platform=$BUILDPLATFORM golang:alpine AS build
ARG TARGETARCH ARG TARGETARCH
WORKDIR /src WORKDIR /src
RUN apk --no-cache add ca-certificates git nodejs pnpm RUN apk --no-cache add ca-certificates git nodejs npm
COPY . . COPY . .
RUN pnpm install && pnpm run build RUN npx tailwindcss -i static/tailwind.css -o static/app.css -m
RUN go mod download 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)" 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 WORKDIR /app
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

View File

@@ -1,14 +1,7 @@
build: build:
pnpm run build tailwindcss -i static/tailwind.css -o static/app.css -m
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)" 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: dev:
go run github.com/air-verse/air@latest -c .air.toml tailwindcss -i static/tailwind.css -o static/app.css -m -w &
go run github.com/cosmtrek/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

View File

@@ -17,6 +17,8 @@ An alternative frontend for Imgur. Originally based on [rimgu](https://codeberg.
- [Privacy](#privacy) - [Privacy](#privacy)
- [Usage](#usage) - [Usage](#usage)
- [Instances](#instances) - [Instances](#instances)
- [Clearnet](#clearnet)
- [Tor](#tor)
- [Contributing](#contributing) - [Contributing](#contributing)
- [License](#license) - [License](#license)
@@ -26,7 +28,7 @@ Our new documentation is now available at [https://rimgo.codeberg.page/docs/](ht
- [Install](https://rimgo.codeberg.page/docs/getting-started/install/) - [Install](https://rimgo.codeberg.page/docs/getting-started/install/)
- [Configuration](https://rimgo.codeberg.page/docs/usage/configuration/) - [Configuration](https://rimgo.codeberg.page/docs/usage/configuration/)
- [Redirection](https://rimgo.codeberg.page/docs/usage/redirection/) - [Redirection](https://rimgo.codeberg.page/docs/usage/configuration/)
- [Instance privacy](https://rimgo.codeberg.page/docs/usage/instance-privacy/) - [Instance privacy](https://rimgo.codeberg.page/docs/usage/instance-privacy/)
## Features ## Features
@@ -50,12 +52,12 @@ Tested using [Google PageSpeed Insights](https://pagespeed.web.dev/).
| Time to Interactive | 1.6s | 23.8s | | Time to Interactive | 1.6s | 23.8s |
### Privacy ### 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 84 trackers and 264 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 31 trackers and 87 third-party cookies.
See what cookies and trackers Imgur uses and where your data gets sent: https://themarkup.org/blacklight?url=imgur.com See what cookies and trackers Imgur uses and where your data gets sent: https://themarkup.org/blacklight?url=imgur.com
## Usage ## 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. 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).
Imgur: `https://imgur.com/gallery/j2sOQkJ` -> `https://rimgo.bcow.xyz/gallery/j2sOQkJ` 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` Stack Overflow: `https://i.stack.imgur.com/KnO3v.jpg?s=64&g=1` -> `https://rimgo.bcow.xyz/stack/KnO3v.jpg?s=64&g=1`
@@ -63,8 +65,58 @@ Stack Overflow: `https://i.stack.imgur.com/KnO3v.jpg?s=64&g=1` -> `https://rimgo
To automatically redirect Imgur links, see [Redirection](https://rimgo.codeberg.page/docs/usage/redirection/). To automatically redirect Imgur links, see [Redirection](https://rimgo.codeberg.page/docs/usage/redirection/).
## Instances ## Instances
Open an issue to have your instance listed here! See the rules for the instance list [here](https://rimgo.codeberg.page/docs/usage/instance-list-rules/).
Available at https://rimgo.codeberg.page/ or https://codeberg.org/rimgo/instances > For more details on instance privacy, see https://rimgo.codeberg.page/docs/usage/instance-privacy/
### Clearnet
| URL | Country | Provider | Privacy | Notes |
| :------------------------------------------------------------ | :----------- | :----------------------- | :-------------------- | :---- |
| [rimgo.pussthecat.org](https://rimgo.pussthecat.org) | 🇩🇪 DE | Hetzner | ⚠️ Data collected | |
| [rimgo.totaldarkness.net](https://rimgo.totaldarkness.net) | 🇨🇦 CA | Vultr | ✅ Data not collected | |
| [rimgo.bus-hit.me](https://rimgo.bus-hit.me) | 🇨🇦 CA | Oracle | ⚠️ Data collected | |
| [imgur.artemislena.eu](https://imgur.artemislena.eu) | 🇩🇪 DE | Vodafone Deutschland | ✅ Data not collected | Self-hosted, provider is ISP |
| [rimgo.vern.cc](https://rimgo.vern.cc) | 🇺🇸 US | OVHCloud | ✅ Data not collected | [Edited theme](https://git.vern.cc/root/modifications/src/branch/master/rimgo) |
| [rim.odyssey346.dev](https://rim.odyssey346.dev/) | 🇫🇷️ FR | Trolling Solutions (OVH) | ✅ Data not collected | |
| [i.habedieeh.re](https://i.habedieeh.re/) | 🇨🇦️ CA | Oracle Cloud | ✅ Data not collected | |
| [rimgo.hostux.net](https://rimgo.hostux.net/) | 🇫🇷️ FR | Gandi | ⚠️ Data collected | |
| [ri.zzls.xyz](https://ri.zzls.xyz/) | 🇨🇱 CL | TELEFÓNICA CHILE | ✅ Data not collected | Self-hosted, provider is ISP |
| [rimgo.lunar.icu](https://rimgo.marcopisco.com/) | 🇩🇪 DE | Cloudflare | ✅ Data not collected | |
| [imgur.010032.xyz](https://imgur.010032.xyz/) | 🇰🇷 KR | Oracle Cloud | ✅ Data not collected | |
| [rimgo.kling.gg](https://rimgo.kling.gg/) | 🇳🇱 NL | RamNode | ✅ Data not collected | |
| [i.01r.xyz](https://i.01r.xyz/) | 🇺🇸 US | Cloudflare | ✅ Data not collected | |
| [rimgo.projectsegfau.lt](https://rimgo.projectsegfau.lt/) | 🇫🇷 FR, 🇺🇸 US, 🇮🇳 IN | See below | ✅ Data not collected | |
| [rimgo.eu.projectsegfau.lt](https://rimgo.eu.projectsegfau.lt/) | 🇫🇷 FR | Orange S.A. | ✅ Data not collected | |
| [rimgo.us.projectsegfau.lt](https://rimgo.us.projectsegfau.lt/) | 🇺🇸 US | Racknerd | ✅ Data not collected | |
| [rimgo.in.projectsegfau.lt](https://rimgo.in.projectsegfau.lt/) | 🇮🇳 IN | Airtel | ✅ Data not collected | |
| [rimgo.whateveritworks.org](https://rimgo.whateveritworks.org/) | 🇩🇪 DE | Cloudflare | ✅ Data not collected | |
| [rimgo.nohost.network](https://rimgo.nohost.network/) | 🇲🇽 MX | Telmex | ✅ Data not collected | |
| [rimgo.catsarch.com](https://rimgo.catsarch.com/) | 🇺🇸 US | Comcast | ✅ Data not collected | Self-hosted, provider is ISP |
| [rimgo.frontendfriendly.xyz](https://rimgo.frontendfriendly.xyz/) | 🇩🇪 DE | Hetzner | ⚠️ Data collected | |
### Tor
| URL | Privacy | Notes |
| :-- | :------ | :----------------------- |
| [rimgo.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion](http://rimgo.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion) | ✅ Data not collected | Onion of rimgo.vern.cc |
| [imgur.lpoaj7z2zkajuhgnlltpeqh3zyq7wk2iyeggqaduhgxhyajtdt2j7wad.onion](http://imgur.lpoaj7z2zkajuhgnlltpeqh3zyq7wk2iyeggqaduhgxhyajtdt2j7wad.onion) | ✅ Data not collected | Onion of imgur.artemislena.eu |
| [rim.odysfvr23q5wgt7i456o5t3trw2cw5dgn56vbjfbq2m7xsc5vqbqpcyd.onion](http://rim.odysfvr23q5wgt7i456o5t3trw2cw5dgn56vbjfbq2m7xsc5vqbqpcyd.onion) | ⚠️ Data collected | |
| [tdp6uqjtmok723suum5ms3jbquht6d7dssug4cgcxhfniatb25gcipad.onion](http://tdp6uqjtmok723suum5ms3jbquht6d7dssug4cgcxhfniatb25gcipad.onion) | ✅ Data not collected | Onion of rimgo.privacytools.io |
| [i.habeehrhadazsw3izbrbilqajalfyqqln54mrja3iwpqxgcuxnus7eid.onion](http://i.habeehrhadazsw3izbrbilqajalfyqqln54mrja3iwpqxgcuxnus7eid.onion/) | ✅ Data not collected | Onion of i.habedieeh.re |
| [rimgo.zzlsghu6mvvwyy75mvga6gaf4znbp3erk5xwfzedb4gg6qqh2j6rlvid.onion](http://rimgo.zzlsghu6mvvwyy75mvga6gaf4znbp3erk5xwfzedb4gg6qqh2j6rlvid.onion/) | ✅ Data not collected | Onion of ri.zzls.xyz |
| [tdn7zoxctmsopey77mp4eg2gazaudyhgbuyytf4zpk5u7lknlxlgbnid.onion/](http://tdn7zoxctmsopey77mp4eg2gazaudyhgbuyytf4zpk5u7lknlxlgbnid.onion/) | ✅ Data not collected | Onion of rimgo.kling.gg |
| [rimgo.pjsfkvpxlinjamtawaksbnnaqs2fc2mtvmozrzckxh7f3kis6yea25ad.onion](http://rimgo.pjsfkvpxlinjamtawaksbnnaqs2fc2mtvmozrzckxh7f3kis6yea25ad.onion/) | ✅ Data not collected | Onion of rimgo.eu.projectsegfau.lt |
### I2P
| URL | Privacy | Notes |
| :-- | :------ | :----------------------- |
| [rimgo.i2p](http://rimgo.i2p) | ✅ Data not collected | i.habedieeh.re on I2P |
| [rimgov7l2tqyrm5txrtvhtnfyrzkc5d7ipafofavchbnnyog4r3q.b32.i2p](http://rimgov7l2tqyrm5txrtvhtnfyrzkc5d7ipafofavchbnnyog4r3q.b32.i2p) | ✅ Data not collected | Same as rimgo.i2p |
| [rimgo.zzls.i2p](http://rimgo.zzls.i2p) | ✅ Data not collected | ri.zzls.xyz on I2P |
| [p57356k2xwhxrg2lxrjajcftkrptv4zejeeblzfgkcvpzuetkz2a.b32.i2p](http://p57356k2xwhxrg2lxrjajcftkrptv4zejeeblzfgkcvpzuetkz2a.b32.i2p) | ✅ Data not collected | Same as rimgo.zzls.i2p |
| [ovzamsts5czfx3jasbbhbccyyl2z7qmdngtlqxdh4oi7abhdz3ia.b32.i2p](http://ovzamsts5czfx3jasbbhbccyyl2z7qmdngtlqxdh4oi7abhdz3ia.b32.i2p) | ✅ Data not collected | rimgo.kling.gg on I2P |
## Install ## Install
@@ -75,10 +127,7 @@ See [Install](https://rimgo.codeberg.page/docs/getting-started/install/).
See [Configuration](https://rimgo.codeberg.page/docs/usage/configuration/). See [Configuration](https://rimgo.codeberg.page/docs/usage/configuration/).
## Contributing ## Contributing
Pull requests are welcome! If you have any questions or bug reports, open an [issue](https://codeberg.org/rimgo/rimgo/issues/new). Please remember to follow our [Code of Conduct](https://rimgo.codeberg.page/docs/code-of-conduct/)! Pull requests are welcome! If you have any questions or bug reports, open an [issue](https://codeberg.org/rimgo/rimgo/issues/new).
## License ## License
This software is released under the AGPL-3.0 license. If you make any modifications to the code and distribute it (including use on a network server), you must publicly distribute your changes and release them under the AGPL-3.0. This software is released under the AGPL-3.0 license. If you make any modifications to the code and distribute it (including use on a network server), you must publicly distribute your changes and release them under the AGPL-3.0.
## Legal notice
rimgo does not allow uploads or host any content, media. All content on any rimgo instances is from Imgur™. Imgur is a trademark of Imgur, Inc. Any issues with content on rimgo should be be reported to Imgur. rimgo is not affiliated with Imgur, Inc.

View File

@@ -5,6 +5,7 @@ import (
"time" "time"
"codeberg.org/rimgo/rimgo/utils" "codeberg.org/rimgo/rimgo/utils"
"github.com/microcosm-cc/bluemonday"
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
) )
@@ -100,13 +101,17 @@ func parseAlbum(data gjson.Result) (Album, error) {
url := value.Get("url").String() url := value.Get("url").String()
url = strings.ReplaceAll(url, "https://i.imgur.com", "") url = strings.ReplaceAll(url, "https://i.imgur.com", "")
description := value.Get("metadata.description").String()
description = strings.ReplaceAll(description, "\n", "<br>")
description = bluemonday.UGCPolicy().Sanitize(description)
media = append(media, Media{ media = append(media, Media{
Id: value.Get("id").String(), Id: value.Get("id").String(),
Name: value.Get("name").String(), Name: value.Get("name").String(),
MimeType: value.Get("mime_type").String(), MimeType: value.Get("mime_type").String(),
Type: value.Get("type").String(), Type: value.Get("type").String(),
Title: value.Get("metadata.title").String(), Title: value.Get("metadata.title").String(),
Description: value.Get("metadata.description").String(), Description: description,
Url: url, Url: url,
}) })

View File

@@ -7,15 +7,15 @@ import (
) )
type Client struct { type Client struct {
ClientID string ClientID string
Cache *cache.Cache Cache *cache.Cache
} }
func NewClient(clientId string) *Client { func NewClient(clientId string) (*Client) {
client := Client{ client := Client{
ClientID: clientId, ClientID: clientId,
Cache: cache.New(15*time.Minute, 15*time.Minute), Cache: cache.New(15*time.Minute, 15*time.Minute),
} }
return &client return &client
} }

View File

@@ -1,114 +1,145 @@
package api package api
import ( import (
"encoding/json" "regexp"
"errors" "strings"
"fmt"
"sync" "sync"
"time" "time"
"codeberg.org/rimgo/rimgo/utils" "codeberg.org/rimgo/rimgo/utils"
"github.com/dustin/go-humanize"
"github.com/microcosm-cc/bluemonday"
"github.com/patrickmn/go-cache" "github.com/patrickmn/go-cache"
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
"gitlab.com/golang-commonmark/linkify"
) )
type Comment struct { type Comment struct {
ID int `json:"id"` Comments []Comment
ParentID int `json:"parent_id"` User User
Comment string `json:"comment"` Post Submission
PostID string `json:"post_id"` Id string
UpvoteCount int `json:"upvote_count"` Comment string
DownvoteCount int `json:"downvote_count"` Upvotes int64
PointCount int `json:"point_count"` Downvotes int64
Vote struct{} `json:"vote"` // ??? Platform string
PlatformID int `json:"platform_id"` CreatedAt string
Platform string `json:"platform"` RelTime string
CreatedAt time.Time `json:"created_at"` UpdatedAt string
UpdatedAt time.Time `json:"updated_at"` DeletedAt string
DeletedAt time.Time `json:"deleted_at"`
Next struct{} `json:"next"` // ???
Comments commentArray `json:"comments"`
AccountID int `json:"account_id"`
Account _ApiUser `json:"account"`
Post commentPost `json:"post"`
}
type _ApiUser struct {
ID int `json:"id"`
Username string `json:"username"`
Avatar string `json:"avatar"`
} }
func (client *Client) FetchComments(galleryID string) ([]Comment, error) { func (client *Client) FetchComments(galleryID string) ([]Comment, error) {
cacheData, found := client.Cache.Get(galleryID + "-comments") cacheData, found := client.Cache.Get(galleryID + "-comments")
if found { if found {
return cacheData.(commentArray), nil return cacheData.([]Comment), nil
} }
data, err := utils.GetJSONNew("https://api.imgur.com/comment/v1/comments?client_id=" + client.ClientID + "&filter[post]=eq:" + galleryID + "&include=account,adconfig&per_page=30&sort=best") data, err := utils.GetJSON("https://api.imgur.com/comment/v1/comments?client_id=" + client.ClientID + "&filter[post]=eq:" + galleryID + "&include=account,adconfig&per_page=30&sort=best")
if err != nil { if err != nil {
return []Comment{}, nil return []Comment{}, nil
} }
var parsed commentApiResponse
err = json.Unmarshal(data, &parsed)
if err != nil {
return []Comment{}, err
}
client.Cache.Set(galleryID+"-comments", parsed.Data, cache.DefaultExpiration)
return parsed.Data, nil
}
func parseComment(data json.RawMessage, out *Comment) {
err := json.Unmarshal(data, &out)
if err != nil {
panic(err)
}
}
type commentArray []Comment
func (arr *commentArray) UnmarshalJSON(data []byte) error {
var rawArr []json.RawMessage
err := json.Unmarshal(data, &rawArr)
if err != nil {
return err
}
*arr = make(commentArray, len(rawArr))
panics := make([]error, 0, len(rawArr))
wg := sync.WaitGroup{} wg := sync.WaitGroup{}
var handlePanic = func() { comments := make([]Comment, 0)
if v := recover(); v != nil { data.Get("data").ForEach(
v, ok := v.(error) func(key, value gjson.Result) bool {
if !ok { wg.Add(1)
v = fmt.Errorf("%v", v)
} go func() {
panics = append(panics, v) defer wg.Done()
} comments = append(comments, parseComment(value))
} }()
for i, value := range rawArr {
wg.Add(1) return true
go func() { },
defer handlePanic() )
defer wg.Done()
parseComment(value, &(*arr)[i])
}()
}
wg.Wait() wg.Wait()
if len(panics) != 0 {
return errors.Join(panics...) client.Cache.Set(galleryID+"-comments", comments, cache.DefaultExpiration)
return comments, nil
}
var imgurRe = regexp.MustCompile(`https?://imgur\.com/(gallery|a)?/(.*)`)
var imgurRe2 = regexp.MustCompile(`https?://imgur\.com/(.*)`)
var imgRe = regexp.MustCompile(`https?://i\.imgur\.com/(.*)\.(png|gif|jpe?g|webp)`)
var vidRe = regexp.MustCompile(`https?://i\.imgur\.com/(.*)\.(mp4|webm)`)
var vidFormatRe = regexp.MustCompile(`\.(mp4|webm)`)
var iImgurRe = regexp.MustCompile(`https?://i\.imgur\.com`)
func parseComment(data gjson.Result) Comment {
createdTime, _ := time.Parse("2006-01-02T15:04:05Z", data.Get("created_at").String())
createdAt := createdTime.Format("January 2, 2006 3:04 PM")
updatedAt, _ := utils.FormatDate(data.Get("updated_at").String())
deletedAt, _ := utils.FormatDate(data.Get("deleted_at").String())
userAvatar := strings.ReplaceAll(data.Get("account.avatar").String(), "https://i.imgur.com", "")
wg := sync.WaitGroup{}
comments := make([]Comment, 0)
data.Get("comments").ForEach(
func(key, value gjson.Result) bool {
wg.Add(1)
go func() {
defer wg.Done()
comments = append(comments, parseComment(value))
}()
return true
},
)
wg.Wait()
comment := data.Get("comment").String()
comment = strings.ReplaceAll(comment, "\n", "<br>")
for _, match := range imgRe.FindAllString(comment, -1) {
img := iImgurRe.ReplaceAllString(match, "")
img = `<img src="` + img + `" class="comment__media" loading="lazy"/>`
comment = strings.Replace(comment, match, img, 1)
} }
return nil for _, match := range vidRe.FindAllString(comment, -1) {
} vid := iImgurRe.ReplaceAllString(match, "")
vid = `<video class="comment__media" controls loop preload="none" poster="` + vidFormatRe.ReplaceAllString(vid, ".webp") + `"><source type="` + strings.Split(vid, ".")[1] + `" src="` + vid + `" /></video>`
comment = strings.Replace(comment, match, vid, 1)
}
for _, l := range linkify.Links(comment) {
origLink := comment[l.Start:l.End]
link := `<a href="` + origLink + `">` + origLink + `</a>`
comment = strings.Replace(comment, origLink, link, 1)
}
comment = imgurRe.ReplaceAllString(comment, "/$1/$2")
comment = imgurRe2.ReplaceAllString(comment, "/$1")
type commentPost Submission p := bluemonday.UGCPolicy()
p.AllowImages()
p.AllowElements("video", "source")
p.AllowAttrs("src", "tvpe").OnElements("source")
p.AllowAttrs("controls", "loop", "preload", "poster").OnElements("video")
p.AllowAttrs("class", "loading").OnElements("img", "video")
p.RequireNoReferrerOnLinks(true)
p.RequireNoFollowOnLinks(true)
p.RequireCrossOriginAnonymous(true)
comment = p.Sanitize(comment)
func (post *commentPost) UnmarshalJSON(data []byte) error { return Comment{
*post = commentPost(parseSubmission(gjson.Parse(string(data)))) Comments: comments,
return nil User: User{
} Id: data.Get("account.id").Int(),
Username: data.Get("account.username").String(),
type commentApiResponse struct { Avatar: userAvatar,
Data commentArray `json:"data"` },
Post: parseSubmission(data.Get("post")),
Id: data.Get("id").String(),
Comment: comment,
Upvotes: data.Get("upvote_count").Int(),
Downvotes: data.Get("downvote_count").Int(),
Platform: data.Get("platform").String(),
CreatedAt: createdAt,
RelTime: humanize.Time(createdTime),
UpdatedAt: updatedAt,
DeletedAt: deletedAt,
}
} }

View File

@@ -11,19 +11,19 @@ import (
) )
type SearchResult struct { type SearchResult struct {
Id string Id string
Url string Url string
ImageUrl string ImageUrl string
Title string Title string
User string User string
Points string Points string
Views string Views string
RelTime string RelTime string
} }
func (client *Client) Search(query string, page string) ([]SearchResult, error) { func (client *Client) Search(query string, page string) ([]SearchResult, error) {
query = url.QueryEscape(query) 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 { if err != nil {
return []SearchResult{}, err return []SearchResult{}, err
} }
@@ -35,16 +35,16 @@ func (client *Client) Search(query string, page string) ([]SearchResult, error)
} }
defer res.Body.Close() defer res.Body.Close()
if res.StatusCode != 200 { 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) doc, err := goquery.NewDocumentFromReader(res.Body)
if err != nil { if err != nil {
return []SearchResult{}, err return []SearchResult{}, err
} }
results := []SearchResult{} 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") url, _ := s.Find("a").Attr("href")
imageUrl, _ := s.Find("img").Attr("src") 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") views = strings.TrimSuffix(views, " views")
result := SearchResult{ result := SearchResult{
Id: strings.Split(url, "/")[2], Id: strings.Split(url, "/")[2],
Url: url, Url: url,
ImageUrl: strings.ReplaceAll(imageUrl, "//i.imgur.com", ""), ImageUrl: strings.ReplaceAll(imageUrl, "//i.imgur.com", ""),
Title: s.Find(".search-item-title a").Text(), Title: s.Find(".search-item-title a").Text(),
User: s.Find(".account").Text(), User: s.Find(".account").Text(),
Views: views, Views: views,
Points: points, Points: points,
RelTime: strings.TrimSpace(postInfo[2]), RelTime: strings.TrimSpace(postInfo[2]),
} }
results = append(results, result) results = append(results, result)
}) })
return results, nil return results, nil
} }

View File

@@ -1,11 +1,11 @@
package api package api
import ( import (
"io" "io/ioutil"
"net/http" "net/http"
"net/url"
"strings" "strings"
"sync"
"github.com/patrickmn/go-cache" "github.com/patrickmn/go-cache"
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
) )
@@ -21,9 +21,6 @@ type Tag struct {
} }
func (client *Client) FetchTag(tag string, sort string, page string) (Tag, error) { 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") cacheData, found := client.Cache.Get(tag + sort + page + "-tag")
if found { if found {
return cacheData.(Tag), nil return cacheData.(Tag), nil
@@ -60,51 +57,54 @@ func (client *Client) FetchTag(tag string, sort string, page string) (Tag, error
return Tag{}, err return Tag{}, err
} }
body, err := io.ReadAll(res.Body) body, err := ioutil.ReadAll(res.Body)
if err != nil { if err != nil {
return Tag{}, err return Tag{}, err
} }
data := gjson.Parse(string(body)) data := gjson.Parse(string(body))
wg := sync.WaitGroup{}
posts := make([]Submission, 0) posts := make([]Submission, 0)
data.Get("posts").ForEach( data.Get("posts").ForEach(
func(key, value gjson.Result) bool { func(key, value gjson.Result) bool {
url, _ := url.Parse(strings.ReplaceAll(value.Get("url").String(), "https://imgur.com", "")) wg.Add(1)
q := url.Query()
q.Add("tag", tag+"."+sort+"."+page+"."+key.String())
url.RawQuery = q.Encode()
posts = append(posts, Submission{ go func() {
Id: value.Get("id").String(), defer wg.Done()
Title: value.Get("title").String(), posts = append(posts, Submission{
Link: url.String(), Id: value.Get("id").String(),
Cover: Media{ Title: value.Get("title").String(),
Id: value.Get("cover_id").String(), Link: strings.ReplaceAll(value.Get("url").String(), "https://imgur.com", ""),
Type: value.Get("cover.type").String(), Cover: Media{
Url: strings.ReplaceAll(value.Get("cover.url").String(), "https://i.imgur.com", ""), Id: value.Get("cover_id").String(),
}, Type: value.Get("cover.type").String(),
Points: value.Get("point_count").Int(), Url: strings.ReplaceAll(value.Get("cover.url").String(), "https://i.imgur.com", ""),
Upvotes: value.Get("upvote_count").Int(), },
Downvotes: value.Get("downvote_count").Int(), Points: value.Get("point_count").Int(),
Comments: value.Get("comment_count").Int(), Upvotes: value.Get("upvote_count").Int(),
Views: value.Get("view_count").Int(), Downvotes: value.Get("downvote_count").Int(),
IsAlbum: value.Get("is_album").Bool(), Comments: value.Get("comment_count").Int(),
}) Views: value.Get("view_count").Int(),
IsAlbum: value.Get("is_album").Bool(),
})
}()
return true return true
}, },
) )
wg.Wait()
tagData := Tag{ tagData := Tag{
Tag: tag, Tag: tag,
Display: data.Get("display").String(), Display: data.Get("display").String(),
Sort: sort, Sort: sort,
PostCount: data.Get("post_count").Int(), PostCount: data.Get("post_count").Int(),
Posts: posts, Posts: posts,
Background: "/" + data.Get("background_id").String() + ".webp", Background: "/" + data.Get("background_id").String() + ".webp",
} }
client.Cache.Set(tag+sort+page+"-tag", tagData, 4*cache.DefaultExpiration) client.Cache.Set(tag + sort + page + "-tag", tagData, cache.DefaultExpiration)
return tagData, nil return tagData, nil
} }

View File

@@ -1,7 +1,6 @@
package api package api
import ( import (
"encoding/json"
"io" "io"
"net/http" "net/http"
"strings" "strings"
@@ -155,7 +154,7 @@ func (client *Client) FetchUserFavorites(username string, sort string, page stri
func (client *Client) FetchUserComments(username string) ([]Comment, error) { func (client *Client) FetchUserComments(username string) ([]Comment, error) {
cacheData, found := client.Cache.Get(username + "-usercomments") cacheData, found := client.Cache.Get(username + "-usercomments")
if found { if found {
return cacheData.(commentArray), nil return cacheData.([]Comment), nil
} }
req, err := http.NewRequest("GET", "https://api.imgur.com/comment/v1/comments", nil) req, err := http.NewRequest("GET", "https://api.imgur.com/comment/v1/comments", nil)
@@ -177,19 +176,23 @@ func (client *Client) FetchUserComments(username string) ([]Comment, error) {
return []Comment{}, err return []Comment{}, err
} }
data, err := io.ReadAll(res.Body) body, err := io.ReadAll(res.Body)
if err != nil { if err != nil {
return []Comment{}, err return []Comment{}, err
} }
var parsed commentApiResponse data := gjson.Parse(string(body))
err = json.Unmarshal(data, &parsed)
if err != nil {
return []Comment{}, err
}
client.Cache.Set(username+"-usercomments", parsed.Data, cache.DefaultExpiration) comments := make([]Comment, 0)
return parsed.Data, nil data.Get("data").ForEach(
func(key, value gjson.Result) bool {
comments = append(comments, parseComment(value))
return true
},
)
client.Cache.Set(username+"-usercomments", comments, cache.DefaultExpiration)
return comments, nil
} }
func parseSubmission(value gjson.Result) Submission { func parseSubmission(value gjson.Result) Submission {

View File

@@ -1,3 +1,5 @@
version: '3'
services: services:
rimgo: rimgo:
image: codeberg.org/rimgo/rimgo # Official image image: codeberg.org/rimgo/rimgo # Official image

41
go.mod
View File

@@ -1,28 +1,41 @@
module codeberg.org/rimgo/rimgo module codeberg.org/rimgo/rimgo
go 1.24.0 go 1.17
require ( require (
github.com/PuerkitoBio/goquery v1.11.0 github.com/PuerkitoBio/goquery v1.8.1
github.com/aymerick/raymond v2.0.2+incompatible
github.com/dustin/go-humanize v1.0.1 github.com/dustin/go-humanize v1.0.1
github.com/gorilla/feeds v1.2.0 github.com/gofiber/fiber/v2 v2.48.0
github.com/gofiber/template/handlebars/v2 v2.1.4
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/mailgun/raymond/v2 v2.0.48 github.com/microcosm-cc/bluemonday v1.0.25
github.com/microcosm-cc/bluemonday v1.0.27
github.com/patrickmn/go-cache v2.1.0+incompatible github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/tidwall/gjson v1.18.0 github.com/tidwall/gjson v1.14.4
gitlab.com/golang-commonmark/linkify v0.0.0-20200225224916-64bca66f6ad3 gitlab.com/golang-commonmark/linkify v0.0.0-20200225224916-64bca66f6ad3
) )
require ( require (
github.com/andybalholm/cascadia v1.3.3 // indirect github.com/andybalholm/brotli v1.0.5 // indirect
github.com/andybalholm/cascadia v1.3.2 // indirect
github.com/aymerick/douceur v0.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect
github.com/gorilla/css v1.0.1 // indirect github.com/gofiber/template v1.8.2 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect github.com/gofiber/utils v1.1.0 // indirect
github.com/stretchr/testify v1.10.0 // indirect github.com/google/uuid v1.3.0 // indirect
github.com/tidwall/match v1.2.0 // indirect github.com/gorilla/css v1.0.0 // indirect
github.com/klauspost/compress v1.16.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-runewidth v0.0.14 // 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/tidwall/pretty v1.2.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect
golang.org/x/net v0.48.0 // indirect github.com/tinylib/msgp v1.1.8 // indirect
golang.org/x/sys v0.39.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
golang.org/x/text v0.32.0 // indirect github.com/valyala/fasthttp v1.48.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/net v0.12.0 // indirect
golang.org/x/sys v0.10.0 // indirect
golang.org/x/text v0.11.0 // indirect
) )

174
go.sum
View File

@@ -1,128 +1,148 @@
github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw= github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ= github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= github.com/andybalholm/brotli v1.0.5/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/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= 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/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/aymerick/raymond v2.0.2+incompatible h1:VEp3GpgdAnv9B2GFyTvqgcKvY+mfKMjPOA3SbKLtnU0=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/aymerick/raymond v2.0.2+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g=
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 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/gofiber/fiber/v2 v2.46.0/go.mod h1:DNl0/c37WLe0g92U6lx1VMQuxGUQY5V7EIaVoEsUffc=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gofiber/fiber/v2 v2.48.0 h1:cRVMCb9aUJDsyHxGFLwz/sGzDggdailZZyptU9F9cU0=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gofiber/fiber/v2 v2.48.0/go.mod h1:xqJgfqrc23FJuqGOW6DVgi3HyZEm2Mn9pRqUb2kHSX8=
github.com/gorilla/feeds v1.2.0 h1:O6pBiXJ5JHhPvqy53NsjKOThq+dNFm8+DFrxBEdzSCc= github.com/gofiber/template v1.8.2 h1:PIv9s/7Uq6m+Fm2MDNd20pAFFKt5wWs7ZBd8iV9pWwk=
github.com/gorilla/feeds v1.2.0/go.mod h1:WMib8uJP3BbY+X8Szd1rA5Pzhdfh+HCCAYT2z7Fza6Y= github.com/gofiber/template v1.8.2/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8=
github.com/gofiber/template/handlebars/v2 v2.1.4 h1:m/GwEnzv5bpifOg9BrpCIzkhD2GU10E5oZjI8NdDbYY=
github.com/gofiber/template/handlebars/v2 v2.1.4/go.mod h1:bb6ip6ZEgBqKSdZcbnFLlfL8PcCRslnX6WgpcxVBiTE=
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.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg=
github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= 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/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94/go.mod h1:90zrgN3D/WJsDd1iXHT96alCoN2KJo6/4x1DZC3wZs8=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d/go.mod h1:Gy+0tqhJvgGlqnTF8CVGP0AaGRjwBtXs/a5PA0Y3+A4=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
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.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.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw=
github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
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.47.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
github.com/valyala/fasthttp v1.48.0 h1:oJWvHb9BIZToTQS3MuQ2R3bJZiNSa2KiNdeI8A+79Tc=
github.com/valyala/fasthttp v1.48.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
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.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 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 h1:1Coh5BsUBlXoEJmIEaNzVAWrtg9k7/eJzailMQr1grw=
gitlab.com/golang-commonmark/linkify v0.0.0-20200225224916-64bca66f6ad3/go.mod h1:Gn+LZmCrhPECMD3SOKlE+BOHwhOYD9j7WT9NUtkCrC8= 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-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 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.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
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.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 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/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 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.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 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.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 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.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
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-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/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.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.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-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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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-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-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-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.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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-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.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
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.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
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.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.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.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
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-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.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 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= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 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=

View File

@@ -28,6 +28,21 @@
], ],
"cloudflare": false "cloudflare": false
}, },
{
"url": "https://rimgo.vern.cc",
"onion": "http://rimgo.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion",
"countries": [
"ca"
],
"cloudflare": false
},
{
"url": "https://rim.odyssey346.dev",
"countries": [
"fr"
],
"cloudflare": false
},
{ {
"url": "https://imgur.010032.xyz", "url": "https://imgur.010032.xyz",
"countries": [ "countries": [
@@ -82,6 +97,13 @@
], ],
"cloudflare": false "cloudflare": false
}, },
{
"url": "https://i.01r.xyz",
"countries": [
"us"
],
"cloudflare": true
},
{ {
"url": "https://rimgo.eu.projectsegfau.lt", "url": "https://rimgo.eu.projectsegfau.lt",
"countries": [ "countries": [
@@ -103,13 +125,6 @@
], ],
"cloudflare": false "cloudflare": false
}, },
{
"url": "https://rimgo.fascinated.cc",
"countries": [
"us"
],
"cloudflare": false
},
{ {
"url": "https://rimgo.whateveritworks.org", "url": "https://rimgo.whateveritworks.org",
"countries": [ "countries": [
@@ -137,26 +152,5 @@
"de" "de"
], ],
"cloudflare": false "cloudflare": false
},
{
"url": "https://rimgo.drgns.space",
"countries": [
"us"
],
"cloudflare": false
},
{
"url": "https://rimgo.quantenzitrone.eu",
"countries": [
"cz"
],
"cloudflare": false
},
{
"url": "https://rimgo.frylo.net",
"countries": [
"nl"
],
"cloudflare": false
} }
] ]

224
main.go
View File

@@ -3,37 +3,23 @@ package main
import ( import (
"flag" "flag"
"fmt" "fmt"
"io"
"net/http" "net/http"
"os" "os"
"strings" "time"
"codeberg.org/rimgo/rimgo/pages" "codeberg.org/rimgo/rimgo/pages"
"codeberg.org/rimgo/rimgo/render"
"codeberg.org/rimgo/rimgo/static" "codeberg.org/rimgo/rimgo/static"
"codeberg.org/rimgo/rimgo/utils" "codeberg.org/rimgo/rimgo/utils"
"codeberg.org/rimgo/rimgo/views" "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" "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 {
pages.RenderError(w, r, 500, fmt.Sprint(v))
}
}()
err := h(w, r)
if err != nil {
fmt.Println(err)
pages.RenderError(w, r, 500, err.Error())
}
})
}
func main() { func main() {
envPath := flag.String("c", ".env", "Path to env file") envPath := flag.String("c", ".env", "Path to env file")
godotenv.Load(*envPath) godotenv.Load(*envPath)
@@ -41,111 +27,113 @@ func main() {
pages.InitializeApiClient() pages.InitializeApiClient()
views := views.GetFiles() views := http.FS(views.GetFiles())
static := static.GetFiles() if os.Getenv("ENV") == "dev" {
render.Initialize(views) views = http.Dir("./views")
}
engine := handlebars.NewFileSystem(views, ".hbs")
app := http.NewServeMux() engine.AddFunc("noteq", func(a interface{}, b interface{}, options *raymond.Options) interface{} {
if raymond.Str(a) != raymond.Str(b) {
return options.Fn()
}
return ""
})
app.Handle("GET /static/", http.StripPrefix("/static/", http.FileServerFS(static))) app := fiber.New(fiber.Config{
app.Handle("GET /robots.txt", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { Views: engine,
file, _ := static.Open("robots.txt") Prefork: utils.Config.FiberPrefork,
defer file.Close() UnescapePath: true,
io.Copy(w, file) StreamRequestBody: true,
})) ErrorHandler: func(ctx *fiber.Ctx, err error) error {
app.Handle("GET /favicon.ico", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { code := fiber.StatusInternalServerError
file, _ := static.Open("favicon/favicon.ico")
defer file.Close() if e, ok := err.(*fiber.Error); ok {
io.Copy(w, file) 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)
},
})) }))
if os.Getenv("ENV") == "dev" { if os.Getenv("ENV") == "dev" {
app.Handle("GET /errors/429", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { app.Use("/static", filesystem.New(filesystem.Config{
pages.RenderError(w, r, 429) Root: http.Dir("./static"),
})) }))
app.Handle("GET /errors/429/img", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { app.Get("/errors/429", func(c *fiber.Ctx) error {
w.Header().Set("Location", "/static/img/error-429.png") return c.Render("errors/429", nil)
w.WriteHeader(302) })
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/404", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { app.Use(cache.New(cache.Config{
pages.RenderError(w, r, 404) Expiration: 30 * time.Minute,
})) MaxBytes: 25000000,
app.Handle("GET /errors/404/img", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { KeyGenerator: func(c *fiber.Ctx) string {
w.Header().Set("Location", "/static/img/error-404.png") return c.OriginalURL()
w.WriteHeader(302) },
})) CacheControl: true,
app.Handle("GET /errors/error", wrapHandler(func(w http.ResponseWriter, r *http.Request) error { StoreResponseHeaders: true,
return fmt.Errorf("Test error")
}))
app.Handle("GET /errors/panic", wrapHandler(func(w http.ResponseWriter, r *http.Request) error {
panic("Test error")
})) }))
} }
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
}))
addr := utils.Config.Addr + ":" + utils.Config.Port app.Get("/robots.txt", func(c *fiber.Ctx) error {
fmt.Println("listening on " + addr) file, _ := static.GetFiles().ReadFile("robots.txt")
err := http.ListenAndServe(addr, app) _, err := c.Write(file)
if err != nil { return err
fmt.Println(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)
} }

View File

@@ -1,10 +0,0 @@
{
"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"
}
}

View File

@@ -1,21 +1,22 @@
package pages package pages
import ( import (
"net/http" "os"
"codeberg.org/rimgo/rimgo/render"
"codeberg.org/rimgo/rimgo/utils" "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")
return render.Render(w, "about", map[string]any{ func HandleAbout(c *fiber.Ctx) error {
"proto": r.Proto, utils.SetHeaders(c)
"domain": r.Host, c.Set("X-Frame-Options", "DENY")
"force_webp": utils.Config.ForceWebp, 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"),
}) })
} }

View File

@@ -8,5 +8,5 @@ import (
var ApiClient *api.Client var ApiClient *api.Client
func InitializeApiClient() { func InitializeApiClient() {
ApiClient = api.NewClient(utils.Config.ImgurId) ApiClient = api.NewClient(utils.Config.ImgurId)
} }

View File

@@ -1,49 +1,48 @@
package pages package pages
import ( import (
"net/http"
"strings" "strings"
"codeberg.org/rimgo/rimgo/api" "codeberg.org/rimgo/rimgo/api"
"codeberg.org/rimgo/rimgo/render"
"codeberg.org/rimgo/rimgo/utils" "codeberg.org/rimgo/rimgo/utils"
"github.com/gofiber/fiber/v2"
) )
func HandleEmbed(w http.ResponseWriter, r *http.Request) error { func HandleEmbed(c *fiber.Ctx) error {
utils.SetHeaders(w) utils.SetHeaders(c)
w.Header().Set("Cache-Control", "public,max-age=31557600") c.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") 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")
post, err := api.Album{}, error(nil) post, err := api.Album{}, error(nil)
switch { switch {
case strings.HasPrefix(r.URL.Path, "/a"): case strings.HasPrefix(c.Path(), "/a"):
post, err = ApiClient.FetchAlbum(r.PathValue("postID")) post, err = ApiClient.FetchAlbum(c.Params("postID"))
case strings.HasPrefix(r.URL.Path, "/gallery"): case strings.HasPrefix(c.Path(), "/gallery"):
post, err = ApiClient.FetchPosts(r.PathValue("postID")) post, err = ApiClient.FetchPosts(c.Params("postID"))
default: default:
post, err = ApiClient.FetchMedia(r.PathValue("postID")) post, err = ApiClient.FetchMedia(c.Params("postID"))
} }
if err != nil && err.Error() == "ratelimited by imgur" { if err != nil && err.Error() == "ratelimited by imgur" {
return RenderError(w, r, 429) return c.Status(429).Render("errors/429", nil)
} }
if err != nil && post.Id == "" && strings.Contains(err.Error(), "404") { if err != nil && post.Id == "" && strings.Contains(err.Error(), "404") {
return RenderError(w, r, 404) return c.Status(404).Render("errors/404", nil)
} }
if err != nil { if err != nil {
return err return err
} }
return render.Render(w, "embed", map[string]any{ return c.Render("embed", fiber.Map{
"post": post, "post": post,
}) })
} }
func HandleGifv(w http.ResponseWriter, r *http.Request) error { func HandleGifv(c *fiber.Ctx) error {
utils.SetHeaders(w) utils.SetHeaders(c)
w.Header().Set("Cache-Control", "public,max-age=31557600") c.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") 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")
return render.Render(w, "gifv", map[string]any{ return c.Render("gifv", fiber.Map{
"id": r.PathValue("postID"), "id": c.Params("postID"),
}) })
} }

View File

@@ -1,44 +0,0 @@
package pages
import (
"fmt"
"io"
"net/http"
"strconv"
"codeberg.org/rimgo/rimgo/render"
"codeberg.org/rimgo/rimgo/static"
"codeberg.org/rimgo/rimgo/utils"
)
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 !utils.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
}

View File

@@ -1,22 +1,20 @@
package pages package pages
import ( import (
"net/http"
"codeberg.org/rimgo/rimgo/render"
"codeberg.org/rimgo/rimgo/utils" "codeberg.org/rimgo/rimgo/utils"
"github.com/gofiber/fiber/v2"
) )
var VersionInfo string var VersionInfo string
func HandleFrontpage(w http.ResponseWriter, r *http.Request) error { func HandleFrontpage(c *fiber.Ctx) error {
utils.SetHeaders(w) utils.SetHeaders(c)
w.Header().Set("X-Frame-Options", "DENY") c.Set("X-Frame-Options", "DENY")
w.Header().Set("Cache-Control", "public,max-age=31557600") c.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") 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 render.Render(w, "frontpage", map[string]any{ return c.Render("frontpage", fiber.Map{
"config": utils.Config, "config": utils.Config,
"version": VersionInfo, "version": VersionInfo,
}) })
} }

View File

@@ -1,70 +1,55 @@
package pages package pages
import ( import (
"io"
"mime"
"net/http" "net/http"
"os"
"strings" "strings"
"codeberg.org/rimgo/rimgo/utils" "codeberg.org/rimgo/rimgo/utils"
"github.com/gofiber/fiber/v2"
) )
func HandleMedia(w http.ResponseWriter, r *http.Request) error { func HandleMedia(c *fiber.Ctx) error {
w.Header().Set("Cache-Control", "public,max-age=31557600") c.Set("Cache-Control", "public,max-age=31557600")
w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'self'; img-src 'self'") if strings.HasPrefix(c.Path(), "/stack") {
splitName := strings.SplitN(r.PathValue("component"), ".", 2) return handleMedia(c, "https://i.stack.imgur.com/" + strings.ReplaceAll(c.Params("baseName"), "stack/", "") + "." + c.Params("extension"))
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 { } else {
return handleMedia(w, r, "https://i.imgur.com/"+baseName+"."+extension) return handleMedia(c, "https://i.imgur.com/" + c.Params("baseName") + "." + c.Params("extension"))
} }
} }
func HandleUserCover(w http.ResponseWriter, r *http.Request) error { func HandleUserCover(c *fiber.Ctx) error {
w.Header().Set("Cache-Control", "public,max-age=604800") c.Set("Cache-Control", "public,max-age=604800")
w.Header().Set("Content-Security-Policy", "default-src 'none'") return handleMedia(c, "https://imgur.com/user/" + c.Params("userID") + "/cover?maxwidth=2560")
return handleMedia(w, r, "https://imgur.com/user/"+r.PathValue("userID")+"/cover?maxwidth=2560") };
}
func HandleUserAvatar(w http.ResponseWriter, r *http.Request) error { func HandleUserAvatar(c *fiber.Ctx) error {
w.Header().Set("Cache-Control", "public,max-age=604800") c.Set("Cache-Control", "public,max-age=604800")
w.Header().Set("Content-Security-Policy", "default-src 'none'") return handleMedia(c, "https://imgur.com/user/" + c.Params("userID") + "/avatar")
return handleMedia(w, r, "https://imgur.com/user/"+r.PathValue("userID")+"/avatar") };
}
func handleMedia(w http.ResponseWriter, r *http.Request, url string) error { func handleMedia(c *fiber.Ctx, url string) error {
utils.SetHeaders(w) utils.SetHeaders(c)
path := r.URL.Path
if utils.Config.ForceWebp && if os.Getenv("FORCE_WEBP") == "1" && c.Query("no_webp") == "" && c.Accepts("image/webp") == "image/webp" && !strings.HasPrefix(c.Path(), "/stack") {
!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, ".png", ".webp")
url = strings.ReplaceAll(url, ".jpg", ".webp") url = strings.ReplaceAll(url, ".jpg", ".webp")
url = strings.ReplaceAll(url, ".jpeg", ".webp") url = strings.ReplaceAll(url, ".jpeg", ".webp")
filename := strings.TrimPrefix(path, "/")
w.Header().Set("Content-Disposition", mime.FormatMediaType("attachment", map[string]string{"filename*": filename}))
} }
queryStr := r.URL.Query().Encode() if strings.HasPrefix(c.Path(), "/stack") && strings.Contains(c.OriginalURL(), "?") {
if strings.HasPrefix(path, "/stack") && queryStr != "" { url = url + "?" + strings.Split(c.OriginalURL(), "?")[1]
url = url + "?" + queryStr
} }
req, err := http.NewRequest("GET", url, nil) req, err := http.NewRequest("GET", url, nil)
if err != nil { if err != nil {
return err return err
} }
utils.SetReqHeaders(req) utils.SetReqHeaders(req)
rng := r.URL.Query().Get("Range") if c.Get("Range") != "" {
if rng != "" { req.Header.Set("Range", c.Get("Range"))
req.Header.Set("Range", rng)
} }
res, err := http.DefaultClient.Do(req) res, err := http.DefaultClient.Do(req)
@@ -72,19 +57,23 @@ func handleMedia(w http.ResponseWriter, r *http.Request, url string) error {
return err return err
} }
if res.StatusCode == 404 || strings.Contains(res.Request.URL.String(), "error/404") { c.Status(res.StatusCode)
return RenderError(w, r, 404) if res.StatusCode == 404 {
return c.Render("errors/404", fiber.Map{
"path": c.Path(),
})
} else if res.StatusCode == 429 { } else if res.StatusCode == 429 {
return RenderError(w, r, 429) return c.Render("errors/429", fiber.Map{
"path": c.Path(),
})
} }
w.Header().Set("Accept-Ranges", "bytes") c.Set("Accept-Ranges", "bytes")
w.Header().Set("Content-Type", res.Header.Get("Content-Type")) c.Set("Content-Type", res.Header.Get("Content-Type"));
w.Header().Set("Content-Length", res.Header.Get("Content-Length")) c.Set("Content-Length", res.Header.Get("Content-Length"))
if res.Header.Get("Content-Range") != "" { if res.Header.Get("Content-Range") != "" {
w.Header().Set("Content-Range", res.Header.Get("Content-Range")) c.Set("Content-Range", res.Header.Get("Content-Range"))
} }
_, err = io.Copy(w, res.Body) return c.SendStream(res.Body)
return err }
}

View File

@@ -3,76 +3,49 @@ package pages
import ( import (
"crypto/rand" "crypto/rand"
"fmt" "fmt"
"net/http"
"strconv"
"strings" "strings"
"codeberg.org/rimgo/rimgo/api" "codeberg.org/rimgo/rimgo/api"
"codeberg.org/rimgo/rimgo/render"
"codeberg.org/rimgo/rimgo/utils" "codeberg.org/rimgo/rimgo/utils"
"github.com/gofiber/fiber/v2"
) )
// Cursed function func HandlePost(c *fiber.Ctx) error {
func nextInTag(client *api.Client, tagname, sort, page, I string) string { utils.SetHeaders(c)
i, err := strconv.Atoi(I) c.Set("X-Frame-Options", "DENY")
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) post, err := api.Album{}, error(nil)
switch { switch {
case strings.HasPrefix(r.URL.Path, "/a"): case strings.HasPrefix(c.Path(), "/a"):
post, err = ApiClient.FetchAlbum(postId) post, err = ApiClient.FetchAlbum(c.Params("postID"))
case strings.HasPrefix(r.URL.Path, "/gallery"): case strings.HasPrefix(c.Path(), "/gallery"):
post, err = ApiClient.FetchPosts(postId) post, err = ApiClient.FetchPosts(c.Params("postID"))
case strings.HasPrefix(r.URL.Path, "/t"): case strings.HasPrefix(c.Path(), "/t"):
post, err = ApiClient.FetchPosts(postId) post, err = ApiClient.FetchPosts(c.Params("postID"))
default: default:
post, err = ApiClient.FetchMedia(postId) post, err = ApiClient.FetchMedia(c.Params("postID"))
} }
if err != nil && err.Error() == "ratelimited by imgur" { if err != nil && err.Error() == "ratelimited by imgur" {
return RenderError(w, r, 429) return c.Status(429).Render("errors/429", fiber.Map{
"path": c.Path(),
})
} }
if err != nil && post.Id == "" && strings.Contains(err.Error(), "404") { if err != nil && post.Id == "" && strings.Contains(err.Error(), "404") {
return RenderError(w, r, 404) return c.Status(404).Render("errors/404", nil)
} }
if err != nil { if err != nil {
return err return err
} }
comments := []api.Comment{} comments := []api.Comment{}
if post.SharedWithCommunity { if post.SharedWithCommunity {
w.Header().Set("Cache-Control", "public,max-age=604800") c.Set("Cache-Control", "public,max-age=604800")
comments, err = ApiClient.FetchComments(postId) comments, err = ApiClient.FetchComments(c.Params("postID"))
if err != nil { if err != nil {
return err return err
} }
} else { } else {
w.Header().Set("Cache-Control", "public,max-age=31557600") c.Set("Cache-Control", "public,max-age=31557600")
} }
nonce := "" nonce := ""
@@ -83,18 +56,10 @@ func HandlePost(w http.ResponseWriter, r *http.Request) error {
nonce = fmt.Sprintf("%x", b) nonce = fmt.Sprintf("%x", b)
csp = csp + " 'nonce-" + nonce + "'" csp = csp + " 'nonce-" + nonce + "'"
} }
w.Header().Set("Content-Security-Policy", csp) c.Set("Content-Security-Policy", csp)
var next string return c.Render("post", fiber.Map{
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, "post": post,
"next": next,
"comments": comments, "comments": comments,
"nonce": nonce, "nonce": nonce,
}) })

View File

@@ -1,18 +1,16 @@
package pages package pages
import ( import (
"net/http" "github.com/gofiber/fiber/v2"
"codeberg.org/rimgo/rimgo/render"
"codeberg.org/rimgo/rimgo/utils" "codeberg.org/rimgo/rimgo/utils"
) )
func HandlePrivacy(w http.ResponseWriter, r *http.Request) error { func HandlePrivacy(c *fiber.Ctx) error {
utils.SetHeaders(w) utils.SetHeaders(c)
w.Header().Set("X-Frame-Options", "DENY") c.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 render.Render(w, "privacy", map[string]any{ return c.Render("privacy", fiber.Map{
"config": utils.Config, "config": utils.Config,
"version": VersionInfo, "version": VersionInfo,
}) })

View File

@@ -1,143 +0,0 @@
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 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
}

View File

@@ -1,47 +1,50 @@
package pages package pages
import ( import (
"net/http"
"strconv" "strconv"
"codeberg.org/rimgo/rimgo/render"
"codeberg.org/rimgo/rimgo/utils" "codeberg.org/rimgo/rimgo/utils"
"github.com/gofiber/fiber/v2"
) )
func HandleSearch(w http.ResponseWriter, r *http.Request) error { func HandleSearch(c *fiber.Ctx) error {
utils.SetHeaders(w) utils.SetHeaders(c)
w.Header().Set("X-Frame-Options", "DENY") c.Set("X-Frame-Options", "DENY")
w.Header().Set("Cache-Control", "public,max-age=604800") c.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") 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")
query := r.URL.Query().Get("q") query := c.Query("q")
if utils.ImgurRe.MatchString(query) { if utils.ImgurRe.MatchString(query) {
w.Header().Set("Location", utils.ImgurRe.ReplaceAllString(query, "")) return c.Redirect(utils.ImgurRe.ReplaceAllString(query, ""))
w.WriteHeader(302)
return nil
} }
page := r.URL.Query().Get("page") page := "0"
if page == "" { if c.Query("page") != "" {
page = "0" page = c.Query("page")
} }
pageNumber, err := strconv.Atoi(page) pageNumber, err := strconv.Atoi(c.Query("page"))
if err != nil { if err != nil {
pageNumber = 0 pageNumber = 0
} }
displayPrevPage := true
if page == "0" {
displayPrevPage = false
}
results, err := ApiClient.Search(query, page) results, err := ApiClient.Search(query, page)
if err != nil { if err != nil {
return err return err
} }
return render.Render(w, "search", map[string]any{ return c.Render("search", fiber.Map{
"query": query, "query": query,
"results": results, "results": results,
"page": pageNumber, "page": pageNumber + 1,
"nextPage": pageNumber + 1, "displayPrev": displayPrevPage,
"prevPage": pageNumber - 1, "nextPage": pageNumber + 1,
"prevPage": pageNumber - 1,
}) })
} }

View File

@@ -1,44 +1,51 @@
package pages package pages
import ( import (
"net/http"
"strconv" "strconv"
"codeberg.org/rimgo/rimgo/render"
"codeberg.org/rimgo/rimgo/utils" "codeberg.org/rimgo/rimgo/utils"
"github.com/gofiber/fiber/v2"
) )
func HandleTag(w http.ResponseWriter, r *http.Request) error { func HandleTag(c *fiber.Ctx) error {
utils.SetHeaders(w) utils.SetHeaders(c)
w.Header().Set("X-Frame-Options", "DENY") c.Set("X-Frame-Options", "DENY")
w.Header().Set("Cache-Control", "public,max-age=604800") c.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") 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")
page := r.URL.Query().Get("page") page := "1"
if page == "" { if c.Query("page") != "" {
page = "1" page = c.Query("page")
} }
pageNumber, err := strconv.Atoi(page) pageNumber, err := strconv.Atoi(c.Query("page"))
if err != nil { if err != nil {
pageNumber = 0 pageNumber = 0
} }
tag, err := ApiClient.FetchTag(r.PathValue("tag"), r.URL.Query().Get("sort"), page) displayPrevPage := true
if page == "1" {
displayPrevPage = false
}
tag, err := ApiClient.FetchTag(c.Params("tag"), c.Query("sort"), page)
if err != nil && err.Error() == "ratelimited by imgur" { if err != nil && err.Error() == "ratelimited by imgur" {
return RenderError(w, r, 429) return c.Status(429).Render("errors/429", fiber.Map{
"path": c.Path(),
})
} }
if err != nil { if err != nil {
return err return err
} }
if tag.Display == "" { if tag.Display == "" {
return RenderError(w, r, 404) return c.Status(404).Render("errors/404", nil)
} }
return render.Render(w, "tag", map[string]any{ return c.Render("tag", fiber.Map{
"tag": tag, "tag": tag,
"page": page, "page": page,
"nextPage": pageNumber + 1, "displayPrev": displayPrevPage,
"prevPage": pageNumber - 1, "nextPage": pageNumber + 1,
"prevPage": pageNumber - 1,
}) })
} }

View File

@@ -1,53 +1,58 @@
package pages package pages
import ( import (
"net/http"
"strconv" "strconv"
"codeberg.org/rimgo/rimgo/render"
"codeberg.org/rimgo/rimgo/utils" "codeberg.org/rimgo/rimgo/utils"
"github.com/gofiber/fiber/v2"
) )
func HandleTrending(w http.ResponseWriter, r *http.Request) error { func HandleTrending(c *fiber.Ctx) error {
utils.SetHeaders(w) utils.SetHeaders(c)
w.Header().Set("X-Frame-Options", "DENY") c.Set("X-Frame-Options", "DENY")
w.Header().Set("Cache-Control", "public,max-age=604800") c.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") 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")
page := r.URL.Query().Get("page") page := "1"
if page == "" { if c.Query("page") != "" {
page = "1" page = c.Query("page")
} }
pageNumber, err := strconv.Atoi(page) pageNumber, err := strconv.Atoi(c.Query("page"))
if err != nil { if err != nil {
pageNumber = 1 pageNumber = 1
} }
section := r.URL.Query().Get("section") section := c.Query("section")
switch section { switch section {
case "hot", "new", "top": case "hot", "new", "top":
default: default:
section = "hot" section = "hot"
} }
sort := r.URL.Query().Get("sort") sort := c.Query("sort")
switch sort { switch sort {
case "newest", "best", "popular": case "newest", "best", "popular":
default: default:
sort = "popular" sort = "popular"
} }
displayPrevPage := true
if page == "1" {
displayPrevPage = false
}
results, err := ApiClient.FetchTrending(section, sort, page) results, err := ApiClient.FetchTrending(section, sort, page)
if err != nil { if err != nil {
return err return err
} }
return render.Render(w, "trending", map[string]any{ return c.Render("trending", fiber.Map{
"results": results, "results": results,
"section": section, "section": section,
"sort": sort, "sort": sort,
"page": pageNumber, "page": pageNumber,
"nextPage": pageNumber + 1, "displayPrev": displayPrevPage,
"prevPage": pageNumber - 1, "nextPage": pageNumber + 1,
"prevPage": pageNumber - 1,
}) })
} }

View File

@@ -1,49 +1,53 @@
package pages package pages
import ( import (
"net/http"
"strconv" "strconv"
"codeberg.org/rimgo/rimgo/render"
"codeberg.org/rimgo/rimgo/utils" "codeberg.org/rimgo/rimgo/utils"
"github.com/gofiber/fiber/v2"
) )
func HandleUser(w http.ResponseWriter, r *http.Request) error { func HandleUser(c *fiber.Ctx) error {
utils.SetHeaders(w) utils.SetHeaders(c)
w.Header().Set("X-Frame-Options", "DENY") c.Set("X-Frame-Options", "DENY")
w.Header().Set("Cache-Control", "public,max-age=604800") c.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") 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")
page := r.URL.Query().Get("page") page := "0"
if page == "" { if c.Query("page") != "" {
page = "0" page = c.Query("page")
} }
pageNumber, err := strconv.Atoi(page) pageNumber, err := strconv.Atoi(c.Query("page"))
if err != nil { if err != nil {
pageNumber = 0 pageNumber = 0
} }
user, err := ApiClient.FetchUser(r.PathValue("userID")) user, err := ApiClient.FetchUser(c.Params("userID"))
if err != nil && err.Error() == "ratelimited by imgur" { if err != nil && err.Error() == "ratelimited by imgur" {
return RenderError(w, r, 429) return c.Status(429).Render("errors/429", fiber.Map{
"path": c.Path(),
})
} }
if err != nil { if err != nil {
return err return err
} }
if user.Username == "" { if user.Username == "" {
return RenderError(w, r, 404) return c.Status(404).Render("errors/404", nil)
} }
submissions, err := ApiClient.FetchSubmissions(r.PathValue("userID"), "newest", page) submissions, err := ApiClient.FetchSubmissions(c.Params("userID"), "newest", page)
if err != nil && err.Error() == "ratelimited by imgur" { if err != nil && err.Error() == "ratelimited by imgur" {
return RenderError(w, r, 429) c.Status(429)
return c.Render("errors/429", fiber.Map{
"path": c.Path(),
})
} }
if err != nil { if err != nil {
return err return err
} }
return render.Render(w, "user", map[string]any{ return c.Render("user", fiber.Map{
"user": user, "user": user,
"submissions": submissions, "submissions": submissions,
"page": page, "page": page,
@@ -52,73 +56,83 @@ func HandleUser(w http.ResponseWriter, r *http.Request) error {
}) })
} }
func HandleUserComments(w http.ResponseWriter, r *http.Request) error { func HandleUserComments(c *fiber.Ctx) error {
utils.SetHeaders(w) utils.SetHeaders(c)
w.Header().Set("X-Frame-Options", "DENY") c.Set("X-Frame-Options", "DENY")
w.Header().Set("Cache-Control", "public,max-age=604800") c.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") 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")
user, err := ApiClient.FetchUser(r.PathValue("userID")) user, err := ApiClient.FetchUser(c.Params("userID"))
if err != nil && err.Error() == "ratelimited by imgur" { if err != nil && err.Error() == "ratelimited by imgur" {
return RenderError(w, r, 429) return c.Status(429).Render("errors/429", fiber.Map{
"path": c.Path(),
})
} }
if err != nil { if err != nil {
return err return err
} }
if user.Username == "" { if user.Username == "" {
return RenderError(w, r, 404) return c.Status(404).Render("errors/404", nil)
} }
comments, err := ApiClient.FetchUserComments(r.PathValue("userID")) comments, err := ApiClient.FetchUserComments(c.Params("userID"))
if err != nil && err.Error() == "ratelimited by imgur" { if err != nil && err.Error() == "ratelimited by imgur" {
return RenderError(w, r, 429) c.Status(429)
return c.Render("errors/429", fiber.Map{
"path": c.Path(),
})
} }
if err != nil { if err != nil {
return err return err
} }
return render.Render(w, "userComments", map[string]any{ return c.Render("userComments", fiber.Map{
"user": user, "user": user,
"comments": comments, "comments": comments,
}) })
} }
func HandleUserFavorites(w http.ResponseWriter, r *http.Request) error { func HandleUserFavorites(c *fiber.Ctx) error {
utils.SetHeaders(w) utils.SetHeaders(c)
w.Header().Set("X-Frame-Options", "DENY") c.Set("X-Frame-Options", "DENY")
w.Header().Set("Cache-Control", "public,max-age=604800") c.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") 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")
page := r.URL.Query().Get("page") page := "0"
if page == "" { if c.Query("page") != "" {
page = "0" page = c.Query("page")
} }
pageNumber, err := strconv.Atoi(page) pageNumber, err := strconv.Atoi(c.Query("page"))
if err != nil { if err != nil {
pageNumber = 0 pageNumber = 0
} }
user, err := ApiClient.FetchUser(r.PathValue("userID")) user, err := ApiClient.FetchUser(c.Params("userID"))
if err != nil && err.Error() == "ratelimited by imgur" { if err != nil && err.Error() == "ratelimited by imgur" {
return RenderError(w, r, 429) return c.Status(429).Render("errors/429", fiber.Map{
"path": c.Path(),
})
} }
if err != nil { if err != nil {
return err return err
} }
if user.Username == "" { if user.Username == "" {
return RenderError(w, r, 404) return c.Status(404).Render("errors/404", nil)
} }
favorites, err := ApiClient.FetchUserFavorites(r.PathValue("userID"), "newest", page) favorites, err := ApiClient.FetchUserFavorites(c.Params("userID"), "newest", page)
if err != nil && err.Error() == "ratelimited by imgur" { if err != nil && err.Error() == "ratelimited by imgur" {
return RenderError(w, r, 429) c.Status(429)
return c.Render("errors/429", fiber.Map{
"path": c.Path(),
})
} }
if err != nil { if err != nil {
return err return err
} }
return render.Render(w, "userFavorites", map[string]any{ return c.Render("userFavorites", fiber.Map{
"user": user, "user": user,
"favorites": favorites, "favorites": favorites,
"page": page, "page": page,

547
pnpm-lock.yaml generated
View File

@@ -1,547 +0,0 @@
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

View File

@@ -1,44 +0,0 @@
package render
import (
"time"
"codeberg.org/rimgo/rimgo/utils"
"github.com/dustin/go-humanize"
"github.com/mailgun/raymond/v2"
)
func (r *renderer) registerHelpers() {
funcmap := map[string]any{
"noteq": noteq,
"ifNonZeroTime": ifNonZeroTime,
"relTime": relTime,
"rewriteUrl": rewriteUrl,
"sanitizeDescription": sanitizeDescription,
"sanitizeComment": sanitizeComment,
}
raymond.RegisterHelpers(funcmap)
}
func noteq(a, b any, options *raymond.Options) any {
if raymond.Str(a) != raymond.Str(b) {
return options.Fn()
}
return ""
}
func ifNonZeroTime(v any, options *raymond.Options) any {
if v.(time.Time).IsZero() {
return ""
}
return options.Fn()
}
func relTime(date time.Time) string {
return humanize.Time(date)
}
func rewriteUrl(link string) string {
r, err := utils.RewriteUrl(link)
if err != nil {
panic(err)
}
return r
}

View File

@@ -1,76 +0,0 @@
// 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
}

View File

@@ -1,53 +0,0 @@
package render
import (
"regexp"
"strings"
"github.com/microcosm-cc/bluemonday"
"gitlab.com/golang-commonmark/linkify"
)
var imgurRe = regexp.MustCompile(`https?://imgur\.com/(gallery|a)?/(.*)`)
var imgurRe2 = regexp.MustCompile(`https?://imgur\.com/(.*)`)
var imgRe = regexp.MustCompile(`https?://i\.imgur\.com/(.*)\.(png|gif|jpe?g|webp)`)
var vidRe = regexp.MustCompile(`https?://i\.imgur\.com/(.*)\.(mp4|webm)`)
var vidFormatRe = regexp.MustCompile(`\.(mp4|webm)`)
var iImgurRe = regexp.MustCompile(`https?://i\.imgur\.com`)
func sanitizeDescription(src string) string {
src = strings.ReplaceAll(src, "\n", "<br>")
return bluemonday.UGCPolicy().Sanitize(src)
}
func sanitizeComment(src string) string {
src = strings.ReplaceAll(src, "\n", "<br>")
for _, match := range imgRe.FindAllString(src, -1) {
img := iImgurRe.ReplaceAllString(match, "")
img = `<img src="` + img + `" class="comment__media" loading="lazy"/>`
src = strings.Replace(src, match, img, 1)
}
for _, match := range vidRe.FindAllString(src, -1) {
vid := iImgurRe.ReplaceAllString(match, "")
vid = `<video class="comment__media" controls loop preload="none" poster="` + vidFormatRe.ReplaceAllString(vid, ".webp") + `"><source type="` + strings.Split(vid, ".")[1] + `" src="` + vid + `" /></video>`
src = strings.Replace(src, match, vid, 1)
}
for _, l := range linkify.Links(src) {
origLink := (src)[l.Start:l.End]
link := `<a href="` + origLink + `">` + origLink + `</a>`
src = strings.Replace(src, origLink, link, 1)
}
src = imgurRe.ReplaceAllString(src, "/$1/$2")
src = imgurRe2.ReplaceAllString(src, "/$1")
p := bluemonday.UGCPolicy()
p.AllowImages()
p.AllowElements("video", "source")
p.AllowAttrs("src", "tvpe").OnElements("source")
p.AllowAttrs("controls", "loop", "preload", "poster").OnElements("video")
p.AllowAttrs("class", "loading").OnElements("img", "video")
p.RequireNoReferrerOnLinks(true)
p.RequireNoFollowOnLinks(true)
p.RequireCrossOriginAnonymous(true)
return p.Sanitize(src)
}

View File

@@ -7,4 +7,4 @@ var files embed.FS
func GetFiles() embed.FS { func GetFiles() embed.FS {
return files return files
} }

View File

@@ -1,7 +1,6 @@
{ {
"name": "rimgo", "name": "rimgo",
"short_name": "rimgo", "short_name": "rimgo",
"start_url": "/",
"icons": [ "icons": [
{ {
"src": "/static/favicon/android-chrome-192x192.png", "src": "/static/favicon/android-chrome-192x192.png",

View File

@@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 508 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -1,4 +1,6 @@
@import "tailwindcss" source("../views"); @tailwind base;
@tailwind components;
@tailwind utilities;
body { body {
margin: 0 24vw; margin: 0 24vw;

8
tailwind.config.js Normal file
View File

@@ -0,0 +1,8 @@
module.exports = {
content: ["./views/*.hbs", "./views/**/*.hbs"],
theme: {
extend: {},
},
variants: {},
plugins: [],
};

View File

@@ -1,22 +0,0 @@
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
}

View File

@@ -6,51 +6,60 @@ import (
) )
type config struct { type config struct {
Port string Port string
Addr string Addr string
ImgurId string ImgurId string
ProtocolDetection bool FiberPrefork bool
Secure bool ImageCache bool
ForceWebp bool CleanupInterval time.Duration
ImageCache bool CacheDir string
CleanupInterval time.Duration Privacy map[string]interface{}
CacheDir string
Privacy map[string]interface{}
} }
var Config config 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() { 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{ Config = config{
Port: envString("PORT", "3000"), Port: port,
Addr: envString("ADDR", "0.0.0.0"), Addr: addr,
ImgurId: envString("IMGUR_CLIENT_ID", "546c25a59c58ad7"), ImgurId: imgurId,
ProtocolDetection: envBool("PROTOCOL_DETECTION"), FiberPrefork: os.Getenv("FIBER_PREFORK") == "true",
Secure: envBool("SECURE"),
ForceWebp: envBool("FORCE_WEBP"),
Privacy: map[string]interface{}{ Privacy: map[string]interface{}{
"set": os.Getenv("PRIVACY_NOT_COLLECTED") != "", "set": os.Getenv("PRIVACY_NOT_COLLECTED") != "",
"policy": os.Getenv("PRIVACY_POLICY"), "policy": os.Getenv("PRIVACY_POLICY"),
"message": os.Getenv("PRIVACY_MESSAGE"), "message": os.Getenv("PRIVACY_MESSAGE"),
"country": os.Getenv("PRIVACY_COUNTRY"), "country": os.Getenv("PRIVACY_COUNTRY"),
"provider": os.Getenv("PRIVACY_PROVIDER"), "provider": os.Getenv("PRIVACY_PROVIDER"),
"cloudflare": envBool("PRIVACY_CLOUDFLARE"), "cloudflare": os.Getenv("PRIVACY_CLOUDFLARE") == "true",
"not_collected": envBool("PRIVACY_NOT_COLLECTED"), "not_collected": os.Getenv("PRIVACY_NOT_COLLECTED") == "true",
"ip": envBool("PRIVACY_IP"), "ip": os.Getenv("PRIVACY_IP") == "true",
"url": envBool("PRIVACY_URL"), "url": os.Getenv("PRIVACY_URL") == "true",
"device": envBool("PRIVACY_DEVICE"), "device": os.Getenv("PRIVACY_DEVICE") == "true",
"diagnostics": envBool("PRIVACY_DIAGNOSTICS"), "diagnostics": os.Getenv("PRIVACY_DIAGNOSTICS") == "true",
}, },
} }
} }

View File

@@ -9,4 +9,4 @@ func FormatDate(date string) (string, error) {
} }
return time.Format("Jan 2, 2006 3:04 PM"), nil return time.Format("Jan 2, 2006 3:04 PM"), nil
} }

View File

@@ -1,21 +0,0 @@
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
}

View File

@@ -2,4 +2,4 @@ package utils
import "regexp" import "regexp"
var ImgurRe = regexp.MustCompile(`https?://(i\.)?imgur\.com`) var ImgurRe = regexp.MustCompile(`https?://(i\.)?imgur\.com`)

View File

@@ -1,7 +1,6 @@
package utils package utils
import ( import (
"encoding/json"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
@@ -10,42 +9,6 @@ import (
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
) )
func GetJSONNew(url string) (json.RawMessage, error) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return json.RawMessage{}, err
}
SetReqHeaders(req)
client := http.Client{}
res, err := client.Do(req)
if err != nil {
return json.RawMessage{}, err
}
rateLimitRemaining := res.Header.Get("X-RateLimit-UserRemaining")
if rateLimitRemaining != "" {
ratelimit, _ := strconv.Atoi(rateLimitRemaining)
if ratelimit <= 0 {
return json.RawMessage{}, fmt.Errorf("ratelimited by imgur")
}
}
body, err := io.ReadAll(res.Body)
if err != nil {
return json.RawMessage{}, err
}
switch res.StatusCode {
case 200:
return body, nil
case 429:
return json.RawMessage{}, fmt.Errorf("ratelimited by imgur")
default:
return json.RawMessage{}, fmt.Errorf("received status %s, expected 200 OK.\n%s", res.Status, string(body))
}
}
func GetJSON(url string) (gjson.Result, error) { func GetJSON(url string) (gjson.Result, error) {
req, err := http.NewRequest("GET", url, nil) req, err := http.NewRequest("GET", url, nil)
if err != nil { if err != nil {
@@ -72,7 +35,7 @@ func GetJSON(url string) (gjson.Result, error) {
return gjson.Result{}, err return gjson.Result{}, err
} }
switch res.StatusCode { switch (res.StatusCode) {
case 200: case 200:
return gjson.Parse(string(body)), nil return gjson.Parse(string(body)), nil
case 429: case 429:
@@ -80,4 +43,4 @@ func GetJSON(url string) (gjson.Result, error) {
default: default:
return gjson.Result{}, fmt.Errorf("received status %s, expected 200 OK.\n%s", res.Status, string(body)) return gjson.Result{}, fmt.Errorf("received status %s, expected 200 OK.\n%s", res.Status, string(body))
} }
} }

View File

@@ -1,25 +0,0 @@
package utils
import (
"fmt"
"net/url"
"strings"
)
func RewriteUrl(link string) (string, error) {
url, err := url.Parse(link)
if err != nil {
return "", err
}
path := url.Path
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
switch url.Host {
case "", "imgur.com", "www.imgur.com", "i.imgur.com":
return path, nil
case "i.stack.imgur.com":
return "/stack" + path, nil
}
return "", fmt.Errorf("unknown host %s", url.Host)
}

View File

@@ -2,14 +2,16 @@ package utils
import ( import (
"net/http" "net/http"
"github.com/gofiber/fiber/v2"
) )
func SetHeaders(w http.ResponseWriter) { func SetHeaders(c *fiber.Ctx) {
w.Header().Set("Referrer-Policy", "no-referrer") c.Set("Referrer-Policy", "no-referrer")
w.Header().Set("X-Content-Type-Options", "nosniff") c.Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Robots-Tag", "noindex, noimageindex, nofollow") c.Set("X-Robots-Tag", "noindex, noimageindex, nofollow")
w.Header().Set("Strict-Transport-Security", "max-age=31557600") c.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=()") 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 SetReqHeaders(req *http.Request) { func SetReqHeaders(req *http.Request) {
@@ -23,4 +25,4 @@ func SetReqHeaders(req *http.Request) {
req.Header.Set("Sec-Fetch-Mode", "cors") req.Header.Set("Sec-Fetch-Mode", "cors")
req.Header.Set("Sec-Fetch-Site", "same-site") 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") req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; rv:102.0) Gecko/20100101 Firefox/102.0")
} }

View File

@@ -1,16 +0,0 @@
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
}

View File

@@ -7,4 +7,4 @@ var files embed.FS
func GetFiles() embed.FS { func GetFiles() embed.FS {
return files return files
} }

View File

@@ -25,8 +25,8 @@
rimgo is not affiliated with Imgur, all content is proxied from Imgur. rimgo is not affiliated with Imgur, all content is proxied from Imgur.
</p> </p>
<br/> <br/>
<h3 class="font-bold text-xl">Legal notice</h3> <h3 class="font-bold text-xl">Notice</h3>
<p>rimgo does not allow uploads or host any content, media. All content on any rimgo instances is from Imgur. Imgur is a trademark of Imgur, Inc. Any issues with content on rimgo should be be reported to Imgur. rimgo is not affiliated with Imgur, Inc.</p> <p>All images and media are from Imgur. rimgo does not allow uploads or comments. Any issues with content should be reported to Imgur.</p>
</main> </main>
<h2 class="font-bold text-2xl">This instance</h2> <h2 class="font-bold text-2xl">This instance</h2>

View File

@@ -1,22 +1,22 @@
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<div class="flex gap-2 items-center"> <div class="flex gap-2 items-center">
{{#noteq this.Account.Username "[deleted]"}} {{#noteq this.User.Username "[deleted]"}}
<img src="{{rewriteUrl(this.Account.Avatar)}}" class="rounded-full" width="24" height="24" loading="lazy"> <img src="{{this.User.Avatar}}" class="rounded-full" width="24" height="24" loading="lazy">
<a href="/user/{{this.Account.Username}}"> <a href="/user/{{this.User.Username}}">
<p class="whitespace-nowrap text-ellipsis overflow-hidden"><b>{{this.Account.Username}}</b></p> <p class="whitespace-nowrap text-ellipsis overflow-hidden"><b>{{this.User.Username}}</b></p>
</a> </a>
{{/noteq}} {{/noteq}}
{{#equal this.Account.Username "[deleted]"}} {{#equal this.User.Username "[deleted]"}}
<p class="whitespace-nowrap text-ellipsis overflow-hidden"><b>[deleted]</b></p> <p class="whitespace-nowrap text-ellipsis overflow-hidden"><b>[deleted]</b></p>
{{/equal}} {{/equal}}
</div> </div>
<div> <div>
<p>{{{sanitizeComment(this.Comment)}}}</p> <p>{{{this.Comment}}}</p>
<div class="flex gap-2"> <div class="flex gap-2">
<span title="{{this.CreatedAt}}">{{relTime(this.CreatedAt)}}</span> <span title="{{this.CreatedAt}}">{{this.RelTime}}</span>
{{#ifNonZeroTime this.DeletedAt}} {{#if this.DeletedAt}}
<span class="text-md">(deleted {{this.DeletedAt}})</span> <span class="text-md">(deleted {{this.DeletedAt}})</span>
{{/ifNonZeroTime}} {{/if}}
| |
<img class="invert icon" src="/static/icons/PhArrowFatUp.svg" alt="Likes" width="24px" height="24px"> {{this.Upvotes}} <img class="invert icon" src="/static/icons/PhArrowFatUp.svg" alt="Likes" width="24px" height="24px"> {{this.Upvotes}}
<img class="invert icon" src="/static/icons/PhArrowFatDown.svg" alt="Dislikes" width="24px" height="24px"> {{this.Downvotes}} <img class="invert icon" src="/static/icons/PhArrowFatDown.svg" alt="Dislikes" width="24px" height="24px"> {{this.Downvotes}}

View File

@@ -3,13 +3,13 @@
<img class="object-cover block w-full h-[300px] sm:w-[120px] sm:h-[140px] rounded-lg rounded-b-none sm:rounded-b-lg" src="{{this.Post.Cover.Url}}" alt=""> <img class="object-cover block w-full h-[300px] sm:w-[120px] sm:h-[140px] rounded-lg rounded-b-none sm:rounded-b-lg" src="{{this.Post.Cover.Url}}" alt="">
<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 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"> <div class="flex flex-col h-full">
<p class="md-container">{{{sanitizeComment(this.Comment)}}}</p> <p class="md-container">{{{this.Comment}}}</p>
<div class="grow"></div> <div class="flex-grow"></div>
<div class="flex gap-2"> <div class="flex gap-2">
<span title="{{this.CreatedAt}}">{{relTime(this.CreatedAt)}}</span> <span title="{{this.CreatedAt}}">{{this.RelTime}}</span>
{{#ifNonZeroTime this.DeletedAt}} {{#if this.DeletedAt}}
<span class="text-md">(deleted {{this.DeletedAt}})</span> <span class="text-md">(deleted {{this.DeletedAt}})</span>
{{/ifNonZeroTime}} {{/if}}
| |
<img class="invert icon" src="/static/icons/PhArrowFatUp.svg" alt="Likes" width="24px" height="24px"> <img class="invert icon" src="/static/icons/PhArrowFatUp.svg" alt="Likes" width="24px" height="24px">
{{this.Upvotes}} {{this.Upvotes}}

View File

@@ -4,7 +4,7 @@
<head> <head>
<title> <title>
{{#if post.Title}} {{#if post.Title}}
{{post.Title}} - {{post.Title}} -
{{/if}} {{/if}}
rimgo rimgo
</title> </title>
@@ -23,43 +23,36 @@
<h1 class="text-3xl font-bold">{{post.Title}}</h1> <h1 class="text-3xl font-bold">{{post.Title}}</h1>
<p>{{post.CreatedAt}}</p> <p>{{post.CreatedAt}}</p>
</header> </header>
<main> <main>
<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 my-4">
<div class="flex flex-col gap-2 md:flex-row md:gap-4 md:items-center"> {{#if post.User.Username}}
{{#if post.User.Username}} <a href="/user/{{post.User.Username}}" class="flex gap-2 items-center">
<a href="/user/{{post.User.Username}}" class="flex gap-2 items-center"> <img src="{{post.User.Avatar}}" class="rounded-full" width="36" height="36" />
<img src="{{post.User.Avatar}}" class="rounded-full" width="36" height="36" /> <p>
<p> <b>{{post.User.Username}}</b>
<b>{{post.User.Username}}</b> </p>
</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 &gt;</button>
</a> </a>
{{/noteq}} {{/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> </div>
<div class="flex flex-center flex-col break-words"> <div class="flex flex-center flex-col">
{{#each post.Media}} {{#each post.Media}}
{{#if this.Title}} {{#if this.Title}}
<h4 class="font-bold">{{this.Title}}</h4> <h4 class="font-bold">{{this.Title}}</h4>
@@ -75,9 +68,9 @@
{{/equal}} {{/equal}}
{{#if this.Description}} {{#if this.Description}}
<p>{{{sanitizeDescription(this.Description)}}}</p> <p>{{{this.Description}}}</p>
{{/if}} {{/if}}
{{/each}} {{/each}}
</div> </div>
{{#if post.tags}} {{#if post.tags}}
@@ -85,7 +78,7 @@
<style nonce="{{nonce}}"> <style nonce="{{nonce}}">
{{#each post.tags}} {{#each post.tags}}
.{{this.BackgroundId}} { background-image: url('{{this.Background}}') } .{{this.BackgroundId}} { background-image: url('{{this.Background}}') }
{{/each}} {{/each}}
</style> </style>
{{#each post.tags}} {{#each post.tags}}
<a href="/t/{{this.Tag}}"> <a href="/t/{{this.Tag}}">
@@ -108,8 +101,7 @@
{{#if comments}} {{#if comments}}
<div> <div>
<input id="comments__expandBtn" type="checkbox" checked> <input id="comments__expandBtn" type="checkbox" checked>
<label class="comments__expandBtn__label my-2 py-4 border-solid border-t-2 border-slate-400" <label class="comments__expandBtn__label my-2 py-4 border-solid border-t-2 border-slate-400" for="comments__expandBtn">
for="comments__expandBtn">
<h3 class="text-xl font-bold">Comments ({{post.Comments}})</h3> <h3 class="text-xl font-bold">Comments ({{post.Comments}})</h3>
<span class="text-xl font-bold"></span> <span class="text-xl font-bold"></span>
</label> </label>

View File

@@ -22,10 +22,10 @@
</div> </div>
<div class="flex justify-between mt-4 font-bold"> <div class="flex justify-between mt-4 font-bold">
{{#noteq page "0"}} {{#if displayPrev}}
<a href="/search?q={{query}}&page={{prevPage}}">Previous page</a> <a href="/search?q={{query}}&page={{prevPage}}">Previous page</a>
{{/noteq}} {{/if}}
<p>Page {{nextPage}}</p> <p>Page {{page}}</p>
<a href="/search?q={{query}}&page={{nextPage}}">Next page</a> <a href="/search?q={{query}}&page={{nextPage}}">Next page</a>
</div> </div>
</main> </main>

View File

@@ -5,9 +5,6 @@
<title>{{tag.Display}} - rimgo</title> <title>{{tag.Display}} - rimgo</title>
{{> partials/head }} {{> 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> </head>
<body class="font-sans text-lg bg-slate-800 text-white"> <body class="font-sans text-lg bg-slate-800 text-white">
@@ -19,12 +16,7 @@
<header class="p-4 rounded-xl text-white mb-4" style="background-image: url('{{tag.Background}}');"> <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"> <div class="flex flex-col items-center justify-center text-center">
<div class="flex gap-2 items-center"> <h2 class="text-2xl font-bold">{{tag.Display}}</h2>
<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> <p>{{tag.PostCount}} posts</p>
</div> </div>
<div class="flex flex-col"> <div class="flex flex-col">
@@ -45,7 +37,7 @@
{{/equal}} {{/equal}}
</div> </div>
</header> </header>
<main> <main>
<div class="posts"> <div class="posts">
{{#each tag.Posts}} {{#each tag.Posts}}
@@ -53,11 +45,10 @@
{{/each}} {{/each}}
</div> </div>
<div class="flex justify-between mt-4 font-bold"> <div class="mt-4 font-bold">
{{#noteq page "1"}} {{#if displayPrev}}
<a href="{{channel.RelUrl}}?page={{prevPage}}">Previous page</a> <a href="{{channel.RelUrl}}?page={{prevPage}}">Previous page</a>
{{/noteq}} {{/if}}
<p>Page {{nextPage}}</p>
<a href="{{channel.RelUrl}}?page={{nextPage}}">Next page</a> <a href="{{channel.RelUrl}}?page={{nextPage}}">Next page</a>
</div> </div>
</main> </main>

View File

@@ -5,9 +5,6 @@
<title>Trending - rimgo</title> <title>Trending - rimgo</title>
{{> partials/head }} {{> 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> </head>
<body class="font-sans text-lg bg-slate-800 text-white"> <body class="font-sans text-lg bg-slate-800 text-white">
@@ -18,11 +15,8 @@
</section> </section>
<header class="p-4 rounded-xl text-white mb-4 bg-gradient-to-r from-blue-400 to-emerald-400"> <header class="p-4 rounded-xl text-white mb-4 bg-gradient-to-r from-blue-400 to-emerald-400">
<div class="flex items-center justify-center text-center gap-2"> <div class="flex flex-col items-center justify-center text-center">
<h2 class="text-2xl font-extrabold">Trending</h2> <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>
<div class="flex flex-col sm:flex-row sm:justify-between"> <div class="flex flex-col sm:flex-row sm:justify-between">
<div class="flex flex-col"> <div class="flex flex-col">
@@ -71,9 +65,9 @@
</div> </div>
<div class="flex justify-between mt-4 font-bold"> <div class="flex justify-between mt-4 font-bold">
{{#noteq page "1"}} {{#if displayPrev}}
<a href="/trending?section={{section}}&sort={{sort}}&page={{prevPage}}">Previous page</a> <a href="/trending?section={{section}}&sort={{sort}}&page={{prevPage}}">Previous page</a>
{{/noteq}} {{/if}}
<p>Page {{page}}</p> <p>Page {{page}}</p>
<a href="/trending?section={{section}}&sort={{sort}}&page={{nextPage}}">Next page</a> <a href="/trending?section={{section}}&sort={{sort}}&page={{nextPage}}">Next page</a>
</div> </div>

View File

@@ -5,9 +5,6 @@
<title>{{user.Username}} - rimgo</title> <title>{{user.Username}} - rimgo</title>
{{> partials/head }} {{> 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> </head>
<body class="font-sans text-lg bg-slate-800 text-white"> <body class="font-sans text-lg bg-slate-800 text-white">
@@ -21,15 +18,10 @@
<div class="flex flex-col sm:flex-row items-center gap-2"> <div class="flex flex-col sm:flex-row items-center gap-2">
<img class="rounded-full" src="{{user.Avatar}}" width="72" height="72"> <img class="rounded-full" src="{{user.Avatar}}" width="72" height="72">
<div class="items-center sm:items-start text-center sm:text-left"> <div class="items-center sm:items-start text-center sm:text-left">
<div class="flex flex-row gap-2 items-center"> <h2 class="font-bold text-2xl">{{user.Username}}</h2>
<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> <p>{{user.Points}} pts · {{user.CreatedAt}}</p>
</div> </div>
<hr class="sm:border-0 grow"> <hr class="sm:border-0 flex-grow">
<div class="flex flex-col items-center sm:items-end"> <div class="flex flex-col items-center sm:items-end">
<a href="/user/{{user.Username}}"><b>Submissions</b></a> <a href="/user/{{user.Username}}"><b>Submissions</b></a>
<a href="/user/{{user.Username}}/favorites">Favorites</a> <a href="/user/{{user.Username}}/favorites">Favorites</a>

View File

@@ -21,7 +21,7 @@
<h2 class="font-bold text-2xl">{{user.Username}}</h2> <h2 class="font-bold text-2xl">{{user.Username}}</h2>
<p>{{user.Points}} pts · {{user.CreatedAt}}</p> <p>{{user.Points}} pts · {{user.CreatedAt}}</p>
</div> </div>
<hr class="sm:border-0 grow"> <hr class="sm:border-0 flex-grow">
<div class="flex flex-col items-center sm:items-end"> <div class="flex flex-col items-center sm:items-end">
<a href="/user/{{user.Username}}">Submissions</a> <a href="/user/{{user.Username}}">Submissions</a>
<a href="/user/{{user.Username}}/favorites">Favorites</a> <a href="/user/{{user.Username}}/favorites">Favorites</a>

View File

@@ -21,7 +21,7 @@
<h2 class="font-bold text-2xl">{{user.Username}}</h2> <h2 class="font-bold text-2xl">{{user.Username}}</h2>
<p>{{user.Points}} pts · {{user.CreatedAt}}</p> <p>{{user.Points}} pts · {{user.CreatedAt}}</p>
</div> </div>
<hr class="sm:border-0 grow"> <hr class="sm:border-0 flex-grow">
<div class="flex flex-col items-center sm:items-end"> <div class="flex flex-col items-center sm:items-end">
<a href="/user/{{user.Username}}">Submissions</a> <a href="/user/{{user.Username}}">Submissions</a>
<a href="/user/{{user.Username}}/favorites"><b>Favorites</b></a> <a href="/user/{{user.Username}}/favorites"><b>Favorites</b></a>