16 Commits

Author SHA1 Message Date
orangix
e241d35efe move sanitization code out of api package 2026-01-25 06:25:21 +01:00
orangix
02be603dcc refactor api package comments
* use encoding/json for comment parsing

* refactor by moving loop code to an UnmarshalJSON

* use a preallocated array and indices to maintain order while using
  goroutines again, this was removed a while ago

* use new struct in comment.hbs and contextComment.hbs

* rewriteUrl partial to reduce rimgo-specific code in api

* move RenderError into pages package to avoid import cycle between render and utils
2026-01-25 06:08:28 +01:00
orangix
c208a55f40 go mod tidy 2026-01-23 20:27:20 +01:00
orangix
4441d25d38 fix errors 2026-01-23 20:18:56 +01:00
orangix
975ffa0b9c uncomment stack handler 2026-01-23 19:42:55 +01:00
orangix
7b1314fae3 rss 2026-01-23 19:27:24 +01:00
orangix
fd704f53e7 update getUrl.go to use net/http 2026-01-19 19:25:46 +01:00
orangix
bf849e1cbc remove FIBER_PREFORK 2026-01-19 19:17:36 +01:00
orangix
cd4a36c9f7 port most routes 2026-01-19 19:11:01 +01:00
orangix
04fbc7f5f4 fix embed handling 2026-01-19 17:26:45 +01:00
orangix
189ebeefde add noteq 2026-01-19 17:08:15 +01:00
orangix
3f40c25b04 port template engine 2026-01-19 05:43:04 +01:00
orangix
33fa04e98d change pages/about.go to Config.ForceWebp 2026-01-19 03:04:06 +01:00
orangix
e5b87dc924 gofmt 2026-01-19 02:36:52 +01:00
orangix
8cb2524924 drop RIMGU_ environment variables 2026-01-16 22:55:45 +01:00
video-prize-ranch
23b66cba47 Use full instance url for cache key (#243)
Closes #240

Reviewed-on: https://codeberg.org/rimgo/rimgo/pulls/243
Reviewed-by: orangix <orangix@noreply.codeberg.org>
Co-authored-by: video-prize-ranch <cb.8a3w5@simplelogin.co>
Co-committed-by: video-prize-ranch <cb.8a3w5@simplelogin.co>
2026-01-14 18:27:25 +01:00
40 changed files with 865 additions and 651 deletions

View File

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

View File

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

View File

@@ -1,145 +1,114 @@
package api package api
import ( import (
"regexp" "encoding/json"
"strings" "errors"
"fmt"
"sync" "sync"
"time" "time"
"codeberg.org/rimgo/rimgo/utils" "codeberg.org/rimgo/rimgo/utils"
"github.com/dustin/go-humanize"
"github.com/microcosm-cc/bluemonday"
"github.com/patrickmn/go-cache" "github.com/patrickmn/go-cache"
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
"gitlab.com/golang-commonmark/linkify"
) )
type Comment struct { type Comment struct {
Comments []Comment ID int `json:"id"`
User User ParentID int `json:"parent_id"`
Post Submission Comment string `json:"comment"`
Id string PostID string `json:"post_id"`
Comment string UpvoteCount int `json:"upvote_count"`
Upvotes int64 DownvoteCount int `json:"downvote_count"`
Downvotes int64 PointCount int `json:"point_count"`
Platform string Vote struct{} `json:"vote"` // ???
CreatedAt string PlatformID int `json:"platform_id"`
RelTime string Platform string `json:"platform"`
UpdatedAt string CreatedAt time.Time `json:"created_at"`
DeletedAt string 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"`
} }
func (client *Client) FetchComments(galleryID string) ([]Comment, error) { func (client *Client) FetchComments(galleryID string) ([]Comment, error) {
cacheData, found := client.Cache.Get(galleryID + "-comments") cacheData, found := client.Cache.Get(galleryID + "-comments")
if found { if found {
return cacheData.([]Comment), nil return cacheData.(commentArray), nil
} }
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") 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")
if err != nil { if err != nil {
return []Comment{}, nil return []Comment{}, nil
} }
wg := sync.WaitGroup{} var parsed commentApiResponse
comments := make([]Comment, 0) err = json.Unmarshal(data, &parsed)
data.Get("data").ForEach( if err != nil {
func(key, value gjson.Result) bool { return []Comment{}, err
wg.Add(1) }
go func() { client.Cache.Set(galleryID+"-comments", parsed.Data, cache.DefaultExpiration)
defer wg.Done() return parsed.Data, nil
comments = append(comments, parseComment(value))
}()
return true
},
)
wg.Wait()
client.Cache.Set(galleryID+"-comments", comments, cache.DefaultExpiration)
return comments, nil
} }
var imgurRe = regexp.MustCompile(`https?://imgur\.com/(gallery|a)?/(.*)`) func parseComment(data json.RawMessage, out *Comment) {
var imgurRe2 = regexp.MustCompile(`https?://imgur\.com/(.*)`) err := json.Unmarshal(data, &out)
var imgRe = regexp.MustCompile(`https?://i\.imgur\.com/(.*)\.(png|gif|jpe?g|webp)`) if err != nil {
var vidRe = regexp.MustCompile(`https?://i\.imgur\.com/(.*)\.(mp4|webm)`) panic(err)
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)
}
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")
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)
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,
} }
} }
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])
}()
}
wg.Wait()
if len(panics) != 0 {
return errors.Join(panics...)
}
return nil
}
type commentPost Submission
func (post *commentPost) UnmarshalJSON(data []byte) error {
*post = commentPost(parseSubmission(gjson.Parse(string(data))))
return nil
}
type commentApiResponse struct {
Data commentArray `json:"data"`
}

View File

@@ -11,19 +11,19 @@ import (
) )
type SearchResult struct { type SearchResult struct {
Id string Id string
Url string Url string
ImageUrl string ImageUrl string
Title string Title string
User string User string
Points string Points string
Views string Views string
RelTime string RelTime string
} }
func (client *Client) Search(query string, page string) ([]SearchResult, error) { func (client *Client) Search(query string, page string) ([]SearchResult, error) {
query = url.QueryEscape(query) query = url.QueryEscape(query)
req, err := http.NewRequest("GET", "https://imgur.com/search/all/page/" + page + "?scrolled&q_size_is_mpx=off&qs=list&q=" + query, nil) req, err := http.NewRequest("GET", "https://imgur.com/search/all/page/"+page+"?scrolled&q_size_is_mpx=off&qs=list&q="+query, nil)
if err != nil { if err != nil {
return []SearchResult{}, err return []SearchResult{}, err
} }
@@ -35,16 +35,16 @@ func (client *Client) Search(query string, page string) ([]SearchResult, error)
} }
defer res.Body.Close() defer res.Body.Close()
if res.StatusCode != 200 { if res.StatusCode != 200 {
return []SearchResult{}, fmt.Errorf("invalid status code, got %d", res.StatusCode) return []SearchResult{}, fmt.Errorf("invalid status code, got %d", res.StatusCode)
} }
doc, err := goquery.NewDocumentFromReader(res.Body) doc, err := goquery.NewDocumentFromReader(res.Body)
if err != nil { if err != nil {
return []SearchResult{}, err return []SearchResult{}, err
} }
results := []SearchResult{} results := []SearchResult{}
doc.Find(".post-list").Each(func(i int, s *goquery.Selection) { doc.Find(".post-list").Each(func(i int, s *goquery.Selection) {
url, _ := s.Find("a").Attr("href") url, _ := s.Find("a").Attr("href")
imageUrl, _ := s.Find("img").Attr("src") imageUrl, _ := s.Find("img").Attr("src")
@@ -55,14 +55,14 @@ func (client *Client) Search(query string, page string) ([]SearchResult, error)
views = strings.TrimSuffix(views, " views") views = strings.TrimSuffix(views, " views")
result := SearchResult{ result := SearchResult{
Id: strings.Split(url, "/")[2], Id: strings.Split(url, "/")[2],
Url: url, Url: url,
ImageUrl: strings.ReplaceAll(imageUrl, "//i.imgur.com", ""), ImageUrl: strings.ReplaceAll(imageUrl, "//i.imgur.com", ""),
Title: s.Find(".search-item-title a").Text(), Title: s.Find(".search-item-title a").Text(),
User: s.Find(".account").Text(), User: s.Find(".account").Text(),
Views: views, Views: views,
Points: points, Points: points,
RelTime: strings.TrimSpace(postInfo[2]), RelTime: strings.TrimSpace(postInfo[2]),
} }
results = append(results, result) results = append(results, result)

View File

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

17
go.mod
View File

@@ -5,8 +5,6 @@ go 1.24.0
require ( require (
github.com/PuerkitoBio/goquery v1.11.0 github.com/PuerkitoBio/goquery v1.11.0
github.com/dustin/go-humanize v1.0.1 github.com/dustin/go-humanize v1.0.1
github.com/gofiber/fiber/v2 v2.52.10
github.com/gofiber/template/handlebars/v2 v2.1.12
github.com/gorilla/feeds v1.2.0 github.com/gorilla/feeds v1.2.0
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/mailgun/raymond/v2 v2.0.48 github.com/mailgun/raymond/v2 v2.0.48
@@ -17,26 +15,13 @@ require (
) )
require ( require (
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/aymerick/douceur v0.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
github.com/gofiber/template v1.8.3 // indirect
github.com/gofiber/utils v1.2.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/css v1.0.1 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/philhofer/fwd v1.2.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect github.com/sirupsen/logrus v1.9.3 // indirect
github.com/stretchr/testify v1.10.0 // indirect
github.com/tidwall/match v1.2.0 // indirect github.com/tidwall/match v1.2.0 // indirect
github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect
github.com/tinylib/msgp v1.6.3 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.68.0 // indirect
golang.org/x/net v0.48.0 // indirect golang.org/x/net v0.48.0 // indirect
golang.org/x/sys v0.39.0 // indirect golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect golang.org/x/text v0.32.0 // indirect

35
go.sum
View File

@@ -1,57 +1,31 @@
github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw= github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw=
github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ= github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= 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/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/gofiber/fiber/v2 v2.52.10 h1:jRHROi2BuNti6NYXmZ6gbNSfT3zj/8c0xy94GOU5elY=
github.com/gofiber/fiber/v2 v2.52.10/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/gofiber/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc=
github.com/gofiber/template v1.8.3/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8=
github.com/gofiber/template/handlebars/v2 v2.1.12 h1:uWBMEnhTxVyarRjyj5uDrNg8rVMGzi6fhsL+PAJBvb0=
github.com/gofiber/template/handlebars/v2 v2.1.12/go.mod h1:K3h933a8wPFjIrLRUcnIVPTUW867ND6gqpw0zZ3yKpk=
github.com/gofiber/utils v1.2.0 h1:NCaqd+Efg3khhN++eeUUTyBz+byIxAsmIjpl8kKOMIc=
github.com/gofiber/utils v1.2.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= 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/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 h1:O6pBiXJ5JHhPvqy53NsjKOThq+dNFm8+DFrxBEdzSCc=
github.com/gorilla/feeds v1.2.0/go.mod h1:WMib8uJP3BbY+X8Szd1rA5Pzhdfh+HCCAYT2z7Fza6Y= github.com/gorilla/feeds v1.2.0/go.mod h1:WMib8uJP3BbY+X8Szd1rA5Pzhdfh+HCCAYT2z7Fza6Y=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw=
github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= 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/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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/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 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
@@ -72,14 +46,6 @@ github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s=
github.com/tinylib/msgp v1.6.3/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
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.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok=
github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
gitlab.com/golang-commonmark/linkify v0.0.0-20200225224916-64bca66f6ad3 h1:1Coh5BsUBlXoEJmIEaNzVAWrtg9k7/eJzailMQr1grw= gitlab.com/golang-commonmark/linkify v0.0.0-20200225224916-64bca66f6ad3 h1:1Coh5BsUBlXoEJmIEaNzVAWrtg9k7/eJzailMQr1grw=
gitlab.com/golang-commonmark/linkify v0.0.0-20200225224916-64bca66f6ad3/go.mod h1:Gn+LZmCrhPECMD3SOKlE+BOHwhOYD9j7WT9NUtkCrC8= gitlab.com/golang-commonmark/linkify v0.0.0-20200225224916-64bca66f6ad3/go.mod h1:Gn+LZmCrhPECMD3SOKlE+BOHwhOYD9j7WT9NUtkCrC8=
@@ -120,7 +86,6 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.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.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=

224
main.go
View File

@@ -3,23 +3,37 @@ package main
import ( import (
"flag" "flag"
"fmt" "fmt"
"io"
"net/http" "net/http"
"os" "os"
"time" "strings"
"codeberg.org/rimgo/rimgo/pages" "codeberg.org/rimgo/rimgo/pages"
"codeberg.org/rimgo/rimgo/render"
"codeberg.org/rimgo/rimgo/static" "codeberg.org/rimgo/rimgo/static"
"codeberg.org/rimgo/rimgo/utils" "codeberg.org/rimgo/rimgo/utils"
"codeberg.org/rimgo/rimgo/views" "codeberg.org/rimgo/rimgo/views"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cache"
"github.com/gofiber/fiber/v2/middleware/filesystem"
"github.com/gofiber/fiber/v2/middleware/recover"
"github.com/gofiber/template/handlebars/v2"
"github.com/joho/godotenv" "github.com/joho/godotenv"
"github.com/mailgun/raymond/v2"
) )
// a handler that returns error if it can't respond
type handler func(w http.ResponseWriter, r *http.Request) error
func wrapHandler(h handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if v := recover(); v != nil {
pages.RenderError(w, r, 500, fmt.Sprint(v))
}
}()
err := h(w, r)
if err != nil {
fmt.Println(err)
pages.RenderError(w, r, 500, err.Error())
}
})
}
func main() { func main() {
envPath := flag.String("c", ".env", "Path to env file") envPath := flag.String("c", ".env", "Path to env file")
godotenv.Load(*envPath) godotenv.Load(*envPath)
@@ -27,120 +41,110 @@ func main() {
pages.InitializeApiClient() pages.InitializeApiClient()
views := http.FS(views.GetFiles()) views := views.GetFiles()
if os.Getenv("ENV") == "dev" { static := static.GetFiles()
views = http.Dir("./views") render.Initialize(views)
}
engine := handlebars.NewFileSystem(views, ".hbs")
engine.AddFunc("noteq", func(a interface{}, b interface{}, options *raymond.Options) interface{} { app := http.NewServeMux()
if raymond.Str(a) != raymond.Str(b) {
return options.Fn()
}
return ""
})
app := fiber.New(fiber.Config{ app.Handle("GET /static/", http.StripPrefix("/static/", http.FileServerFS(static)))
Views: engine, app.Handle("GET /robots.txt", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Prefork: utils.Config.FiberPrefork, file, _ := static.Open("robots.txt")
UnescapePath: true, defer file.Close()
StreamRequestBody: true, io.Copy(w, file)
ErrorHandler: func(ctx *fiber.Ctx, err error) error { }))
code := fiber.StatusInternalServerError app.Handle("GET /favicon.ico", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
file, _ := static.Open("favicon/favicon.ico")
if e, ok := err.(*fiber.Error); ok { defer file.Close()
code = e.Code io.Copy(w, file)
}
return utils.RenderError(ctx, code)
},
})
app.Use(recover.New(recover.Config{
EnableStackTrace: true,
StackTraceHandler: func(c *fiber.Ctx, e interface{}) {
fmt.Println(e)
},
})) }))
if os.Getenv("ENV") == "dev" { if os.Getenv("ENV") == "dev" {
app.Use("/static", filesystem.New(filesystem.Config{ app.Handle("GET /errors/429", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Root: http.Dir("./static"), pages.RenderError(w, r, 429)
})) }))
app.Get("/errors/429", func(c *fiber.Ctx) error { app.Handle("GET /errors/429/img", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
return c.Render("errors/429", nil) w.Header().Set("Location", "/static/img/error-429.png")
}) w.WriteHeader(302)
app.Get("/errors/429/img", func(c *fiber.Ctx) error {
return c.Redirect("/static/img/error-429.png")
})
app.Get("/errors/404", func(c *fiber.Ctx) error {
return c.Render("errors/404", nil)
})
app.Get("/errors/404/img", func(c *fiber.Ctx) error {
return c.Redirect("/static/img/error-404.png")
})
app.Get("/errors/error", func(c *fiber.Ctx) error {
return c.Render("errors/error", fiber.Map{
"err": "Test error",
})
})
app.Get("/errors/error/img", func(c *fiber.Ctx) error {
return c.Redirect("/static/img/error-generic.png")
})
} else {
app.Use("/static", filesystem.New(filesystem.Config{
MaxAge: 2592000,
Root: http.FS(static.GetFiles()),
})) }))
app.Use(cache.New(cache.Config{ app.Handle("GET /errors/404", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expiration: 30 * time.Minute, pages.RenderError(w, r, 404)
MaxBytes: 25000000, }))
KeyGenerator: func(c *fiber.Ctx) string { app.Handle("GET /errors/404/img", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
return utils.GetInstanceProtocol(c) + " " + c.OriginalURL() w.Header().Set("Location", "/static/img/error-404.png")
}, w.WriteHeader(302)
CacheControl: true, }))
StoreResponseHeaders: true, 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.Handle("GET /{$}", wrapHandler(pages.HandleFrontpage))
app.Get("/robots.txt", func(c *fiber.Ctx) error { app.Handle("GET /a/{postID}", wrapHandler(pages.HandlePost))
file, _ := static.GetFiles().ReadFile("robots.txt") app.Handle("GET /a/{postID}/embed", wrapHandler(pages.HandleEmbed))
_, err := c.Write(file) 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 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) addr := utils.Config.Addr + ":" + utils.Config.Port
app.Get("/about", pages.HandleAbout) fmt.Println("listening on " + addr)
app.Get("/privacy", pages.HandlePrivacy) err := http.ListenAndServe(addr, app)
app.Get("/search", pages.HandleSearch)
app.Get("/trending.:type", pages.HandleTrendingRSS)
app.Get("/trending", pages.HandleTrending)
app.Get("/a/:postID", pages.HandlePost)
app.Get("/a/:postID/embed", pages.HandleEmbed)
app.Get("/t/:tag.:type", pages.HandleTagRSS)
app.Get("/t/:tag", pages.HandleTag)
app.Get("/t/:tag/:postID", pages.HandlePost)
app.Get("/r/:sub/:postID", pages.HandlePost)
app.Get("/user/:userID.:type", pages.HandleUserRSS)
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)
err := app.Listen(utils.Config.Addr + ":" + utils.Config.Port)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }

View File

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

View File

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

View File

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

44
pages/error.go Normal file
View File

@@ -0,0 +1,44 @@
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,19 +1,21 @@
package pages package pages
import ( import (
"net/http"
"codeberg.org/rimgo/rimgo/render"
"codeberg.org/rimgo/rimgo/utils" "codeberg.org/rimgo/rimgo/utils"
"github.com/gofiber/fiber/v2"
) )
var VersionInfo string var VersionInfo string
func HandleFrontpage(c *fiber.Ctx) error { func HandleFrontpage(w http.ResponseWriter, r *http.Request) error {
utils.SetHeaders(c) utils.SetHeaders(w)
c.Set("X-Frame-Options", "DENY") w.Header().Set("X-Frame-Options", "DENY")
c.Set("Cache-Control", "public,max-age=31557600") w.Header().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") w.Header().Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'; style-src 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
return c.Render("frontpage", fiber.Map{ return render.Render(w, "frontpage", map[string]any{
"config": utils.Config, "config": utils.Config,
"version": VersionInfo, "version": VersionInfo,
}) })

View File

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

View File

@@ -3,12 +3,13 @@ package pages
import ( import (
"crypto/rand" "crypto/rand"
"fmt" "fmt"
"net/http"
"strconv" "strconv"
"strings" "strings"
"codeberg.org/rimgo/rimgo/api" "codeberg.org/rimgo/rimgo/api"
"codeberg.org/rimgo/rimgo/render"
"codeberg.org/rimgo/rimgo/utils" "codeberg.org/rimgo/rimgo/utils"
"github.com/gofiber/fiber/v2"
) )
// Cursed function // Cursed function
@@ -33,31 +34,31 @@ func nextInTag(client *api.Client, tagname, sort, page, I string) string {
return tag.Posts[i+1].Link return tag.Posts[i+1].Link
} }
func HandlePost(c *fiber.Ctx) error { func HandlePost(w http.ResponseWriter, r *http.Request) error {
utils.SetHeaders(c) utils.SetHeaders(w)
c.Set("X-Frame-Options", "DENY") w.Header().Set("X-Frame-Options", "DENY")
postId := c.Params("postID") postId := r.PathValue("postID")
if strings.Contains(postId, "-") { if strings.Contains(postId, "-") {
postId = postId[len(postId)-7:] postId = postId[len(postId)-7:]
} }
post, err := api.Album{}, error(nil) post, err := api.Album{}, error(nil)
switch { switch {
case strings.HasPrefix(c.Path(), "/a"): case strings.HasPrefix(r.URL.Path, "/a"):
post, err = ApiClient.FetchAlbum(postId) post, err = ApiClient.FetchAlbum(postId)
case strings.HasPrefix(c.Path(), "/gallery"): case strings.HasPrefix(r.URL.Path, "/gallery"):
post, err = ApiClient.FetchPosts(postId) post, err = ApiClient.FetchPosts(postId)
case strings.HasPrefix(c.Path(), "/t"): case strings.HasPrefix(r.URL.Path, "/t"):
post, err = ApiClient.FetchPosts(postId) post, err = ApiClient.FetchPosts(postId)
default: default:
post, err = ApiClient.FetchMedia(postId) post, err = ApiClient.FetchMedia(postId)
} }
if err != nil && err.Error() == "ratelimited by imgur" { if err != nil && err.Error() == "ratelimited by imgur" {
return utils.RenderError(c, 429) return RenderError(w, r, 429)
} }
if err != nil && post.Id == "" && strings.Contains(err.Error(), "404") { if err != nil && post.Id == "" && strings.Contains(err.Error(), "404") {
return utils.RenderError(c, 404) return RenderError(w, r, 404)
} }
if err != nil { if err != nil {
return err return err
@@ -65,13 +66,13 @@ func HandlePost(c *fiber.Ctx) error {
comments := []api.Comment{} comments := []api.Comment{}
if post.SharedWithCommunity { if post.SharedWithCommunity {
c.Set("Cache-Control", "public,max-age=604800") w.Header().Set("Cache-Control", "public,max-age=604800")
comments, err = ApiClient.FetchComments(postId) comments, err = ApiClient.FetchComments(postId)
if err != nil { if err != nil {
return err return err
} }
} else { } else {
c.Set("Cache-Control", "public,max-age=31557600") w.Header().Set("Cache-Control", "public,max-age=31557600")
} }
nonce := "" nonce := ""
@@ -82,16 +83,16 @@ func HandlePost(c *fiber.Ctx) error {
nonce = fmt.Sprintf("%x", b) nonce = fmt.Sprintf("%x", b)
csp = csp + " 'nonce-" + nonce + "'" csp = csp + " 'nonce-" + nonce + "'"
} }
c.Set("Content-Security-Policy", csp) w.Header().Set("Content-Security-Policy", csp)
var next string var next string
tagParam := strings.Split(c.Query("tag"), ".") tagParam := strings.Split(r.URL.Query().Get("tag"), ".")
if len(tagParam) == 4 { if len(tagParam) == 4 {
tag, sort, page, index := tagParam[0], tagParam[1], tagParam[2], tagParam[3] tag, sort, page, index := tagParam[0], tagParam[1], tagParam[2], tagParam[3]
next = nextInTag(ApiClient, tag, sort, page, index) next = nextInTag(ApiClient, tag, sort, page, index)
} }
return c.Render("post", fiber.Map{ return render.Render(w, "post", map[string]any{
"post": post, "post": post,
"next": next, "next": next,
"comments": comments, "comments": comments,

View File

@@ -1,17 +1,18 @@
package pages package pages
import ( import (
"github.com/gofiber/fiber/v2" "net/http"
"codeberg.org/rimgo/rimgo/render"
"codeberg.org/rimgo/rimgo/utils" "codeberg.org/rimgo/rimgo/utils"
) )
func HandlePrivacy(c *fiber.Ctx) error { func HandlePrivacy(w http.ResponseWriter, r *http.Request) error {
utils.SetHeaders(c) utils.SetHeaders(w)
c.Set("X-Frame-Options", "DENY") w.Header().Set("X-Frame-Options", "DENY")
c.Set("Content-Security-Policy", "default-src 'none'; form-action 'self'; style-src 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content") w.Header().Set("Content-Security-Policy", "default-src 'none'; form-action 'self'; style-src 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
return c.Render("privacy", fiber.Map{ return render.Render(w, "privacy", map[string]any{
"config": utils.Config, "config": utils.Config,
"version": VersionInfo, "version": VersionInfo,
}) })

View File

@@ -1,49 +1,54 @@
package pages package pages
import ( import (
"mime"
"net/http"
"time" "time"
"codeberg.org/rimgo/rimgo/api" "codeberg.org/rimgo/rimgo/api"
"codeberg.org/rimgo/rimgo/utils" "codeberg.org/rimgo/rimgo/utils"
"github.com/gofiber/fiber/v2"
"github.com/gorilla/feeds" "github.com/gorilla/feeds"
) )
func HandleTagRSS(c *fiber.Ctx) error { func HandleTagRSS(w http.ResponseWriter, r *http.Request) error {
utils.SetHeaders(c) utils.SetHeaders(w)
tag, err := ApiClient.FetchTag(c.Params("tag"), c.Query("sort"), "1") tag, err := ApiClient.FetchTag(r.PathValue("tag"), r.URL.Query().Get("sort"), "1")
if err != nil && err.Error() == "ratelimited by imgur" { if err != nil && err.Error() == "ratelimited by imgur" {
return c.Status(429).SendString("rate limited by imgur") w.WriteHeader(429)
_, err := w.Write([]byte("rate limited by imgur"))
return err
} }
if err != nil { if err != nil {
return err return err
} }
if tag.Display == "" { if tag.Display == "" {
return c.Status(404).SendString("tag not found") w.WriteHeader(404)
_, err := w.Write([]byte("tag not found"))
return err
} }
instance := utils.GetInstanceUrl(c) instance := utils.GetInstanceUrl(r)
feed := &feeds.Feed{ feed := &feeds.Feed{
Title: tag.Display + " on Imgur", Title: tag.Display + " on Imgur",
Link: &feeds.Link{Href: instance + "/t/" + c.Params("tag")}, Link: &feeds.Link{Href: instance + "/t/" + r.PathValue("tag")},
Created: time.Now(), Created: time.Now(),
} }
return handleFeed(c, instance, feed, tag.Posts) return handleFeed(w, r, instance, feed, tag.Posts)
} }
func HandleTrendingRSS(c *fiber.Ctx) error { func HandleTrendingRSS(w http.ResponseWriter, r *http.Request) error {
utils.SetHeaders(c) utils.SetHeaders(w)
section := c.Query("section") section := r.URL.Query().Get("section")
switch section { switch section {
case "hot", "new", "top": case "hot", "new", "top":
default: default:
section = "hot" section = "hot"
} }
sort := c.Query("sort") sort := r.URL.Query().Get("sort")
switch sort { switch sort {
case "newest", "best", "popular": case "newest", "best", "popular":
default: default:
@@ -55,7 +60,7 @@ func HandleTrendingRSS(c *fiber.Ctx) error {
return err return err
} }
instance := utils.GetInstanceUrl(c) instance := utils.GetInstanceUrl(r)
feed := &feeds.Feed{ feed := &feeds.Feed{
Title: "Trending on Imgur", Title: "Trending on Imgur",
@@ -63,24 +68,23 @@ func HandleTrendingRSS(c *fiber.Ctx) error {
Created: time.Now(), Created: time.Now(),
} }
return handleFeed(c, instance, feed, results) return handleFeed(w, r, instance, feed, results)
} }
func HandleUserRSS(c *fiber.Ctx) error { func HandleUserRSS(w http.ResponseWriter, r *http.Request) error {
utils.SetHeaders(c) utils.SetHeaders(w)
user := c.Params("userID") user := r.PathValue("userID")
submissions, err := ApiClient.FetchSubmissions(user, "newest", "1") submissions, err := ApiClient.FetchSubmissions(user, "newest", "1")
if err != nil && err.Error() == "ratelimited by imgur" { if err != nil && err.Error() == "ratelimited by imgur" {
c.Status(429) return RenderError(w, r, 429)
return utils.RenderError(c, 429)
} }
if err != nil { if err != nil {
return err return err
} }
instance := utils.GetInstanceUrl(c) instance := utils.GetInstanceUrl(r)
feed := &feeds.Feed{ feed := &feeds.Feed{
Title: user + " on Imgur", Title: user + " on Imgur",
@@ -88,10 +92,10 @@ func HandleUserRSS(c *fiber.Ctx) error {
Created: time.Now(), Created: time.Now(),
} }
return handleFeed(c, instance, feed, submissions) return handleFeed(w, r, instance, feed, submissions)
} }
func handleFeed(c *fiber.Ctx, instance string, feed *feeds.Feed, posts []api.Submission) error { func handleFeed(w http.ResponseWriter, r *http.Request, instance string, feed *feeds.Feed, posts []api.Submission) error {
feed.Items = []*feeds.Item{} feed.Items = []*feeds.Item{}
for _, post := range posts { for _, post := range posts {
@@ -110,27 +114,30 @@ func handleFeed(c *fiber.Ctx, instance string, feed *feeds.Feed, posts []api.Sub
feed.Items = append(feed.Items, item) feed.Items = append(feed.Items, item)
} }
c.Type(c.Params("type")) w.Header().Set("Content-Type", mime.TypeByExtension("."+r.PathValue("type")))
switch c.Params("type") { switch r.PathValue("type") {
case "atom": case "atom":
body, err := feed.ToAtom() body, err := feed.ToAtom()
if err != nil { if err != nil {
return err return err
} }
return c.SendString(body) w.Write([]byte(body))
case "json": case "json":
body, err := feed.ToJSON() body, err := feed.ToJSON()
if err != nil { if err != nil {
return err return err
} }
return c.JSON(body) w.Write([]byte(body))
case "rss": case "rss":
body, err := feed.ToRss() body, err := feed.ToRss()
if err != nil { if err != nil {
return err return err
} }
return c.SendString(body) w.Write([]byte(body))
default: default:
return c.Status(400).SendString("invalid type") w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(400)
w.Write([]byte("invalid type"))
} }
return nil
} }

View File

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

View File

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

View File

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

View File

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

44
render/helpers.go Normal file
View File

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

76
render/render.go Normal file
View File

@@ -0,0 +1,76 @@
// stolen from gofiber/template but simpler
package render
import (
"fmt"
"io"
"io/fs"
"path/filepath"
"strings"
"github.com/mailgun/raymond/v2"
)
var Renderer *renderer
func Render(out io.Writer, name string, bind map[string]any) error {
return Renderer.Render(out, name, bind)
}
type renderer struct {
templates map[string]*raymond.Template
}
const ext = ".hbs"
func Initialize(views fs.FS) {
r := new(renderer)
r.templates = make(map[string]*raymond.Template)
r.registerHelpers()
fs.WalkDir(views, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() {
return err
}
name, hasExt := strings.CutSuffix(path, ext)
if !hasExt {
return nil
}
path = filepath.ToSlash(path)
file, err := views.Open(path)
if err != nil {
return err
}
defer file.Close()
buf, err := io.ReadAll(file)
if err != nil {
return err
}
tmpl, err := raymond.Parse(string(buf))
if err != nil {
return err
}
r.templates[name] = tmpl
return nil
})
for j := range r.templates {
for n, template := range r.templates {
r.templates[j].RegisterPartialTemplate(n, template)
}
}
Renderer = r
}
func (r *renderer) Render(out io.Writer, name string, bind map[string]any) error {
tmpl := r.templates[name]
if tmpl == nil {
return fmt.Errorf("render: template %s does not exist", name)
}
parsed, err := tmpl.Exec(bind)
if err != nil {
return fmt.Errorf("render: %w", err)
}
if _, err = out.Write([]byte(parsed)); err != nil {
return fmt.Errorf("render: %w", err)
}
return err
}

53
render/sanitize.go Normal file
View File

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

22
utils/accepts.go Normal file
View File

@@ -0,0 +1,22 @@
package utils
import (
"net/http"
"strings"
)
func Accepts(r *http.Request, format string) bool {
format = strings.ToLower(format)
group := strings.Split(format, "/")[0] + "/*"
header := r.Header.Get("Accept")
if header == "" {
return false
}
for _, mime := range strings.Split(header, ",") {
mime = strings.ToLower(strings.TrimSpace(strings.SplitN(mime, ";", 2)[0]))
if mime == "*/*" || mime == format || mime == group {
return true
}
}
return false
}

View File

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

View File

@@ -1,25 +0,0 @@
package utils
import (
"strconv"
"strings"
"codeberg.org/rimgo/rimgo/static"
"github.com/gofiber/fiber/v2"
)
func RenderError(c *fiber.Ctx, code int) error {
if !strings.Contains(c.Get("Accept"), "html") && c.Params("extension") != "" {
codeStr := "generic"
if code != 0 {
codeStr = strconv.Itoa(code)
}
img, _ := static.GetFiles().ReadFile("img/error-" + codeStr + ".png")
c.Set("Content-Type", "image/png")
return c.Status(code).Send(img)
} else {
return c.Status(code).Render("errors/" + strconv.Itoa(code), fiber.Map{
"path": c.Path(),
})
}
}

View File

@@ -1,18 +1,21 @@
package utils package utils
import "github.com/gofiber/fiber/v2" import "net/http"
func GetInstanceProtocol(c *fiber.Ctx) string { func GetInstanceProtocol(r *http.Request) string {
proto := "https" proto := "https"
if !Config.Secure { if !Config.Secure {
proto = "http" proto = "http"
} }
if Config.ProtocolDetection { if Config.ProtocolDetection {
proto = c.Get("X-Forwarded-Proto", proto) xproto := r.Header.Get("X-Forwarded-Proto")
if xproto != "" {
proto = xproto
}
} }
return proto return proto
} }
func GetInstanceUrl(c *fiber.Ctx) string { func GetInstanceUrl(r *http.Request) string {
return GetInstanceProtocol(c) + "://" + c.Hostname() return GetInstanceProtocol(r) + "://" + r.Host
} }

View File

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

25
utils/rewriteUrl.go Normal file
View File

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

16
utils/splitNameExt.go Normal file
View File

@@ -0,0 +1,16 @@
package utils
func SplitNameExt(path string) (name, ext string) {
name, ext = path, ""
for range 5 {
if len(name) == 0 || name[len(name)-1] == '.' || name[len(name)-1] == '/' {
break
}
name = name[:len(name)-1]
ext = path[len(name):]
}
if len(name) == 0 || name[len(name)-1] != '.' {
return path, ""
}
return
}

View File

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

View File

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

View File

@@ -75,7 +75,7 @@
{{/equal}} {{/equal}}
{{#if this.Description}} {{#if this.Description}}
<p>{{{this.Description}}}</p> <p>{{{sanitizeDescription(this.Description)}}}</p>
{{/if}} {{/if}}
{{/each}} {{/each}}
</div> </div>