mirror of
https://codeberg.org/video-prize-ranch/rimgo.git
synced 2026-01-27 17:11:13 +00:00
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
This commit is contained in:
186
api/comments.go
186
api/comments.go
@@ -1,13 +1,15 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"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/microcosm-cc/bluemonday"
|
||||||
"github.com/patrickmn/go-cache"
|
"github.com/patrickmn/go-cache"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
@@ -15,49 +17,51 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
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)?/(.*)`)
|
var imgurRe = regexp.MustCompile(`https?://imgur\.com/(gallery|a)?/(.*)`)
|
||||||
@@ -67,51 +71,32 @@ var vidRe = regexp.MustCompile(`https?://i\.imgur\.com/(.*)\.(mp4|webm)`)
|
|||||||
var vidFormatRe = regexp.MustCompile(`\.(mp4|webm)`)
|
var vidFormatRe = regexp.MustCompile(`\.(mp4|webm)`)
|
||||||
var iImgurRe = regexp.MustCompile(`https?://i\.imgur\.com`)
|
var iImgurRe = regexp.MustCompile(`https?://i\.imgur\.com`)
|
||||||
|
|
||||||
func parseComment(data gjson.Result) Comment {
|
func parseComment(data json.RawMessage, out *Comment) {
|
||||||
createdTime, _ := time.Parse("2006-01-02T15:04:05Z", data.Get("created_at").String())
|
err := json.Unmarshal(data, &out)
|
||||||
createdAt := createdTime.Format("January 2, 2006 3:04 PM")
|
if err != nil {
|
||||||
updatedAt, _ := utils.FormatDate(data.Get("updated_at").String())
|
panic(err)
|
||||||
deletedAt, _ := utils.FormatDate(data.Get("deleted_at").String())
|
}
|
||||||
|
|
||||||
userAvatar := strings.ReplaceAll(data.Get("account.avatar").String(), "https://i.imgur.com", "")
|
comment := &out.Comment
|
||||||
|
*comment = strings.ReplaceAll(*comment, "\n", "<br>")
|
||||||
|
|
||||||
wg := sync.WaitGroup{}
|
for _, match := range imgRe.FindAllString(*comment, -1) {
|
||||||
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 := iImgurRe.ReplaceAllString(match, "")
|
||||||
img = `<img src="` + img + `" class="comment__media" loading="lazy"/>`
|
img = `<img src="` + img + `" class="comment__media" loading="lazy"/>`
|
||||||
comment = strings.Replace(comment, match, img, 1)
|
*comment = strings.Replace(*comment, match, img, 1)
|
||||||
}
|
}
|
||||||
for _, match := range vidRe.FindAllString(comment, -1) {
|
for _, match := range vidRe.FindAllString(*comment, -1) {
|
||||||
vid := iImgurRe.ReplaceAllString(match, "")
|
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>`
|
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)
|
*comment = strings.Replace(*comment, match, vid, 1)
|
||||||
}
|
}
|
||||||
for _, l := range linkify.Links(comment) {
|
for _, l := range linkify.Links(*comment) {
|
||||||
origLink := comment[l.Start:l.End]
|
origLink := (*comment)[l.Start:l.End]
|
||||||
link := `<a href="` + origLink + `">` + origLink + `</a>`
|
link := `<a href="` + origLink + `">` + origLink + `</a>`
|
||||||
comment = strings.Replace(comment, origLink, link, 1)
|
*comment = strings.Replace(*comment, origLink, link, 1)
|
||||||
}
|
}
|
||||||
comment = imgurRe.ReplaceAllString(comment, "/$1/$2")
|
*comment = imgurRe.ReplaceAllString(*comment, "/$1/$2")
|
||||||
comment = imgurRe2.ReplaceAllString(comment, "/$1")
|
*comment = imgurRe2.ReplaceAllString(*comment, "/$1")
|
||||||
|
|
||||||
p := bluemonday.UGCPolicy()
|
p := bluemonday.UGCPolicy()
|
||||||
p.AllowImages()
|
p.AllowImages()
|
||||||
@@ -122,24 +107,51 @@ func parseComment(data gjson.Result) Comment {
|
|||||||
p.RequireNoReferrerOnLinks(true)
|
p.RequireNoReferrerOnLinks(true)
|
||||||
p.RequireNoFollowOnLinks(true)
|
p.RequireNoFollowOnLinks(true)
|
||||||
p.RequireCrossOriginAnonymous(true)
|
p.RequireCrossOriginAnonymous(true)
|
||||||
comment = p.Sanitize(comment)
|
*comment = p.Sanitize(*comment)
|
||||||
|
}
|
||||||
return Comment{
|
|
||||||
Comments: comments,
|
type commentArray []Comment
|
||||||
User: User{
|
|
||||||
Id: data.Get("account.id").Int(),
|
func (arr *commentArray) UnmarshalJSON(data []byte) error {
|
||||||
Username: data.Get("account.username").String(),
|
var rawArr []json.RawMessage
|
||||||
Avatar: userAvatar,
|
err := json.Unmarshal(data, &rawArr)
|
||||||
},
|
if err != nil {
|
||||||
Post: parseSubmission(data.Get("post")),
|
return err
|
||||||
Id: data.Get("id").String(),
|
}
|
||||||
Comment: comment,
|
*arr = make(commentArray, len(rawArr))
|
||||||
Upvotes: data.Get("upvote_count").Int(),
|
panics := make([]error, 0, len(rawArr))
|
||||||
Downvotes: data.Get("downvote_count").Int(),
|
wg := sync.WaitGroup{}
|
||||||
Platform: data.Get("platform").String(),
|
var handlePanic = func() {
|
||||||
CreatedAt: createdAt,
|
if v := recover(); v != nil {
|
||||||
RelTime: humanize.Time(createdTime),
|
v, ok := v.(error)
|
||||||
UpdatedAt: updatedAt,
|
if !ok {
|
||||||
DeletedAt: deletedAt,
|
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"`
|
||||||
}
|
}
|
||||||
|
|||||||
23
api/user.go
23
api/user.go
@@ -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 {
|
||||||
|
|||||||
8
main.go
8
main.go
@@ -23,13 +23,13 @@ func wrapHandler(h handler) http.Handler {
|
|||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if v := recover(); v != nil {
|
if v := recover(); v != nil {
|
||||||
utils.RenderError(w, r, 500, fmt.Sprint(v))
|
pages.RenderError(w, r, 500, fmt.Sprint(v))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
err := h(w, r)
|
err := h(w, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
utils.RenderError(w, r, 500, err.Error())
|
pages.RenderError(w, r, 500, err.Error())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -61,14 +61,14 @@ func main() {
|
|||||||
|
|
||||||
if os.Getenv("ENV") == "dev" {
|
if os.Getenv("ENV") == "dev" {
|
||||||
app.Handle("GET /errors/429", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
app.Handle("GET /errors/429", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
utils.RenderError(w, r, 429)
|
pages.RenderError(w, r, 429)
|
||||||
}))
|
}))
|
||||||
app.Handle("GET /errors/429/img", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
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.Header().Set("Location", "/static/img/error-429.png")
|
||||||
w.WriteHeader(302)
|
w.WriteHeader(302)
|
||||||
}))
|
}))
|
||||||
app.Handle("GET /errors/404", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
app.Handle("GET /errors/404", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
utils.RenderError(w, r, 404)
|
pages.RenderError(w, r, 404)
|
||||||
}))
|
}))
|
||||||
app.Handle("GET /errors/404/img", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
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.Header().Set("Location", "/static/img/error-404.png")
|
||||||
|
|||||||
@@ -24,10 +24,10 @@ func HandleEmbed(w http.ResponseWriter, r *http.Request) error {
|
|||||||
post, err = ApiClient.FetchMedia(r.PathValue("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(w, r, 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(w, r, 404)
|
return RenderError(w, r, 404)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package utils
|
package pages
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"codeberg.org/rimgo/rimgo/render"
|
"codeberg.org/rimgo/rimgo/render"
|
||||||
"codeberg.org/rimgo/rimgo/static"
|
"codeberg.org/rimgo/rimgo/static"
|
||||||
|
"codeberg.org/rimgo/rimgo/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func RenderError(w http.ResponseWriter, r *http.Request, code int, str ...string) (err error) {
|
func RenderError(w http.ResponseWriter, r *http.Request, code int, str ...string) (err error) {
|
||||||
@@ -21,7 +22,7 @@ func RenderError(w http.ResponseWriter, r *http.Request, code int, str ...string
|
|||||||
if code != 500 {
|
if code != 500 {
|
||||||
codeStr = strconv.Itoa(code)
|
codeStr = strconv.Itoa(code)
|
||||||
}
|
}
|
||||||
if !Accepts(r, "text/html") && r.PathValue("extension") != "" {
|
if !utils.Accepts(r, "text/html") && r.PathValue("extension") != "" {
|
||||||
w.Header().Set("Content-Type", "image/png")
|
w.Header().Set("Content-Type", "image/png")
|
||||||
w.WriteHeader(code)
|
w.WriteHeader(code)
|
||||||
file, _ := static.GetFiles().Open("img/error-" + codeStr + ".png")
|
file, _ := static.GetFiles().Open("img/error-" + codeStr + ".png")
|
||||||
@@ -73,9 +73,9 @@ func handleMedia(w http.ResponseWriter, r *http.Request, 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(w, r, 404)
|
return RenderError(w, r, 404)
|
||||||
} else if res.StatusCode == 429 {
|
} else if res.StatusCode == 429 {
|
||||||
return utils.RenderError(w, r, 429)
|
return RenderError(w, r, 429)
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Accept-Ranges", "bytes")
|
w.Header().Set("Accept-Ranges", "bytes")
|
||||||
|
|||||||
@@ -55,10 +55,10 @@ func HandlePost(w http.ResponseWriter, r *http.Request) error {
|
|||||||
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(w, r, 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(w, r, 404)
|
return RenderError(w, r, 404)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ func HandleUserRSS(w http.ResponseWriter, r *http.Request) error {
|
|||||||
|
|
||||||
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" {
|
||||||
return utils.RenderError(w, r, 429)
|
return RenderError(w, r, 429)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -26,13 +26,13 @@ func HandleTag(w http.ResponseWriter, r *http.Request) error {
|
|||||||
|
|
||||||
tag, err := ApiClient.FetchTag(r.PathValue("tag"), r.URL.Query().Get("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(w, r, 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(w, r, 404)
|
return RenderError(w, r, 404)
|
||||||
}
|
}
|
||||||
|
|
||||||
return render.Render(w, "tag", map[string]any{
|
return render.Render(w, "tag", map[string]any{
|
||||||
|
|||||||
@@ -26,18 +26,18 @@ func HandleUser(w http.ResponseWriter, r *http.Request) error {
|
|||||||
|
|
||||||
user, err := ApiClient.FetchUser(r.PathValue("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(w, r, 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(w, r, 404)
|
return RenderError(w, r, 404)
|
||||||
}
|
}
|
||||||
|
|
||||||
submissions, err := ApiClient.FetchSubmissions(r.PathValue("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" {
|
||||||
return utils.RenderError(w, r, 429)
|
return RenderError(w, r, 429)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -60,18 +60,18 @@ func HandleUserComments(w http.ResponseWriter, r *http.Request) error {
|
|||||||
|
|
||||||
user, err := ApiClient.FetchUser(r.PathValue("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(w, r, 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(w, r, 404)
|
return RenderError(w, r, 404)
|
||||||
}
|
}
|
||||||
|
|
||||||
comments, err := ApiClient.FetchUserComments(r.PathValue("userID"))
|
comments, err := ApiClient.FetchUserComments(r.PathValue("userID"))
|
||||||
if err != nil && err.Error() == "ratelimited by imgur" {
|
if err != nil && err.Error() == "ratelimited by imgur" {
|
||||||
return utils.RenderError(w, r, 429)
|
return RenderError(w, r, 429)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -101,18 +101,18 @@ func HandleUserFavorites(w http.ResponseWriter, r *http.Request) error {
|
|||||||
|
|
||||||
user, err := ApiClient.FetchUser(r.PathValue("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(w, r, 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(w, r, 404)
|
return RenderError(w, r, 404)
|
||||||
}
|
}
|
||||||
|
|
||||||
favorites, err := ApiClient.FetchUserFavorites(r.PathValue("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(w, r, 429)
|
return RenderError(w, r, 429)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
package render
|
package render
|
||||||
|
|
||||||
import "github.com/mailgun/raymond/v2"
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"codeberg.org/rimgo/rimgo/utils"
|
||||||
|
"github.com/dustin/go-humanize"
|
||||||
|
"github.com/mailgun/raymond/v2"
|
||||||
|
)
|
||||||
|
|
||||||
func (r *renderer) registerHelpers() {
|
func (r *renderer) registerHelpers() {
|
||||||
funcmap := map[string]any{
|
funcmap := map[string]any{
|
||||||
"noteq": noteq,
|
"noteq": noteq,
|
||||||
|
"ifNonZeroTime": ifNonZeroTime,
|
||||||
|
"relTime": relTime,
|
||||||
|
"rewriteUrl": rewriteUrl,
|
||||||
}
|
}
|
||||||
raymond.RegisterHelpers(funcmap)
|
raymond.RegisterHelpers(funcmap)
|
||||||
}
|
}
|
||||||
@@ -15,3 +24,19 @@ func noteq(a, b any, options *raymond.Options) any {
|
|||||||
}
|
}
|
||||||
return ""
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
25
utils/rewriteUrl.go
Normal file
25
utils/rewriteUrl.go
Normal 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)
|
||||||
|
}
|
||||||
@@ -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>{{{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}}
|
||||||
|
|||||||
@@ -6,10 +6,10 @@
|
|||||||
<p class="md-container">{{{this.Comment}}}</p>
|
<p class="md-container">{{{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}}
|
||||||
|
|||||||
Reference in New Issue
Block a user