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
FIBER_PREFORK=false
IMGUR_CLIENT_ID=546c25a59c58ad7
SECURE=true
# Instance privacy
# For more information, see https://codeberg.org/librarian/librarian/wiki/Instance-privacy

1
.gitignore vendored
View File

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

View File

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

View File

@@ -3,14 +3,14 @@ FROM --platform=$BUILDPLATFORM golang:alpine AS build
ARG TARGETARCH
WORKDIR /src
RUN apk --no-cache add ca-certificates git nodejs pnpm
RUN apk --no-cache add ca-certificates git nodejs npm
COPY . .
RUN pnpm install && pnpm run build
RUN npx tailwindcss -i static/tailwind.css -o static/app.css -m
RUN go mod download
RUN GOOS=linux GOARCH=$TARGETARCH CGO_ENABLED=0 go build -ldflags "-X codeberg.org/rimgo/rimgo/pages.VersionInfo=$(date '+%Y-%m-%d')-$(git rev-list --abbrev-commit -1 HEAD)"
FROM scratch AS bin
FROM scratch as bin
WORKDIR /app
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

View File

@@ -1,14 +1,7 @@
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)"
dev-css:
pnpm run watch
dev:
go run github.com/air-verse/air@latest -c .air.toml
tag-vpr:
podman pull codeberg.org/rimgo/rimgo:latest
podman image tag codeberg.org/rimgo/rimgo:latest codeberg.org/video-prize-ranch/rimgo:latest
podman push codeberg.org/video-prize-ranch/rimgo:latest
tailwindcss -i static/tailwind.css -o static/app.css -m -w &
go run github.com/cosmtrek/air@latest -c .air.toml

View File

@@ -17,6 +17,8 @@ An alternative frontend for Imgur. Originally based on [rimgu](https://codeberg.
- [Privacy](#privacy)
- [Usage](#usage)
- [Instances](#instances)
- [Clearnet](#clearnet)
- [Tor](#tor)
- [Contributing](#contributing)
- [License](#license)
@@ -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/)
- [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/)
## Features
@@ -50,12 +52,12 @@ Tested using [Google PageSpeed Insights](https://pagespeed.web.dev/).
| Time to Interactive | 1.6s | 23.8s |
### Privacy
Imgur collects information about your device and uses tracking cookies for advertising, this is mentioned in their [privacy policy](https://imgur.com/privacy/). [Blacklight](https://themarkup.org/blacklight) found 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
## 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`
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/).
## 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
@@ -75,10 +127,7 @@ See [Install](https://rimgo.codeberg.page/docs/getting-started/install/).
See [Configuration](https://rimgo.codeberg.page/docs/usage/configuration/).
## 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
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"
"codeberg.org/rimgo/rimgo/utils"
"github.com/microcosm-cc/bluemonday"
"github.com/tidwall/gjson"
)
@@ -100,13 +101,17 @@ func parseAlbum(data gjson.Result) (Album, error) {
url := value.Get("url").String()
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{
Id: value.Get("id").String(),
Name: value.Get("name").String(),
MimeType: value.Get("mime_type").String(),
Type: value.Get("type").String(),
Title: value.Get("metadata.title").String(),
Description: value.Get("metadata.description").String(),
Description: description,
Url: url,
})

View File

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

View File

@@ -1,114 +1,145 @@
package api
import (
"encoding/json"
"errors"
"fmt"
"regexp"
"strings"
"sync"
"time"
"codeberg.org/rimgo/rimgo/utils"
"github.com/dustin/go-humanize"
"github.com/microcosm-cc/bluemonday"
"github.com/patrickmn/go-cache"
"github.com/tidwall/gjson"
"gitlab.com/golang-commonmark/linkify"
)
type Comment struct {
ID int `json:"id"`
ParentID int `json:"parent_id"`
Comment string `json:"comment"`
PostID string `json:"post_id"`
UpvoteCount int `json:"upvote_count"`
DownvoteCount int `json:"downvote_count"`
PointCount int `json:"point_count"`
Vote struct{} `json:"vote"` // ???
PlatformID int `json:"platform_id"`
Platform string `json:"platform"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
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"`
Comments []Comment
User User
Post Submission
Id string
Comment string
Upvotes int64
Downvotes int64
Platform string
CreatedAt string
RelTime string
UpdatedAt string
DeletedAt string
}
func (client *Client) FetchComments(galleryID string) ([]Comment, error) {
cacheData, found := client.Cache.Get(galleryID + "-comments")
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 {
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{}
var handlePanic = func() {
if v := recover(); v != nil {
v, ok := v.(error)
if !ok {
v = fmt.Errorf("%v", v)
}
panics = append(panics, v)
}
}
for i, value := range rawArr {
wg.Add(1)
go func() {
defer handlePanic()
defer wg.Done()
parseComment(value, &(*arr)[i])
}()
}
comments := make([]Comment, 0)
data.Get("data").ForEach(
func(key, value gjson.Result) bool {
wg.Add(1)
go func() {
defer wg.Done()
comments = append(comments, parseComment(value))
}()
return true
},
)
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 {
*post = commentPost(parseSubmission(gjson.Parse(string(data))))
return nil
}
type commentApiResponse struct {
Data commentArray `json:"data"`
return Comment{
Comments: comments,
User: User{
Id: data.Get("account.id").Int(),
Username: data.Get("account.username").String(),
Avatar: userAvatar,
},
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 {
Id string
Url string
ImageUrl string
Title string
User string
Points string
Views string
RelTime string
Id string
Url string
ImageUrl string
Title string
User string
Points string
Views string
RelTime string
}
func (client *Client) Search(query string, page string) ([]SearchResult, error) {
query = url.QueryEscape(query)
req, err := http.NewRequest("GET", "https://imgur.com/search/all/page/"+page+"?scrolled&q_size_is_mpx=off&qs=list&q="+query, nil)
req, err := http.NewRequest("GET", "https://imgur.com/search/all/page/" + page + "?scrolled&q_size_is_mpx=off&qs=list&q=" + query, nil)
if err != nil {
return []SearchResult{}, err
}
@@ -35,16 +35,16 @@ func (client *Client) Search(query string, page string) ([]SearchResult, error)
}
defer res.Body.Close()
if res.StatusCode != 200 {
return []SearchResult{}, fmt.Errorf("invalid status code, got %d", res.StatusCode)
}
return []SearchResult{}, fmt.Errorf("invalid status code, got %d", res.StatusCode)
}
doc, err := goquery.NewDocumentFromReader(res.Body)
if err != nil {
return []SearchResult{}, err
}
if err != nil {
return []SearchResult{}, err
}
results := []SearchResult{}
doc.Find(".post-list").Each(func(i int, s *goquery.Selection) {
doc.Find(".post-list").Each(func(i int, s *goquery.Selection) {
url, _ := s.Find("a").Attr("href")
imageUrl, _ := s.Find("img").Attr("src")
@@ -55,14 +55,14 @@ func (client *Client) Search(query string, page string) ([]SearchResult, error)
views = strings.TrimSuffix(views, " views")
result := SearchResult{
Id: strings.Split(url, "/")[2],
Url: url,
Id: strings.Split(url, "/")[2],
Url: url,
ImageUrl: strings.ReplaceAll(imageUrl, "//i.imgur.com", ""),
Title: s.Find(".search-item-title a").Text(),
User: s.Find(".account").Text(),
Views: views,
Points: points,
RelTime: strings.TrimSpace(postInfo[2]),
Title: s.Find(".search-item-title a").Text(),
User: s.Find(".account").Text(),
Views: views,
Points: points,
RelTime: strings.TrimSpace(postInfo[2]),
}
results = append(results, result)

View File

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

View File

@@ -1,7 +1,6 @@
package api
import (
"encoding/json"
"io"
"net/http"
"strings"
@@ -155,7 +154,7 @@ func (client *Client) FetchUserFavorites(username string, sort string, page stri
func (client *Client) FetchUserComments(username string) ([]Comment, error) {
cacheData, found := client.Cache.Get(username + "-usercomments")
if found {
return cacheData.(commentArray), nil
return cacheData.([]Comment), 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
}
data, err := io.ReadAll(res.Body)
body, err := io.ReadAll(res.Body)
if err != nil {
return []Comment{}, err
}
var parsed commentApiResponse
err = json.Unmarshal(data, &parsed)
if err != nil {
return []Comment{}, err
}
data := gjson.Parse(string(body))
client.Cache.Set(username+"-usercomments", parsed.Data, cache.DefaultExpiration)
return parsed.Data, nil
comments := make([]Comment, 0)
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 {

View File

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

41
go.mod
View File

@@ -1,28 +1,41 @@
module codeberg.org/rimgo/rimgo
go 1.24.0
go 1.17
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/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/mailgun/raymond/v2 v2.0.48
github.com/microcosm-cc/bluemonday v1.0.27
github.com/microcosm-cc/bluemonday v1.0.25
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
)
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/gorilla/css v1.0.1 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/stretchr/testify v1.10.0 // indirect
github.com/tidwall/match v1.2.0 // indirect
github.com/gofiber/template v1.8.2 // indirect
github.com/gofiber/utils v1.1.0 // indirect
github.com/google/uuid v1.3.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
golang.org/x/net v0.48.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
github.com/tinylib/msgp v1.1.8 // indirect
github.com/valyala/bytebufferpool v1.0.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.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ=
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
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/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/aymerick/raymond v2.0.2+incompatible h1:VEp3GpgdAnv9B2GFyTvqgcKvY+mfKMjPOA3SbKLtnU0=
github.com/aymerick/raymond v2.0.2+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g=
github.com/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/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/gorilla/feeds v1.2.0 h1:O6pBiXJ5JHhPvqy53NsjKOThq+dNFm8+DFrxBEdzSCc=
github.com/gorilla/feeds v1.2.0/go.mod h1:WMib8uJP3BbY+X8Szd1rA5Pzhdfh+HCCAYT2z7Fza6Y=
github.com/gofiber/fiber/v2 v2.46.0/go.mod h1:DNl0/c37WLe0g92U6lx1VMQuxGUQY5V7EIaVoEsUffc=
github.com/gofiber/fiber/v2 v2.48.0 h1:cRVMCb9aUJDsyHxGFLwz/sGzDggdailZZyptU9F9cU0=
github.com/gofiber/fiber/v2 v2.48.0/go.mod h1:xqJgfqrc23FJuqGOW6DVgi3HyZEm2Mn9pRqUb2kHSX8=
github.com/gofiber/template v1.8.2 h1:PIv9s/7Uq6m+Fm2MDNd20pAFFKt5wWs7ZBd8iV9pWwk=
github.com/gofiber/template v1.8.2/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8=
github.com/gofiber/template/handlebars/v2 v2.1.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/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw=
github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.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/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94/go.mod h1:90zrgN3D/WJsDd1iXHT96alCoN2KJo6/4x1DZC3wZs8=
github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d/go.mod h1:Gy+0tqhJvgGlqnTF8CVGP0AaGRjwBtXs/a5PA0Y3+A4=
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tinylib/msgp v1.1.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=
gitlab.com/golang-commonmark/linkify v0.0.0-20200225224916-64bca66f6ad3 h1:1Coh5BsUBlXoEJmIEaNzVAWrtg9k7/eJzailMQr1grw=
gitlab.com/golang-commonmark/linkify v0.0.0-20200225224916-64bca66f6ad3/go.mod h1:Gn+LZmCrhPECMD3SOKlE+BOHwhOYD9j7WT9NUtkCrC8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-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.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
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.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
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-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.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.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.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-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-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.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.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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.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.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.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.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.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.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
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-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.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
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-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/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -28,6 +28,21 @@
],
"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",
"countries": [
@@ -82,6 +97,13 @@
],
"cloudflare": false
},
{
"url": "https://i.01r.xyz",
"countries": [
"us"
],
"cloudflare": true
},
{
"url": "https://rimgo.eu.projectsegfau.lt",
"countries": [
@@ -103,13 +125,6 @@
],
"cloudflare": false
},
{
"url": "https://rimgo.fascinated.cc",
"countries": [
"us"
],
"cloudflare": false
},
{
"url": "https://rimgo.whateveritworks.org",
"countries": [
@@ -137,26 +152,5 @@
"de"
],
"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 (
"flag"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
"codeberg.org/rimgo/rimgo/pages"
"codeberg.org/rimgo/rimgo/render"
"codeberg.org/rimgo/rimgo/static"
"codeberg.org/rimgo/rimgo/utils"
"codeberg.org/rimgo/rimgo/views"
"github.com/aymerick/raymond"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cache"
"github.com/gofiber/fiber/v2/middleware/filesystem"
"github.com/gofiber/fiber/v2/middleware/recover"
"github.com/gofiber/template/handlebars/v2"
"github.com/joho/godotenv"
)
// a handler that returns error if it can't respond
type handler func(w http.ResponseWriter, r *http.Request) error
func wrapHandler(h handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if v := recover(); v != nil {
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() {
envPath := flag.String("c", ".env", "Path to env file")
godotenv.Load(*envPath)
@@ -41,111 +27,113 @@ func main() {
pages.InitializeApiClient()
views := views.GetFiles()
static := static.GetFiles()
render.Initialize(views)
views := http.FS(views.GetFiles())
if os.Getenv("ENV") == "dev" {
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.Handle("GET /robots.txt", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
file, _ := static.Open("robots.txt")
defer file.Close()
io.Copy(w, file)
}))
app.Handle("GET /favicon.ico", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
file, _ := static.Open("favicon/favicon.ico")
defer file.Close()
io.Copy(w, file)
app := fiber.New(fiber.Config{
Views: engine,
Prefork: utils.Config.FiberPrefork,
UnescapePath: true,
StreamRequestBody: true,
ErrorHandler: func(ctx *fiber.Ctx, err error) error {
code := fiber.StatusInternalServerError
if e, ok := err.(*fiber.Error); ok {
code = e.Code
}
err = ctx.Status(code).Render("errors/error", fiber.Map{
"err": err,
})
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString("Internal Server Error")
}
return nil
},
})
app.Use(recover.New(recover.Config{
EnableStackTrace: true,
StackTraceHandler: func(c *fiber.Ctx, e interface{}) {
fmt.Println(e)
},
}))
if os.Getenv("ENV") == "dev" {
app.Handle("GET /errors/429", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
pages.RenderError(w, r, 429)
app.Use("/static", filesystem.New(filesystem.Config{
Root: http.Dir("./static"),
}))
app.Handle("GET /errors/429/img", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Location", "/static/img/error-429.png")
w.WriteHeader(302)
app.Get("/errors/429", func(c *fiber.Ctx) error {
return c.Render("errors/429", nil)
})
app.Get("/errors/404", func(c *fiber.Ctx) error {
return c.Render("errors/404", nil)
})
app.Get("/errors/error", func(c *fiber.Ctx) error {
return c.Render("errors/error", fiber.Map{
"err": "Test error",
})
})
} else {
app.Use("/static", filesystem.New(filesystem.Config{
MaxAge: 2592000,
Root: http.FS(static.GetFiles()),
}))
app.Handle("GET /errors/404", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
pages.RenderError(w, r, 404)
}))
app.Handle("GET /errors/404/img", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Location", "/static/img/error-404.png")
w.WriteHeader(302)
}))
app.Handle("GET /errors/error", wrapHandler(func(w http.ResponseWriter, r *http.Request) error {
return fmt.Errorf("Test error")
}))
app.Handle("GET /errors/panic", wrapHandler(func(w http.ResponseWriter, r *http.Request) error {
panic("Test error")
app.Use(cache.New(cache.Config{
Expiration: 30 * time.Minute,
MaxBytes: 25000000,
KeyGenerator: func(c *fiber.Ctx) string {
return c.OriginalURL()
},
CacheControl: true,
StoreResponseHeaders: true,
}))
}
app.Handle("GET /{$}", 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
fmt.Println("listening on " + addr)
err := http.ListenAndServe(addr, app)
if err != nil {
fmt.Println(err)
}
app.Get("/robots.txt", func(c *fiber.Ctx) error {
file, _ := static.GetFiles().ReadFile("robots.txt")
_, err := c.Write(file)
return err
})
app.Get("/favicon.ico", func(c *fiber.Ctx) error {
file, _ := static.GetFiles().ReadFile("favicon/favicon.ico")
_, err := c.Write(file)
return err
})
app.Get("/", pages.HandleFrontpage)
app.Get("/about", pages.HandleAbout)
app.Get("/privacy", pages.HandlePrivacy)
app.Get("/search", pages.HandleSearch)
app.Get("/trending", pages.HandleTrending)
app.Get("/a/:postID", pages.HandlePost)
app.Get("/a/:postID/embed", pages.HandleEmbed)
app.Get("/t/:tag", pages.HandleTag)
app.Get("/t/:tag/:postID", pages.HandlePost)
app.Get("/r/:sub/:postID", pages.HandlePost)
app.Get("/user/:userID", pages.HandleUser)
app.Get("/user/:userID/favorites", pages.HandleUserFavorites)
app.Get("/user/:userID/comments", pages.HandleUserComments)
app.Get("/user/:userID/cover", pages.HandleUserCover)
app.Get("/user/:userID/avatar", pages.HandleUserAvatar)
app.Get("/gallery/:postID", pages.HandlePost)
app.Get("/gallery/:postID/embed", pages.HandleEmbed)
app.Get("/:postID.gifv", pages.HandleGifv)
app.Get("/:baseName.:extension", pages.HandleMedia)
app.Get("/stack/:baseName.:extension", pages.HandleMedia)
app.Get("/:postID", pages.HandlePost)
app.Get("/:postID/embed", pages.HandleEmbed)
app.Listen(utils.Config.Addr + ":" + utils.Config.Port)
}

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
import (
"net/http"
"os"
"codeberg.org/rimgo/rimgo/render"
"codeberg.org/rimgo/rimgo/utils"
"github.com/gofiber/fiber/v2"
)
func HandleAbout(w http.ResponseWriter, r *http.Request) error {
utils.SetHeaders(w)
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("Cache-Control", "public,max-age=31557600")
w.Header().Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; style-src 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
return render.Render(w, "about", map[string]any{
"proto": r.Proto,
"domain": r.Host,
"force_webp": utils.Config.ForceWebp,
func HandleAbout(c *fiber.Ctx) error {
utils.SetHeaders(c)
c.Set("X-Frame-Options", "DENY")
c.Set("Cache-Control", "public,max-age=31557600")
c.Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; style-src 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
return c.Render("about", fiber.Map{
"proto": c.Protocol(),
"domain": c.Hostname(),
"force_webp": os.Getenv("FORCE_WEBP"),
})
}

View File

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

View File

@@ -1,49 +1,48 @@
package pages
import (
"net/http"
"strings"
"codeberg.org/rimgo/rimgo/api"
"codeberg.org/rimgo/rimgo/render"
"codeberg.org/rimgo/rimgo/utils"
"github.com/gofiber/fiber/v2"
)
func HandleEmbed(w http.ResponseWriter, r *http.Request) error {
utils.SetHeaders(w)
w.Header().Set("Cache-Control", "public,max-age=31557600")
w.Header().Set("Content-Security-Policy", "default-src 'none'; base-uri 'none'; form-action 'none'; media-src 'self'; style-src 'self'; img-src 'self'; block-all-mixed-content")
func HandleEmbed(c *fiber.Ctx) error {
utils.SetHeaders(c)
c.Set("Cache-Control", "public,max-age=31557600")
c.Set("Content-Security-Policy", "default-src 'none'; base-uri 'none'; form-action 'none'; media-src 'self'; style-src 'self'; img-src 'self'; block-all-mixed-content")
post, err := api.Album{}, error(nil)
switch {
case strings.HasPrefix(r.URL.Path, "/a"):
post, err = ApiClient.FetchAlbum(r.PathValue("postID"))
case strings.HasPrefix(r.URL.Path, "/gallery"):
post, err = ApiClient.FetchPosts(r.PathValue("postID"))
case strings.HasPrefix(c.Path(), "/a"):
post, err = ApiClient.FetchAlbum(c.Params("postID"))
case strings.HasPrefix(c.Path(), "/gallery"):
post, err = ApiClient.FetchPosts(c.Params("postID"))
default:
post, err = ApiClient.FetchMedia(r.PathValue("postID"))
post, err = ApiClient.FetchMedia(c.Params("postID"))
}
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") {
return RenderError(w, r, 404)
return c.Status(404).Render("errors/404", nil)
}
if err != nil {
if err != nil {
return err
}
return render.Render(w, "embed", map[string]any{
return c.Render("embed", fiber.Map{
"post": post,
})
}
func HandleGifv(w http.ResponseWriter, r *http.Request) error {
utils.SetHeaders(w)
w.Header().Set("Cache-Control", "public,max-age=31557600")
w.Header().Set("Content-Security-Policy", "default-src 'none'; base-uri 'none'; form-action 'none'; media-src 'self'; style-src 'self'; img-src 'self'; block-all-mixed-content")
func HandleGifv(c *fiber.Ctx) error {
utils.SetHeaders(c)
c.Set("Cache-Control", "public,max-age=31557600")
c.Set("Content-Security-Policy", "default-src 'none'; base-uri 'none'; form-action 'none'; media-src 'self'; style-src 'self'; img-src 'self'; block-all-mixed-content")
return render.Render(w, "gifv", map[string]any{
"id": r.PathValue("postID"),
return c.Render("gifv", fiber.Map{
"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,21 +1,19 @@
package pages
import (
"net/http"
"codeberg.org/rimgo/rimgo/render"
"codeberg.org/rimgo/rimgo/utils"
"github.com/gofiber/fiber/v2"
)
var VersionInfo string
func HandleFrontpage(w http.ResponseWriter, r *http.Request) error {
utils.SetHeaders(w)
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("Cache-Control", "public,max-age=31557600")
w.Header().Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; style-src 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
func HandleFrontpage(c *fiber.Ctx) error {
utils.SetHeaders(c)
c.Set("X-Frame-Options", "DENY")
c.Set("Cache-Control", "public,max-age=31557600")
c.Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; style-src 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
return render.Render(w, "frontpage", map[string]any{
return c.Render("frontpage", fiber.Map{
"config": utils.Config,
"version": VersionInfo,
})

View File

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

View File

@@ -3,76 +3,49 @@ package pages
import (
"crypto/rand"
"fmt"
"net/http"
"strconv"
"strings"
"codeberg.org/rimgo/rimgo/api"
"codeberg.org/rimgo/rimgo/render"
"codeberg.org/rimgo/rimgo/utils"
"github.com/gofiber/fiber/v2"
)
// Cursed function
func nextInTag(client *api.Client, tagname, sort, page, I string) string {
i, err := strconv.Atoi(I)
if err != nil || i < 0 {
return ""
}
tag, err := client.FetchTag(tagname, sort, page)
if err != nil {
return ""
}
if i >= len(tag.Posts)-1 {
pageNumber, _ := strconv.Atoi(page)
tagn, err := client.FetchTag(tagname, sort, strconv.Itoa(pageNumber+1))
// Check length - Imgur will not return an error if there are no more posts and you request the next page
if err != nil || len(tagn.Posts) < 1 {
return ""
}
return tagn.Posts[0].Link
}
return tag.Posts[i+1].Link
}
func HandlePost(w http.ResponseWriter, r *http.Request) error {
utils.SetHeaders(w)
w.Header().Set("X-Frame-Options", "DENY")
postId := r.PathValue("postID")
if strings.Contains(postId, "-") {
postId = postId[len(postId)-7:]
}
func HandlePost(c *fiber.Ctx) error {
utils.SetHeaders(c)
c.Set("X-Frame-Options", "DENY")
post, err := api.Album{}, error(nil)
switch {
case strings.HasPrefix(r.URL.Path, "/a"):
post, err = ApiClient.FetchAlbum(postId)
case strings.HasPrefix(r.URL.Path, "/gallery"):
post, err = ApiClient.FetchPosts(postId)
case strings.HasPrefix(r.URL.Path, "/t"):
post, err = ApiClient.FetchPosts(postId)
case strings.HasPrefix(c.Path(), "/a"):
post, err = ApiClient.FetchAlbum(c.Params("postID"))
case strings.HasPrefix(c.Path(), "/gallery"):
post, err = ApiClient.FetchPosts(c.Params("postID"))
case strings.HasPrefix(c.Path(), "/t"):
post, err = ApiClient.FetchPosts(c.Params("postID"))
default:
post, err = ApiClient.FetchMedia(postId)
post, err = ApiClient.FetchMedia(c.Params("postID"))
}
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") {
return RenderError(w, r, 404)
return c.Status(404).Render("errors/404", nil)
}
if err != nil {
if err != nil {
return err
}
comments := []api.Comment{}
if post.SharedWithCommunity {
w.Header().Set("Cache-Control", "public,max-age=604800")
comments, err = ApiClient.FetchComments(postId)
c.Set("Cache-Control", "public,max-age=604800")
comments, err = ApiClient.FetchComments(c.Params("postID"))
if err != nil {
return err
}
} else {
w.Header().Set("Cache-Control", "public,max-age=31557600")
c.Set("Cache-Control", "public,max-age=31557600")
}
nonce := ""
@@ -83,18 +56,10 @@ func HandlePost(w http.ResponseWriter, r *http.Request) error {
nonce = fmt.Sprintf("%x", b)
csp = csp + " 'nonce-" + nonce + "'"
}
w.Header().Set("Content-Security-Policy", csp)
c.Set("Content-Security-Policy", csp)
var next string
tagParam := strings.Split(r.URL.Query().Get("tag"), ".")
if len(tagParam) == 4 {
tag, sort, page, index := tagParam[0], tagParam[1], tagParam[2], tagParam[3]
next = nextInTag(ApiClient, tag, sort, page, index)
}
return render.Render(w, "post", map[string]any{
return c.Render("post", fiber.Map{
"post": post,
"next": next,
"comments": comments,
"nonce": nonce,
})

View File

@@ -1,18 +1,16 @@
package pages
import (
"net/http"
"github.com/gofiber/fiber/v2"
"codeberg.org/rimgo/rimgo/render"
"codeberg.org/rimgo/rimgo/utils"
)
func HandlePrivacy(w http.ResponseWriter, r *http.Request) error {
utils.SetHeaders(w)
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("Content-Security-Policy", "default-src 'none'; form-action 'self'; style-src 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
func HandlePrivacy(c *fiber.Ctx) error {
utils.SetHeaders(c)
c.Set("X-Frame-Options", "DENY")
return render.Render(w, "privacy", map[string]any{
return c.Render("privacy", fiber.Map{
"config": utils.Config,
"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
import (
"net/http"
"strconv"
"codeberg.org/rimgo/rimgo/render"
"codeberg.org/rimgo/rimgo/utils"
"github.com/gofiber/fiber/v2"
)
func HandleSearch(w http.ResponseWriter, r *http.Request) error {
utils.SetHeaders(w)
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("Cache-Control", "public,max-age=604800")
w.Header().Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; style-src 'unsafe-inline' 'self'; media-src 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
func HandleSearch(c *fiber.Ctx) error {
utils.SetHeaders(c)
c.Set("X-Frame-Options", "DENY")
c.Set("Cache-Control", "public,max-age=604800")
c.Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; style-src 'unsafe-inline' 'self'; media-src 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
query := r.URL.Query().Get("q")
query := c.Query("q")
if utils.ImgurRe.MatchString(query) {
w.Header().Set("Location", utils.ImgurRe.ReplaceAllString(query, ""))
w.WriteHeader(302)
return nil
return c.Redirect(utils.ImgurRe.ReplaceAllString(query, ""))
}
page := r.URL.Query().Get("page")
if page == "" {
page = "0"
page := "0"
if c.Query("page") != "" {
page = c.Query("page")
}
pageNumber, err := strconv.Atoi(page)
pageNumber, err := strconv.Atoi(c.Query("page"))
if err != nil {
pageNumber = 0
}
displayPrevPage := true
if page == "0" {
displayPrevPage = false
}
results, err := ApiClient.Search(query, page)
if err != nil {
return err
}
return render.Render(w, "search", map[string]any{
"query": query,
"results": results,
"page": pageNumber,
"nextPage": pageNumber + 1,
"prevPage": pageNumber - 1,
return c.Render("search", fiber.Map{
"query": query,
"results": results,
"page": pageNumber + 1,
"displayPrev": displayPrevPage,
"nextPage": pageNumber + 1,
"prevPage": pageNumber - 1,
})
}

View File

@@ -1,44 +1,51 @@
package pages
import (
"net/http"
"strconv"
"codeberg.org/rimgo/rimgo/render"
"codeberg.org/rimgo/rimgo/utils"
"github.com/gofiber/fiber/v2"
)
func HandleTag(w http.ResponseWriter, r *http.Request) error {
utils.SetHeaders(w)
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("Cache-Control", "public,max-age=604800")
w.Header().Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; style-src 'unsafe-inline' 'self'; media-src 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
func HandleTag(c *fiber.Ctx) error {
utils.SetHeaders(c)
c.Set("X-Frame-Options", "DENY")
c.Set("Cache-Control", "public,max-age=604800")
c.Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; style-src 'unsafe-inline' 'self'; media-src 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
page := r.URL.Query().Get("page")
if page == "" {
page = "1"
page := "1"
if c.Query("page") != "" {
page = c.Query("page")
}
pageNumber, err := strconv.Atoi(page)
pageNumber, err := strconv.Atoi(c.Query("page"))
if err != nil {
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" {
return RenderError(w, r, 429)
return c.Status(429).Render("errors/429", fiber.Map{
"path": c.Path(),
})
}
if err != nil {
return err
}
if tag.Display == "" {
return RenderError(w, r, 404)
return c.Status(404).Render("errors/404", nil)
}
return render.Render(w, "tag", map[string]any{
"tag": tag,
"page": page,
"nextPage": pageNumber + 1,
"prevPage": pageNumber - 1,
return c.Render("tag", fiber.Map{
"tag": tag,
"page": page,
"displayPrev": displayPrevPage,
"nextPage": pageNumber + 1,
"prevPage": pageNumber - 1,
})
}

View File

@@ -1,53 +1,58 @@
package pages
import (
"net/http"
"strconv"
"codeberg.org/rimgo/rimgo/render"
"codeberg.org/rimgo/rimgo/utils"
"github.com/gofiber/fiber/v2"
)
func HandleTrending(w http.ResponseWriter, r *http.Request) error {
utils.SetHeaders(w)
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("Cache-Control", "public,max-age=604800")
w.Header().Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; style-src 'unsafe-inline' 'self'; media-src 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
func HandleTrending(c *fiber.Ctx) error {
utils.SetHeaders(c)
c.Set("X-Frame-Options", "DENY")
c.Set("Cache-Control", "public,max-age=604800")
c.Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; style-src 'unsafe-inline' 'self'; media-src 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
page := r.URL.Query().Get("page")
if page == "" {
page = "1"
page := "1"
if c.Query("page") != "" {
page = c.Query("page")
}
pageNumber, err := strconv.Atoi(page)
pageNumber, err := strconv.Atoi(c.Query("page"))
if err != nil {
pageNumber = 1
}
section := r.URL.Query().Get("section")
section := c.Query("section")
switch section {
case "hot", "new", "top":
default:
section = "hot"
}
sort := r.URL.Query().Get("sort")
sort := c.Query("sort")
switch sort {
case "newest", "best", "popular":
default:
sort = "popular"
}
displayPrevPage := true
if page == "1" {
displayPrevPage = false
}
results, err := ApiClient.FetchTrending(section, sort, page)
if err != nil {
return err
}
return render.Render(w, "trending", map[string]any{
"results": results,
"section": section,
"sort": sort,
"page": pageNumber,
"nextPage": pageNumber + 1,
"prevPage": pageNumber - 1,
return c.Render("trending", fiber.Map{
"results": results,
"section": section,
"sort": sort,
"page": pageNumber,
"displayPrev": displayPrevPage,
"nextPage": pageNumber + 1,
"prevPage": pageNumber - 1,
})
}

View File

@@ -1,49 +1,53 @@
package pages
import (
"net/http"
"strconv"
"codeberg.org/rimgo/rimgo/render"
"codeberg.org/rimgo/rimgo/utils"
"github.com/gofiber/fiber/v2"
)
func HandleUser(w http.ResponseWriter, r *http.Request) error {
utils.SetHeaders(w)
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("Cache-Control", "public,max-age=604800")
w.Header().Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; media-src 'self'; style-src 'unsafe-inline' 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
func HandleUser(c *fiber.Ctx) error {
utils.SetHeaders(c)
c.Set("X-Frame-Options", "DENY")
c.Set("Cache-Control", "public,max-age=604800")
c.Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; media-src 'self'; style-src 'unsafe-inline' 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
page := r.URL.Query().Get("page")
if page == "" {
page = "0"
page := "0"
if c.Query("page") != "" {
page = c.Query("page")
}
pageNumber, err := strconv.Atoi(page)
pageNumber, err := strconv.Atoi(c.Query("page"))
if err != nil {
pageNumber = 0
}
user, err := ApiClient.FetchUser(r.PathValue("userID"))
user, err := ApiClient.FetchUser(c.Params("userID"))
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 {
return err
}
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" {
return RenderError(w, r, 429)
c.Status(429)
return c.Render("errors/429", fiber.Map{
"path": c.Path(),
})
}
if err != nil {
return err
}
return render.Render(w, "user", map[string]any{
return c.Render("user", fiber.Map{
"user": user,
"submissions": submissions,
"page": page,
@@ -52,73 +56,83 @@ func HandleUser(w http.ResponseWriter, r *http.Request) error {
})
}
func HandleUserComments(w http.ResponseWriter, r *http.Request) error {
utils.SetHeaders(w)
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("Cache-Control", "public,max-age=604800")
w.Header().Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; media-src 'self'; style-src 'unsafe-inline' 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
func HandleUserComments(c *fiber.Ctx) error {
utils.SetHeaders(c)
c.Set("X-Frame-Options", "DENY")
c.Set("Cache-Control", "public,max-age=604800")
c.Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; media-src 'self'; style-src 'unsafe-inline' 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
user, err := ApiClient.FetchUser(r.PathValue("userID"))
user, err := ApiClient.FetchUser(c.Params("userID"))
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 {
return err
}
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" {
return RenderError(w, r, 429)
c.Status(429)
return c.Render("errors/429", fiber.Map{
"path": c.Path(),
})
}
if err != nil {
return err
}
return render.Render(w, "userComments", map[string]any{
return c.Render("userComments", fiber.Map{
"user": user,
"comments": comments,
})
}
func HandleUserFavorites(w http.ResponseWriter, r *http.Request) error {
utils.SetHeaders(w)
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("Cache-Control", "public,max-age=604800")
w.Header().Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; media-src 'self'; style-src 'unsafe-inline' 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
func HandleUserFavorites(c *fiber.Ctx) error {
utils.SetHeaders(c)
c.Set("X-Frame-Options", "DENY")
c.Set("Cache-Control", "public,max-age=604800")
c.Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; media-src 'self'; style-src 'unsafe-inline' 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
page := r.URL.Query().Get("page")
if page == "" {
page = "0"
page := "0"
if c.Query("page") != "" {
page = c.Query("page")
}
pageNumber, err := strconv.Atoi(page)
pageNumber, err := strconv.Atoi(c.Query("page"))
if err != nil {
pageNumber = 0
}
user, err := ApiClient.FetchUser(r.PathValue("userID"))
user, err := ApiClient.FetchUser(c.Params("userID"))
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 {
return err
}
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" {
return RenderError(w, r, 429)
c.Status(429)
return c.Render("errors/429", fiber.Map{
"path": c.Path(),
})
}
if err != nil {
return err
}
return render.Render(w, "userFavorites", map[string]any{
return c.Render("userFavorites", fiber.Map{
"user": user,
"favorites": favorites,
"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

@@ -1,7 +1,6 @@
{
"name": "rimgo",
"short_name": "rimgo",
"start_url": "/",
"icons": [
{
"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 {
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 {
Port string
Addr string
ImgurId string
ProtocolDetection bool
Secure bool
ForceWebp bool
ImageCache bool
CleanupInterval time.Duration
CacheDir string
Privacy map[string]interface{}
Port string
Addr string
ImgurId string
FiberPrefork bool
ImageCache bool
CleanupInterval time.Duration
CacheDir string
Privacy map[string]interface{}
}
var Config config
func envString(name, def string) string {
env := os.Getenv(name)
if env != "" {
return env
}
return def
}
func envBool(name string) bool {
return os.Getenv(name) == "true" || os.Getenv(name) == "1"
}
func LoadConfig() {
port := "3000"
if os.Getenv("PORT") != "" {
port = os.Getenv("PORT")
}
if os.Getenv("RIMGU_PORT") != "" {
port = os.Getenv("RIMGU_PORT")
}
addr := "0.0.0.0"
if os.Getenv("ADDRESS") != "" {
addr = os.Getenv("ADDRESS")
}
if os.Getenv("RIMGU_ADDRESS") != "" {
addr = os.Getenv("RIMGU_ADDRESS")
}
imgurId := "546c25a59c58ad7"
if os.Getenv("IMGUR_CLIENT_ID") != "" {
imgurId = os.Getenv("IMGUR_CLIENT_ID")
}
if os.Getenv("RIMGU_IMGUR_CLIENT_ID") != "" {
imgurId = os.Getenv("RIMGU_IMGUR_CLIENT_ID")
}
Config = config{
Port: envString("PORT", "3000"),
Addr: envString("ADDR", "0.0.0.0"),
ImgurId: envString("IMGUR_CLIENT_ID", "546c25a59c58ad7"),
ProtocolDetection: envBool("PROTOCOL_DETECTION"),
Secure: envBool("SECURE"),
ForceWebp: envBool("FORCE_WEBP"),
Port: port,
Addr: addr,
ImgurId: imgurId,
FiberPrefork: os.Getenv("FIBER_PREFORK") == "true",
Privacy: map[string]interface{}{
"set": os.Getenv("PRIVACY_NOT_COLLECTED") != "",
"policy": os.Getenv("PRIVACY_POLICY"),
"message": os.Getenv("PRIVACY_MESSAGE"),
"country": os.Getenv("PRIVACY_COUNTRY"),
"provider": os.Getenv("PRIVACY_PROVIDER"),
"cloudflare": envBool("PRIVACY_CLOUDFLARE"),
"not_collected": envBool("PRIVACY_NOT_COLLECTED"),
"ip": envBool("PRIVACY_IP"),
"url": envBool("PRIVACY_URL"),
"device": envBool("PRIVACY_DEVICE"),
"diagnostics": envBool("PRIVACY_DIAGNOSTICS"),
"cloudflare": os.Getenv("PRIVACY_CLOUDFLARE") == "true",
"not_collected": os.Getenv("PRIVACY_NOT_COLLECTED") == "true",
"ip": os.Getenv("PRIVACY_IP") == "true",
"url": os.Getenv("PRIVACY_URL") == "true",
"device": os.Getenv("PRIVACY_DEVICE") == "true",
"diagnostics": os.Getenv("PRIVACY_DIAGNOSTICS") == "true",
},
}
}

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

@@ -1,7 +1,6 @@
package utils
import (
"encoding/json"
"fmt"
"io"
"net/http"
@@ -10,42 +9,6 @@ import (
"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) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
@@ -72,7 +35,7 @@ func GetJSON(url string) (gjson.Result, error) {
return gjson.Result{}, err
}
switch res.StatusCode {
switch (res.StatusCode) {
case 200:
return gjson.Parse(string(body)), nil
case 429:

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 (
"net/http"
"github.com/gofiber/fiber/v2"
)
func SetHeaders(w http.ResponseWriter) {
w.Header().Set("Referrer-Policy", "no-referrer")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Robots-Tag", "noindex, noimageindex, nofollow")
w.Header().Set("Strict-Transport-Security", "max-age=31557600")
w.Header().Set("Permissions-Policy", "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(self), geolocation=(), gyroscope=(), interest-cohort=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()")
func SetHeaders(c *fiber.Ctx) {
c.Set("Referrer-Policy", "no-referrer")
c.Set("X-Content-Type-Options", "nosniff")
c.Set("X-Robots-Tag", "noindex, noimageindex, nofollow")
c.Set("Strict-Transport-Security", "max-age=31557600")
c.Set("Permissions-Policy", "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(self), geolocation=(), gyroscope=(), interest-cohort=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()")
}
func SetReqHeaders(req *http.Request) {

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

@@ -25,8 +25,8 @@
rimgo is not affiliated with Imgur, all content is proxied from Imgur.
</p>
<br/>
<h3 class="font-bold text-xl">Legal 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>
<h3 class="font-bold text-xl">Notice</h3>
<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>
<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 gap-2 items-center">
{{#noteq this.Account.Username "[deleted]"}}
<img src="{{rewriteUrl(this.Account.Avatar)}}" class="rounded-full" width="24" height="24" loading="lazy">
<a href="/user/{{this.Account.Username}}">
<p class="whitespace-nowrap text-ellipsis overflow-hidden"><b>{{this.Account.Username}}</b></p>
{{#noteq this.User.Username "[deleted]"}}
<img src="{{this.User.Avatar}}" class="rounded-full" width="24" height="24" loading="lazy">
<a href="/user/{{this.User.Username}}">
<p class="whitespace-nowrap text-ellipsis overflow-hidden"><b>{{this.User.Username}}</b></p>
</a>
{{/noteq}}
{{#equal this.Account.Username "[deleted]"}}
{{#equal this.User.Username "[deleted]"}}
<p class="whitespace-nowrap text-ellipsis overflow-hidden"><b>[deleted]</b></p>
{{/equal}}
</div>
<div>
<p>{{{sanitizeComment(this.Comment)}}}</p>
<p>{{{this.Comment}}}</p>
<div class="flex gap-2">
<span title="{{this.CreatedAt}}">{{relTime(this.CreatedAt)}}</span>
{{#ifNonZeroTime this.DeletedAt}}
<span title="{{this.CreatedAt}}">{{this.RelTime}}</span>
{{#if this.DeletedAt}}
<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/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="">
<div class="flex flex-col gap-2 bg-slate-600 p-4 rounded-lg rounded-t-none sm:rounded-t-lg w-full">
<div class="flex flex-col h-full">
<p class="md-container">{{{sanitizeComment(this.Comment)}}}</p>
<div class="grow"></div>
<p class="md-container">{{{this.Comment}}}</p>
<div class="flex-grow"></div>
<div class="flex gap-2">
<span title="{{this.CreatedAt}}">{{relTime(this.CreatedAt)}}</span>
{{#ifNonZeroTime this.DeletedAt}}
<span title="{{this.CreatedAt}}">{{this.RelTime}}</span>
{{#if this.DeletedAt}}
<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}}

View File

@@ -25,41 +25,34 @@
</header>
<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">
{{#if post.User.Username}}
<a href="/user/{{post.User.Username}}" class="flex gap-2 items-center">
<img src="{{post.User.Avatar}}" class="rounded-full" width="36" height="36" />
<p>
<b>{{post.User.Username}}</b>
</p>
</a>
{{/if}}
<div class="flex gap-2 items-center">
<div class="flex flex-center gap-2">
<img class="icon invert" src="/static/icons/PhEye.svg" alt="Views" width="24px" height="24px">
<p>{{post.Views}}</p>
</div>
{{#if post.SharedWithCommunity}}
<div class="flex flex-center gap-2">
<img class="icon invert" src="/static/icons/PhArrowFatUp.svg" alt="Likes" width="24px" height="24px">
<p>{{post.Upvotes}}</p>
</div>
<div class="flex flex-center gap-2">
<img class="icon invert" src="/static/icons/PhArrowFatDown.svg" alt="Dislikes" width="24px" height="24px">
<p>{{post.Downvotes}}</p>
</div>
{{/if}}
</div>
</div>
{{#noteq next ""}}
<a href="{{next}}" class="self-end">
<button class="p-2 rounded-lg bg-slate-600">Next &gt;</button>
<div class="flex flex-col gap-2 md:flex-row md:gap-4 md:items-center my-4">
{{#if post.User.Username}}
<a href="/user/{{post.User.Username}}" class="flex gap-2 items-center">
<img src="{{post.User.Avatar}}" class="rounded-full" width="36" height="36" />
<p>
<b>{{post.User.Username}}</b>
</p>
</a>
{{/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 class="flex flex-center flex-col break-words">
<div class="flex flex-center flex-col">
{{#each post.Media}}
{{#if this.Title}}
<h4 class="font-bold">{{this.Title}}</h4>
@@ -75,9 +68,9 @@
{{/equal}}
{{#if this.Description}}
<p>{{{sanitizeDescription(this.Description)}}}</p>
<p>{{{this.Description}}}</p>
{{/if}}
{{/each}}
{{/each}}
</div>
{{#if post.tags}}
@@ -108,8 +101,7 @@
{{#if comments}}
<div>
<input id="comments__expandBtn" type="checkbox" checked>
<label class="comments__expandBtn__label my-2 py-4 border-solid border-t-2 border-slate-400"
for="comments__expandBtn">
<label class="comments__expandBtn__label my-2 py-4 border-solid border-t-2 border-slate-400" for="comments__expandBtn">
<h3 class="text-xl font-bold">Comments ({{post.Comments}})</h3>
<span class="text-xl font-bold"></span>
</label>

View File

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

View File

@@ -5,9 +5,6 @@
<title>{{tag.Display}} - rimgo</title>
{{> partials/head }}
<link href="/t/{{tag.Tag}}.rss" rel="alternate" title={{tag.Display}} type="application/rss+xml"
<link href="/t/{{tag.Tag}}.atom" rel="alternate" title={{tag.Display}} type="application/atom+xml">
<link href="/t/{{tag.Tag}}.json" rel="alternate" title={{tag.Display}} type="application/feed+json">
</head>
<body class="font-sans text-lg bg-slate-800 text-white">
@@ -19,12 +16,7 @@
<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 gap-2 items-center">
<h2 class="text-2xl font-bold">{{tag.Display}}</h2>
<a href="/t/{{tag.Tag}}.rss" label="RSS">
<img src="/static/icons/PhRss.svg" width="24px" height="24px" class="invert" />
</a>
</div>
<h2 class="text-2xl font-bold">{{tag.Display}}</h2>
<p>{{tag.PostCount}} posts</p>
</div>
<div class="flex flex-col">
@@ -53,11 +45,10 @@
{{/each}}
</div>
<div class="flex justify-between mt-4 font-bold">
{{#noteq page "1"}}
<div class="mt-4 font-bold">
{{#if displayPrev}}
<a href="{{channel.RelUrl}}?page={{prevPage}}">Previous page</a>
{{/noteq}}
<p>Page {{nextPage}}</p>
{{/if}}
<a href="{{channel.RelUrl}}?page={{nextPage}}">Next page</a>
</div>
</main>

View File

@@ -5,9 +5,6 @@
<title>Trending - rimgo</title>
{{> partials/head }}
<link href="/trending.rss" rel="alternate" title="Trending" type="application/rss+xml">
<link href="/trending.atom" rel="alternate" title="Trending" type="application/atom+xml">
<link href="/trending.json" rel="alternate" title="Trending" type="application/feed+json">
</head>
<body class="font-sans text-lg bg-slate-800 text-white">
@@ -18,11 +15,8 @@
</section>
<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>
<a href="/trending.rss" label="RSS">
<img src="/static/icons/PhRss.svg" width="24px" height="24px" class="invert" />
</a>
</div>
<div class="flex flex-col sm:flex-row sm:justify-between">
<div class="flex flex-col">
@@ -71,9 +65,9 @@
</div>
<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>
{{/noteq}}
{{/if}}
<p>Page {{page}}</p>
<a href="/trending?section={{section}}&sort={{sort}}&page={{nextPage}}">Next page</a>
</div>

View File

@@ -5,9 +5,6 @@
<title>{{user.Username}} - rimgo</title>
{{> partials/head }}
<link href="/user/{{user.Username}}.rss" rel="alternate" title={{user.Username}} type="application/rss+xml">
<link href="/user/{{user.Username}}.atom" rel="alternate" title={{user.Username}} type="application/atom+xml">
<link href="/user/{{user.Username}}.json" rel="alternate" title={{user.Username}} type="application/feed+json">
</head>
<body class="font-sans text-lg bg-slate-800 text-white">
@@ -21,15 +18,10 @@
<div class="flex flex-col sm:flex-row items-center gap-2">
<img class="rounded-full" src="{{user.Avatar}}" width="72" height="72">
<div class="items-center sm:items-start text-center sm:text-left">
<div class="flex flex-row gap-2 items-center">
<h2 class="font-bold text-2xl">{{user.Username}}</h2>
<a href="/user/{{user.Username}}.rss" label="RSS">
<img src="/static/icons/PhRss.svg" width="24px" height="24px" class="invert" />
</a>
</div>
<h2 class="font-bold text-2xl">{{user.Username}}</h2>
<p>{{user.Points}} pts · {{user.CreatedAt}}</p>
</div>
<hr class="sm:border-0 grow">
<hr class="sm:border-0 flex-grow">
<div class="flex flex-col items-center sm:items-end">
<a href="/user/{{user.Username}}"><b>Submissions</b></a>
<a href="/user/{{user.Username}}/favorites">Favorites</a>

View File

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

View File

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