diff --git a/api/comments.go b/api/comments.go
index 3243b94..931e7e0 100644
--- a/api/comments.go
+++ b/api/comments.go
@@ -1,13 +1,15 @@
package api
import (
+ "encoding/json"
+ "errors"
+ "fmt"
"regexp"
"strings"
"sync"
"time"
"codeberg.org/rimgo/rimgo/utils"
- "github.com/dustin/go-humanize"
"github.com/microcosm-cc/bluemonday"
"github.com/patrickmn/go-cache"
"github.com/tidwall/gjson"
@@ -15,49 +17,51 @@ import (
)
type Comment struct {
- Comments []Comment
- User User
- Post Submission
- Id string
- Comment string
- Upvotes int64
- Downvotes int64
- Platform string
- CreatedAt string
- RelTime string
- UpdatedAt string
- DeletedAt string
+ ID int `json:"id"`
+ ParentID int `json:"parent_id"`
+ Comment string `json:"comment"`
+ PostID string `json:"post_id"`
+ UpvoteCount int `json:"upvote_count"`
+ DownvoteCount int `json:"downvote_count"`
+ PointCount int `json:"point_count"`
+ Vote struct{} `json:"vote"` // ???
+ PlatformID int `json:"platform_id"`
+ Platform string `json:"platform"`
+ CreatedAt time.Time `json:"created_at"`
+ UpdatedAt time.Time `json:"updated_at"`
+ DeletedAt time.Time `json:"deleted_at"`
+ Next struct{} `json:"next"` // ???
+ Comments commentArray `json:"comments"`
+ AccountID int `json:"account_id"`
+ Account _ApiUser `json:"account"`
+ Post commentPost `json:"post"`
+}
+
+type _ApiUser struct {
+ ID int `json:"id"`
+ Username string `json:"username"`
+ Avatar string `json:"avatar"`
}
func (client *Client) FetchComments(galleryID string) ([]Comment, error) {
cacheData, found := client.Cache.Get(galleryID + "-comments")
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 {
return []Comment{}, nil
}
- wg := sync.WaitGroup{}
- comments := make([]Comment, 0)
- data.Get("data").ForEach(
- func(key, value gjson.Result) bool {
- wg.Add(1)
+ var parsed commentApiResponse
+ err = json.Unmarshal(data, &parsed)
+ if err != nil {
+ return []Comment{}, err
+ }
- go func() {
- defer wg.Done()
- comments = append(comments, parseComment(value))
- }()
-
- return true
- },
- )
- wg.Wait()
-
- client.Cache.Set(galleryID+"-comments", comments, cache.DefaultExpiration)
- return comments, nil
+ client.Cache.Set(galleryID+"-comments", parsed.Data, cache.DefaultExpiration)
+ return parsed.Data, nil
}
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 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())
+func parseComment(data json.RawMessage, out *Comment) {
+ err := json.Unmarshal(data, &out)
+ if err != nil {
+ panic(err)
+ }
- userAvatar := strings.ReplaceAll(data.Get("account.avatar").String(), "https://i.imgur.com", "")
+ comment := &out.Comment
+ *comment = strings.ReplaceAll(*comment, "\n", "
")
- 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", "
")
-
- for _, match := range imgRe.FindAllString(comment, -1) {
+ for _, match := range imgRe.FindAllString(*comment, -1) {
img := iImgurRe.ReplaceAllString(match, "")
img = ``
- 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 = ``
- comment = strings.Replace(comment, match, vid, 1)
+ *comment = strings.Replace(*comment, match, vid, 1)
}
- for _, l := range linkify.Links(comment) {
- origLink := comment[l.Start:l.End]
+ for _, l := range linkify.Links(*comment) {
+ origLink := (*comment)[l.Start:l.End]
link := `` + origLink + ``
- comment = strings.Replace(comment, origLink, link, 1)
+ *comment = strings.Replace(*comment, origLink, link, 1)
}
- comment = imgurRe.ReplaceAllString(comment, "/$1/$2")
- comment = imgurRe2.ReplaceAllString(comment, "/$1")
+ *comment = imgurRe.ReplaceAllString(*comment, "/$1/$2")
+ *comment = imgurRe2.ReplaceAllString(*comment, "/$1")
p := bluemonday.UGCPolicy()
p.AllowImages()
@@ -122,24 +107,51 @@ func parseComment(data gjson.Result) Comment {
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,
- }
+ *comment = p.Sanitize(*comment)
+}
+
+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"`
}
diff --git a/api/user.go b/api/user.go
index 576373f..886eb79 100644
--- a/api/user.go
+++ b/api/user.go
@@ -1,6 +1,7 @@
package api
import (
+ "encoding/json"
"io"
"net/http"
"strings"
@@ -154,7 +155,7 @@ func (client *Client) FetchUserFavorites(username string, sort string, page stri
func (client *Client) FetchUserComments(username string) ([]Comment, error) {
cacheData, found := client.Cache.Get(username + "-usercomments")
if found {
- return cacheData.([]Comment), nil
+ return cacheData.(commentArray), 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
}
- body, err := io.ReadAll(res.Body)
+ data, err := io.ReadAll(res.Body)
if err != nil {
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)
- data.Get("data").ForEach(
- func(key, value gjson.Result) bool {
- comments = append(comments, parseComment(value))
- return true
- },
- )
-
- client.Cache.Set(username+"-usercomments", comments, cache.DefaultExpiration)
- return comments, nil
+ client.Cache.Set(username+"-usercomments", parsed.Data, cache.DefaultExpiration)
+ return parsed.Data, nil
}
func parseSubmission(value gjson.Result) Submission {
diff --git a/main.go b/main.go
index 0b99d4a..505cdc0 100644
--- a/main.go
+++ b/main.go
@@ -23,13 +23,13 @@ func wrapHandler(h handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if v := recover(); v != nil {
- utils.RenderError(w, r, 500, fmt.Sprint(v))
+ pages.RenderError(w, r, 500, fmt.Sprint(v))
}
}()
err := h(w, r)
if err != nil {
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" {
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) {
w.Header().Set("Location", "/static/img/error-429.png")
w.WriteHeader(302)
}))
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) {
w.Header().Set("Location", "/static/img/error-404.png")
diff --git a/pages/embed.go b/pages/embed.go
index f7016c6..92c7694 100644
--- a/pages/embed.go
+++ b/pages/embed.go
@@ -24,10 +24,10 @@ func HandleEmbed(w http.ResponseWriter, r *http.Request) error {
post, err = ApiClient.FetchMedia(r.PathValue("postID"))
}
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") {
- return utils.RenderError(w, r, 404)
+ return RenderError(w, r, 404)
}
if err != nil {
return err
diff --git a/utils/error.go b/pages/error.go
similarity index 87%
rename from utils/error.go
rename to pages/error.go
index 30bd215..0806869 100644
--- a/utils/error.go
+++ b/pages/error.go
@@ -1,4 +1,4 @@
-package utils
+package pages
import (
"fmt"
@@ -8,6 +8,7 @@ import (
"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) {
@@ -21,7 +22,7 @@ func RenderError(w http.ResponseWriter, r *http.Request, code int, str ...string
if code != 500 {
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.WriteHeader(code)
file, _ := static.GetFiles().Open("img/error-" + codeStr + ".png")
diff --git a/pages/media.go b/pages/media.go
index e327811..6480eaa 100644
--- a/pages/media.go
+++ b/pages/media.go
@@ -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") {
- return utils.RenderError(w, r, 404)
+ return RenderError(w, r, 404)
} else if res.StatusCode == 429 {
- return utils.RenderError(w, r, 429)
+ return RenderError(w, r, 429)
}
w.Header().Set("Accept-Ranges", "bytes")
diff --git a/pages/post.go b/pages/post.go
index 171d312..084f134 100644
--- a/pages/post.go
+++ b/pages/post.go
@@ -55,10 +55,10 @@ func HandlePost(w http.ResponseWriter, r *http.Request) error {
post, err = ApiClient.FetchMedia(postId)
}
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") {
- return utils.RenderError(w, r, 404)
+ return RenderError(w, r, 404)
}
if err != nil {
return err
diff --git a/pages/rss.go b/pages/rss.go
index e748b40..d64f71f 100644
--- a/pages/rss.go
+++ b/pages/rss.go
@@ -78,7 +78,7 @@ func HandleUserRSS(w http.ResponseWriter, r *http.Request) error {
submissions, err := ApiClient.FetchSubmissions(user, "newest", "1")
if err != nil && err.Error() == "ratelimited by imgur" {
- return utils.RenderError(w, r, 429)
+ return RenderError(w, r, 429)
}
if err != nil {
return err
diff --git a/pages/tag.go b/pages/tag.go
index 4f1da58..abd7848 100644
--- a/pages/tag.go
+++ b/pages/tag.go
@@ -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)
if err != nil && err.Error() == "ratelimited by imgur" {
- return utils.RenderError(w, r, 429)
+ return RenderError(w, r, 429)
}
if err != nil {
return err
}
if tag.Display == "" {
- return utils.RenderError(w, r, 404)
+ return RenderError(w, r, 404)
}
return render.Render(w, "tag", map[string]any{
diff --git a/pages/user.go b/pages/user.go
index 482b5d9..00aa387 100644
--- a/pages/user.go
+++ b/pages/user.go
@@ -26,18 +26,18 @@ func HandleUser(w http.ResponseWriter, r *http.Request) error {
user, err := ApiClient.FetchUser(r.PathValue("userID"))
if err != nil && err.Error() == "ratelimited by imgur" {
- return utils.RenderError(w, r, 429)
+ return RenderError(w, r, 429)
}
if err != nil {
return err
}
if user.Username == "" {
- return utils.RenderError(w, r, 404)
+ return RenderError(w, r, 404)
}
submissions, err := ApiClient.FetchSubmissions(r.PathValue("userID"), "newest", page)
if err != nil && err.Error() == "ratelimited by imgur" {
- return utils.RenderError(w, r, 429)
+ return RenderError(w, r, 429)
}
if err != nil {
return err
@@ -60,18 +60,18 @@ func HandleUserComments(w http.ResponseWriter, r *http.Request) error {
user, err := ApiClient.FetchUser(r.PathValue("userID"))
if err != nil && err.Error() == "ratelimited by imgur" {
- return utils.RenderError(w, r, 429)
+ return RenderError(w, r, 429)
}
if err != nil {
return err
}
if user.Username == "" {
- return utils.RenderError(w, r, 404)
+ return RenderError(w, r, 404)
}
comments, err := ApiClient.FetchUserComments(r.PathValue("userID"))
if err != nil && err.Error() == "ratelimited by imgur" {
- return utils.RenderError(w, r, 429)
+ return RenderError(w, r, 429)
}
if err != nil {
return err
@@ -101,18 +101,18 @@ func HandleUserFavorites(w http.ResponseWriter, r *http.Request) error {
user, err := ApiClient.FetchUser(r.PathValue("userID"))
if err != nil && err.Error() == "ratelimited by imgur" {
- return utils.RenderError(w, r, 429)
+ return RenderError(w, r, 429)
}
if err != nil {
return err
}
if user.Username == "" {
- return utils.RenderError(w, r, 404)
+ return RenderError(w, r, 404)
}
favorites, err := ApiClient.FetchUserFavorites(r.PathValue("userID"), "newest", page)
if err != nil && err.Error() == "ratelimited by imgur" {
- return utils.RenderError(w, r, 429)
+ return RenderError(w, r, 429)
}
if err != nil {
return err
diff --git a/render/helpers.go b/render/helpers.go
index a752ae3..7f7ba11 100644
--- a/render/helpers.go
+++ b/render/helpers.go
@@ -1,10 +1,19 @@
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() {
funcmap := map[string]any{
- "noteq": noteq,
+ "noteq": noteq,
+ "ifNonZeroTime": ifNonZeroTime,
+ "relTime": relTime,
+ "rewriteUrl": rewriteUrl,
}
raymond.RegisterHelpers(funcmap)
}
@@ -15,3 +24,19 @@ func noteq(a, b any, options *raymond.Options) any {
}
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
+}
diff --git a/utils/request.go b/utils/request.go
index 37f627a..50acaa5 100644
--- a/utils/request.go
+++ b/utils/request.go
@@ -1,6 +1,7 @@
package utils
import (
+ "encoding/json"
"fmt"
"io"
"net/http"
@@ -9,6 +10,42 @@ import (
"github.com/tidwall/gjson"
)
+func GetJSONNew(url string) (json.RawMessage, error) {
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return json.RawMessage{}, err
+ }
+
+ SetReqHeaders(req)
+
+ client := http.Client{}
+ res, err := client.Do(req)
+ if err != nil {
+ return json.RawMessage{}, err
+ }
+ rateLimitRemaining := res.Header.Get("X-RateLimit-UserRemaining")
+ if rateLimitRemaining != "" {
+ ratelimit, _ := strconv.Atoi(rateLimitRemaining)
+ if ratelimit <= 0 {
+ return json.RawMessage{}, fmt.Errorf("ratelimited by imgur")
+ }
+ }
+
+ body, err := io.ReadAll(res.Body)
+ if err != nil {
+ return json.RawMessage{}, err
+ }
+
+ switch res.StatusCode {
+ case 200:
+ return body, nil
+ case 429:
+ return json.RawMessage{}, fmt.Errorf("ratelimited by imgur")
+ default:
+ return json.RawMessage{}, fmt.Errorf("received status %s, expected 200 OK.\n%s", res.Status, string(body))
+ }
+}
+
func GetJSON(url string) (gjson.Result, error) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
diff --git a/utils/rewriteUrl.go b/utils/rewriteUrl.go
new file mode 100644
index 0000000..bc5ac1b
--- /dev/null
+++ b/utils/rewriteUrl.go
@@ -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)
+}
diff --git a/views/partials/comment.hbs b/views/partials/comment.hbs
index 476f868..9a84906 100644
--- a/views/partials/comment.hbs
+++ b/views/partials/comment.hbs
@@ -1,22 +1,22 @@
{{this.User.Username}}
+ {{#noteq this.Account.Username "[deleted]"}} +{{this.Account.Username}}
{{/noteq}} - {{#equal this.User.Username "[deleted]"}} + {{#equal this.Account.Username "[deleted]"}}[deleted]
{{/equal}}{{{this.Comment}}}
{{{this.Comment}}}