mirror of
https://codeberg.org/video-prize-ranch/rimgo.git
synced 2026-03-21 17:14:04 -04:00
Compare commits
8 Commits
encoding-j
...
fix-double
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62c821eb9f | ||
|
|
9bece7bf26 | ||
|
|
e21a9f4856 | ||
|
|
4ffe09bb81 | ||
|
|
d84ca93e0e | ||
|
|
4779d621ef | ||
|
|
3b95e89fa1 | ||
|
|
61a312aba0 |
145
api/album.go
145
api/album.go
@@ -1,55 +1,37 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"codeberg.org/rimgo/rimgo/utils"
|
"codeberg.org/rimgo/rimgo/utils"
|
||||||
|
"github.com/microcosm-cc/bluemonday"
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Album struct {
|
type Album struct {
|
||||||
ID string `json:"id"`
|
Id string
|
||||||
Title string `json:"title"`
|
Title string
|
||||||
SharedWithCommunity bool `json:"shared_with_community"`
|
Views int64
|
||||||
ViewCount int `json:"view_count"`
|
Upvotes int64
|
||||||
UpvoteCount int `json:"upvote_count"`
|
Downvotes int64
|
||||||
DownvoteCount int `json:"downvote_count"`
|
SharedWithCommunity bool
|
||||||
PointCount int `json:"point_count"`
|
CreatedAt string
|
||||||
CommentCount int `json:"comment_count"`
|
UpdatedAt string
|
||||||
Media []Media `json:"media"`
|
Comments int64
|
||||||
Tags array[TagMeta] `json:"tags"`
|
User User
|
||||||
Account _ApiUser `json:"account"`
|
Media []Media
|
||||||
|
Tags []Tag
|
||||||
}
|
}
|
||||||
|
|
||||||
type Media struct {
|
type Media struct {
|
||||||
Id string `json:"id"`
|
Id string
|
||||||
Name string `json:"name"`
|
Name string
|
||||||
Title string `json:"-"`
|
Title string
|
||||||
Description string `json:"description"` // used outside metadata in user page cover
|
Description string
|
||||||
Url string `json:"url"`
|
Url string
|
||||||
Type string `json:"type"`
|
Type string
|
||||||
MimeType string `json:"mime_type"`
|
MimeType string
|
||||||
}
|
|
||||||
|
|
||||||
func (media *Media) UnmarshalJSON(data []byte) error {
|
|
||||||
type PlainMedia Media // type alias to prevent calling this function again
|
|
||||||
var m struct {
|
|
||||||
PlainMedia
|
|
||||||
// Media type is used for user page covers too, but without the metadata field in JSON
|
|
||||||
// so it makes more sense to have these fields at top level as they were before
|
|
||||||
Metadata struct {
|
|
||||||
Title string `json:"title"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
} `json:"metadata"`
|
|
||||||
}
|
|
||||||
err := json.Unmarshal(data, &m)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*media = Media(m.PlainMedia)
|
|
||||||
media.Title = m.Metadata.Title
|
|
||||||
media.Description = m.Metadata.Description
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client *Client) FetchAlbum(albumID string) (Album, error) {
|
func (client *Client) FetchAlbum(albumID string) (Album, error) {
|
||||||
@@ -58,13 +40,12 @@ func (client *Client) FetchAlbum(albumID string) (Album, error) {
|
|||||||
return cacheData.(Album), nil
|
return cacheData.(Album), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := utils.GetJSONNew("https://api.imgur.com/post/v1/albums/" + albumID + "?client_id=" + client.ClientID + "&include=media%2Caccount")
|
data, err := utils.GetJSON("https://api.imgur.com/post/v1/albums/" + albumID + "?client_id=" + client.ClientID + "&include=media%2Caccount")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Album{}, err
|
return Album{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var album Album
|
album, err := parseAlbum(data)
|
||||||
err = json.Unmarshal(data, &album)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Album{}, err
|
return Album{}, err
|
||||||
}
|
}
|
||||||
@@ -79,13 +60,12 @@ func (client *Client) FetchPosts(albumID string) (Album, error) {
|
|||||||
return cacheData.(Album), nil
|
return cacheData.(Album), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := utils.GetJSONNew("https://api.imgur.com/post/v1/posts/" + albumID + "?client_id=" + client.ClientID + "&include=media%2Caccount%2Ctags")
|
data, err := utils.GetJSON("https://api.imgur.com/post/v1/posts/" + albumID + "?client_id=" + client.ClientID + "&include=media%2Caccount%2Ctags")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Album{}, err
|
return Album{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var album Album
|
album, err := parseAlbum(data)
|
||||||
err = json.Unmarshal(data, &album)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Album{}, err
|
return Album{}, err
|
||||||
}
|
}
|
||||||
@@ -100,13 +80,12 @@ func (client *Client) FetchMedia(mediaID string) (Album, error) {
|
|||||||
return cacheData.(Album), nil
|
return cacheData.(Album), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := utils.GetJSONNew("https://api.imgur.com/post/v1/media/" + mediaID + "?client_id=" + client.ClientID + "&include=media%2Caccount")
|
data, err := utils.GetJSON("https://api.imgur.com/post/v1/media/" + mediaID + "?client_id=" + client.ClientID + "&include=media%2Caccount")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Album{}, err
|
return Album{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var album Album
|
album, err := parseAlbum(data)
|
||||||
err = json.Unmarshal(data, &album)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Album{}, err
|
return Album{}, err
|
||||||
}
|
}
|
||||||
@@ -114,3 +93,71 @@ func (client *Client) FetchMedia(mediaID string) (Album, error) {
|
|||||||
client.Cache.Set(mediaID+"-media", album, 1*time.Hour)
|
client.Cache.Set(mediaID+"-media", album, 1*time.Hour)
|
||||||
return album, nil
|
return album, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseAlbum(data gjson.Result) (Album, error) {
|
||||||
|
media := make([]Media, 0)
|
||||||
|
data.Get("media").ForEach(
|
||||||
|
func(key gjson.Result, value gjson.Result) bool {
|
||||||
|
url := value.Get("url").String()
|
||||||
|
url = strings.ReplaceAll(url, "https://i.imgur.com", "")
|
||||||
|
|
||||||
|
description := value.Get("metadata.description").String()
|
||||||
|
description = strings.ReplaceAll(description, "\n", "<br>")
|
||||||
|
description = bluemonday.UGCPolicy().Sanitize(description)
|
||||||
|
|
||||||
|
media = append(media, Media{
|
||||||
|
Id: value.Get("id").String(),
|
||||||
|
Name: value.Get("name").String(),
|
||||||
|
MimeType: value.Get("mime_type").String(),
|
||||||
|
Type: value.Get("type").String(),
|
||||||
|
Title: value.Get("metadata.title").String(),
|
||||||
|
Description: description,
|
||||||
|
Url: url,
|
||||||
|
})
|
||||||
|
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
tags := make([]Tag, 0)
|
||||||
|
data.Get("tags").ForEach(
|
||||||
|
func(key gjson.Result, value gjson.Result) bool {
|
||||||
|
tags = append(tags, Tag{
|
||||||
|
Tag: value.Get("tag").String(),
|
||||||
|
Display: value.Get("display").String(),
|
||||||
|
Background: "/" + value.Get("background_id").String() + ".webp",
|
||||||
|
BackgroundId: value.Get("background_id").String(),
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
createdAt, err := utils.FormatDate(data.Get("created_at").String())
|
||||||
|
if err != nil {
|
||||||
|
return Album{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
album := Album{
|
||||||
|
Id: data.Get("id").String(),
|
||||||
|
Title: data.Get("title").String(),
|
||||||
|
SharedWithCommunity: data.Get("shared_with_community").Bool(),
|
||||||
|
Views: data.Get("view_count").Int(),
|
||||||
|
Upvotes: data.Get("upvote_count").Int(),
|
||||||
|
Downvotes: data.Get("downvote_count").Int(),
|
||||||
|
Comments: data.Get("comment_count").Int(),
|
||||||
|
CreatedAt: createdAt,
|
||||||
|
Media: media,
|
||||||
|
Tags: tags,
|
||||||
|
}
|
||||||
|
|
||||||
|
account := data.Get("account")
|
||||||
|
if account.Raw != "" {
|
||||||
|
album.User = User{
|
||||||
|
Id: account.Get("id").Int(),
|
||||||
|
Username: account.Get("username").String(),
|
||||||
|
Avatar: strings.ReplaceAll(account.Get("avatar_url").String(), "https://i.imgur.com", ""),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return album, nil
|
||||||
|
}
|
||||||
|
|||||||
155
api/comments.go
155
api/comments.go
@@ -1,62 +1,145 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"codeberg.org/rimgo/rimgo/utils"
|
"codeberg.org/rimgo/rimgo/utils"
|
||||||
|
"github.com/dustin/go-humanize"
|
||||||
|
"github.com/microcosm-cc/bluemonday"
|
||||||
"github.com/patrickmn/go-cache"
|
"github.com/patrickmn/go-cache"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
|
"gitlab.com/golang-commonmark/linkify"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Comment struct {
|
type Comment struct {
|
||||||
ID int `json:"id"`
|
Comments []Comment
|
||||||
Comment string `json:"comment"`
|
User User
|
||||||
UpvoteCount int `json:"upvote_count"`
|
Post Submission
|
||||||
DownvoteCount int `json:"downvote_count"`
|
Id string
|
||||||
PointCount int `json:"point_count"`
|
Comment string
|
||||||
CreatedAt time.Time `json:"created_at"`
|
Upvotes int64
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
Downvotes int64
|
||||||
DeletedAt time.Time `json:"deleted_at"`
|
Platform string
|
||||||
Comments array[Comment] `json:"comments"`
|
CreatedAt string
|
||||||
AccountID int `json:"account_id"`
|
RelTime string
|
||||||
Account _ApiUser `json:"account"`
|
UpdatedAt string
|
||||||
Post Submission `json:"post"`
|
DeletedAt string
|
||||||
}
|
|
||||||
|
|
||||||
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.(array[Comment]), nil
|
return cacheData.([]Comment), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := utils.GetJSONNew("https://api.imgur.com/comment/v1/comments?client_id=" + client.ClientID + "&filter[post]=eq:" + galleryID + "&include=account,adconfig&per_page=30&sort=best")
|
data, err := utils.GetJSON("https://api.imgur.com/comment/v1/comments?client_id=" + client.ClientID + "&filter[post]=eq:" + galleryID + "&include=account,adconfig&per_page=30&sort=best")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []Comment{}, nil
|
return []Comment{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var parsed commentApiResponse
|
wg := sync.WaitGroup{}
|
||||||
err = json.Unmarshal(data, &parsed)
|
comments := make([]Comment, 0)
|
||||||
if err != nil {
|
data.Get("data").ForEach(
|
||||||
return []Comment{}, err
|
func(key, value gjson.Result) bool {
|
||||||
|
wg.Add(1)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
var imgurRe = regexp.MustCompile(`https?://imgur\.com/(gallery|a)?/(.*)`)
|
||||||
|
var imgurRe2 = regexp.MustCompile(`https?://imgur\.com/(.*)`)
|
||||||
|
var imgRe = regexp.MustCompile(`https?://i\.imgur\.com/(.*)\.(png|gif|jpe?g|webp)`)
|
||||||
|
var vidRe = regexp.MustCompile(`https?://i\.imgur\.com/(.*)\.(mp4|webm)`)
|
||||||
|
var vidFormatRe = regexp.MustCompile(`\.(mp4|webm)`)
|
||||||
|
var iImgurRe = regexp.MustCompile(`https?://i\.imgur\.com`)
|
||||||
|
|
||||||
|
func parseComment(data gjson.Result) Comment {
|
||||||
|
createdTime, _ := time.Parse("2006-01-02T15:04:05Z", data.Get("created_at").String())
|
||||||
|
createdAt := createdTime.Format("January 2, 2006 3:04 PM")
|
||||||
|
updatedAt, _ := utils.FormatDate(data.Get("updated_at").String())
|
||||||
|
deletedAt, _ := utils.FormatDate(data.Get("deleted_at").String())
|
||||||
|
|
||||||
|
userAvatar := strings.ReplaceAll(data.Get("account.avatar").String(), "https://i.imgur.com", "")
|
||||||
|
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
comments := make([]Comment, 0)
|
||||||
|
data.Get("comments").ForEach(
|
||||||
|
func(key, value gjson.Result) bool {
|
||||||
|
wg.Add(1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
comments = append(comments, parseComment(value))
|
||||||
|
}()
|
||||||
|
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
)
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
comment := data.Get("comment").String()
|
||||||
|
|
||||||
|
comment = strings.ReplaceAll(comment, "\n", "<br>")
|
||||||
|
|
||||||
|
for _, match := range imgRe.FindAllString(comment, -1) {
|
||||||
|
img := iImgurRe.ReplaceAllString(match, "")
|
||||||
|
img = `<img src="` + img + `" class="comment__media" loading="lazy"/>`
|
||||||
|
comment = strings.Replace(comment, match, img, 1)
|
||||||
}
|
}
|
||||||
|
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")
|
||||||
|
|
||||||
client.Cache.Set(galleryID+"-comments", parsed.Data, cache.DefaultExpiration)
|
p := bluemonday.UGCPolicy()
|
||||||
return parsed.Data, nil
|
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)
|
||||||
|
|
||||||
// temporary
|
return Comment{
|
||||||
func (post *Submission) UnmarshalJSON(data []byte) error {
|
Comments: comments,
|
||||||
*post = parseSubmission(gjson.ParseBytes(data))
|
User: User{
|
||||||
return nil
|
Id: data.Get("account.id").Int(),
|
||||||
}
|
Username: data.Get("account.username").String(),
|
||||||
|
Avatar: userAvatar,
|
||||||
type commentApiResponse struct {
|
},
|
||||||
Data array[Comment] `json:"data"`
|
Post: parseSubmission(data.Get("post")),
|
||||||
|
Id: data.Get("id").String(),
|
||||||
|
Comment: comment,
|
||||||
|
Upvotes: data.Get("upvote_count").Int(),
|
||||||
|
Downvotes: data.Get("downvote_count").Int(),
|
||||||
|
Platform: data.Get("platform").String(),
|
||||||
|
CreatedAt: createdAt,
|
||||||
|
RelTime: humanize.Time(createdTime),
|
||||||
|
UpdatedAt: updatedAt,
|
||||||
|
DeletedAt: deletedAt,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
46
api/json.go
46
api/json.go
@@ -1,46 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
type array[T any] []T
|
|
||||||
|
|
||||||
func (arr *array[T]) UnmarshalJSON(data []byte) error {
|
|
||||||
var rawArr []json.RawMessage
|
|
||||||
err := json.Unmarshal(data, &rawArr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*arr = make(array[T], len(rawArr))
|
|
||||||
errs := 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)
|
|
||||||
}
|
|
||||||
errs = append(errs, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i, value := range rawArr {
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer handlePanic()
|
|
||||||
defer wg.Done()
|
|
||||||
err := json.Unmarshal(value, &(*arr)[i])
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
if len(errs) != 0 {
|
|
||||||
return errors.Join(errs...)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
67
api/tag.go
67
api/tag.go
@@ -1,26 +1,23 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/patrickmn/go-cache"
|
"github.com/patrickmn/go-cache"
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TagMeta struct {
|
|
||||||
Tag string `json:"tag"`
|
|
||||||
Display string `json:"display"`
|
|
||||||
Sort string `json:"sort"`
|
|
||||||
PostCount int64 `json:"post_count"`
|
|
||||||
BackgroundId string `json:"background_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Tag struct {
|
type Tag struct {
|
||||||
TagMeta
|
Tag string
|
||||||
Background string `json:"-"`
|
Display string
|
||||||
Posts []Submission `json:"posts"`
|
Sort string
|
||||||
|
PostCount int64
|
||||||
|
Posts []Submission
|
||||||
|
Background string
|
||||||
|
BackgroundId string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client *Client) FetchTag(tag string, sort string, page string) (Tag, error) {
|
func (client *Client) FetchTag(tag string, sort string, page string) (Tag, error) {
|
||||||
@@ -57,19 +54,57 @@ func (client *Client) FetchTag(tag string, sort string, page string) (Tag, error
|
|||||||
}
|
}
|
||||||
|
|
||||||
req.URL.RawQuery = q.Encode()
|
req.URL.RawQuery = q.Encode()
|
||||||
|
|
||||||
res, err := http.DefaultClient.Do(req)
|
res, err := http.DefaultClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Tag{}, err
|
return Tag{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
body, err := io.ReadAll(res.Body)
|
body, err := io.ReadAll(res.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Tag{}, err
|
return Tag{}, err
|
||||||
}
|
}
|
||||||
var tagData Tag
|
|
||||||
err = json.Unmarshal(body, &tagData)
|
data := gjson.Parse(string(body))
|
||||||
if err != nil {
|
|
||||||
return Tag{}, err
|
posts := make([]Submission, 0)
|
||||||
|
data.Get("posts").ForEach(
|
||||||
|
func(key, value gjson.Result) bool {
|
||||||
|
url, _ := url.Parse(strings.ReplaceAll(value.Get("url").String(), "https://imgur.com", ""))
|
||||||
|
q := url.Query()
|
||||||
|
q.Add("tag", tag+"."+sort+"."+page+"."+key.String())
|
||||||
|
url.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
posts = append(posts, Submission{
|
||||||
|
Id: value.Get("id").String(),
|
||||||
|
Title: value.Get("title").String(),
|
||||||
|
Link: url.String(),
|
||||||
|
Cover: Media{
|
||||||
|
Id: value.Get("cover_id").String(),
|
||||||
|
Type: value.Get("cover.type").String(),
|
||||||
|
Url: strings.ReplaceAll(value.Get("cover.url").String(), "https://i.imgur.com", ""),
|
||||||
|
},
|
||||||
|
Points: value.Get("point_count").Int(),
|
||||||
|
Upvotes: value.Get("upvote_count").Int(),
|
||||||
|
Downvotes: value.Get("downvote_count").Int(),
|
||||||
|
Comments: value.Get("comment_count").Int(),
|
||||||
|
Views: value.Get("view_count").Int(),
|
||||||
|
IsAlbum: value.Get("is_album").Bool(),
|
||||||
|
})
|
||||||
|
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
tagData := Tag{
|
||||||
|
Tag: tag,
|
||||||
|
Display: data.Get("display").String(),
|
||||||
|
Sort: sort,
|
||||||
|
PostCount: data.Get("post_count").Int(),
|
||||||
|
Posts: posts,
|
||||||
|
Background: "/" + data.Get("background_id").String() + ".webp",
|
||||||
}
|
}
|
||||||
|
|
||||||
client.Cache.Set(tag+sort+page+"-tag", tagData, 4*cache.DefaultExpiration)
|
client.Cache.Set(tag+sort+page+"-tag", tagData, 4*cache.DefaultExpiration)
|
||||||
return tagData, nil
|
return tagData, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ func (client *Client) FetchTrending(section, sort, page string) ([]Submission, e
|
|||||||
case "best":
|
case "best":
|
||||||
q.Add("filter[window]", "all")
|
q.Add("filter[window]", "all")
|
||||||
q.Add("sort", "-top")
|
q.Add("sort", "-top")
|
||||||
|
case "random":
|
||||||
|
q.Add("sort", "random")
|
||||||
case "popular":
|
case "popular":
|
||||||
fallthrough
|
fallthrough
|
||||||
default:
|
default:
|
||||||
@@ -51,6 +53,8 @@ func (client *Client) FetchTrending(section, sort, page string) ([]Submission, e
|
|||||||
case "top":
|
case "top":
|
||||||
q.Add("filter[section]", "eq:top")
|
q.Add("filter[section]", "eq:top")
|
||||||
q.Add("filter[window]", "day")
|
q.Add("filter[window]", "day")
|
||||||
|
case "random":
|
||||||
|
q.Add("filter[section]", "eq:random")
|
||||||
default:
|
default:
|
||||||
q.Add("filter[section]", "eq:hot")
|
q.Add("filter[section]", "eq:hot")
|
||||||
section = "hot"
|
section = "hot"
|
||||||
|
|||||||
23
api/user.go
23
api/user.go
@@ -1,7 +1,6 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -155,7 +154,7 @@ func (client *Client) FetchUserFavorites(username string, sort string, page stri
|
|||||||
func (client *Client) FetchUserComments(username string) ([]Comment, error) {
|
func (client *Client) FetchUserComments(username string) ([]Comment, error) {
|
||||||
cacheData, found := client.Cache.Get(username + "-usercomments")
|
cacheData, found := client.Cache.Get(username + "-usercomments")
|
||||||
if found {
|
if found {
|
||||||
return cacheData.(array[Comment]), nil
|
return cacheData.([]Comment), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", "https://api.imgur.com/comment/v1/comments", nil)
|
req, err := http.NewRequest("GET", "https://api.imgur.com/comment/v1/comments", nil)
|
||||||
@@ -177,19 +176,23 @@ func (client *Client) FetchUserComments(username string) ([]Comment, error) {
|
|||||||
return []Comment{}, err
|
return []Comment{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := io.ReadAll(res.Body)
|
body, err := io.ReadAll(res.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []Comment{}, err
|
return []Comment{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var parsed commentApiResponse
|
data := gjson.Parse(string(body))
|
||||||
err = json.Unmarshal(data, &parsed)
|
|
||||||
if err != nil {
|
|
||||||
return []Comment{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
client.Cache.Set(username+"-usercomments", parsed.Data, cache.DefaultExpiration)
|
comments := make([]Comment, 0)
|
||||||
return parsed.Data, nil
|
data.Get("data").ForEach(
|
||||||
|
func(key, value gjson.Result) bool {
|
||||||
|
comments = append(comments, parseComment(value))
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
client.Cache.Set(username+"-usercomments", comments, cache.DefaultExpiration)
|
||||||
|
return comments, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseSubmission(value gjson.Result) Submission {
|
func parseSubmission(value gjson.Result) Submission {
|
||||||
|
|||||||
24
main.go
24
main.go
@@ -23,13 +23,16 @@ 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 {
|
||||||
pages.RenderError(w, r, 500, fmt.Sprint(v))
|
utils.RenderError(w, r, 500, fmt.Sprint(v))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
err := h(w, r)
|
err := h(w, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if render.IsRendererError(err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
pages.RenderError(w, r, 500, err.Error())
|
utils.RenderError(w, r, 500, err.Error())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -61,14 +64,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) {
|
||||||
pages.RenderError(w, r, 429)
|
utils.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) {
|
||||||
pages.RenderError(w, r, 404)
|
utils.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")
|
||||||
@@ -87,7 +90,7 @@ func main() {
|
|||||||
app.Handle("GET /t/{tag}", wrapHandler(func(w http.ResponseWriter, r *http.Request) error {
|
app.Handle("GET /t/{tag}", wrapHandler(func(w http.ResponseWriter, r *http.Request) error {
|
||||||
name, ext := utils.SplitNameExt(r.PathValue("tag"))
|
name, ext := utils.SplitNameExt(r.PathValue("tag"))
|
||||||
if ext != "" {
|
if ext != "" {
|
||||||
r.SetPathValue("tag", name[0:len(name)-1])
|
r.SetPathValue("tag", name)
|
||||||
r.SetPathValue("type", ext)
|
r.SetPathValue("type", ext)
|
||||||
return pages.HandleTagRSS(w, r)
|
return pages.HandleTagRSS(w, r)
|
||||||
}
|
}
|
||||||
@@ -98,7 +101,7 @@ func main() {
|
|||||||
app.Handle("GET /user/{userID}", wrapHandler(func(w http.ResponseWriter, r *http.Request) error {
|
app.Handle("GET /user/{userID}", wrapHandler(func(w http.ResponseWriter, r *http.Request) error {
|
||||||
name, ext := utils.SplitNameExt(r.PathValue("userID"))
|
name, ext := utils.SplitNameExt(r.PathValue("userID"))
|
||||||
if ext != "" {
|
if ext != "" {
|
||||||
r.SetPathValue("userID", name[0:len(name)-1])
|
r.SetPathValue("userID", name)
|
||||||
r.SetPathValue("type", ext)
|
r.SetPathValue("type", ext)
|
||||||
return pages.HandleUserRSS(w, r)
|
return pages.HandleUserRSS(w, r)
|
||||||
}
|
}
|
||||||
@@ -129,7 +132,14 @@ func main() {
|
|||||||
r.SetPathValue("postID", component)
|
r.SetPathValue("postID", component)
|
||||||
return pages.HandleGifv(w, r)
|
return pages.HandleGifv(w, r)
|
||||||
case strings.Contains(component, "."):
|
case strings.Contains(component, "."):
|
||||||
return pages.HandleMedia(w, r)
|
baseName, extension := utils.SplitNameExt(r.PathValue("component"))
|
||||||
|
r.SetPathValue("baseName", baseName)
|
||||||
|
r.SetPathValue("extension", extension)
|
||||||
|
switch extension {
|
||||||
|
case ".png", ".gif", ".jpg", ".jpeg", ".webp", ".mp4", ".webm":
|
||||||
|
return pages.HandleMedia(w, r)
|
||||||
|
}
|
||||||
|
fallthrough
|
||||||
default:
|
default:
|
||||||
r.SetPathValue("postID", component)
|
r.SetPathValue("postID", component)
|
||||||
return pages.HandlePost(w, r)
|
return pages.HandlePost(w, r)
|
||||||
|
|||||||
@@ -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 RenderError(w, r, 429)
|
return utils.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 RenderError(w, r, 404)
|
return utils.RenderError(w, r, 404)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -12,12 +12,11 @@ import (
|
|||||||
func HandleMedia(w http.ResponseWriter, r *http.Request) error {
|
func HandleMedia(w http.ResponseWriter, r *http.Request) error {
|
||||||
w.Header().Set("Cache-Control", "public,max-age=31557600")
|
w.Header().Set("Cache-Control", "public,max-age=31557600")
|
||||||
w.Header().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'")
|
||||||
splitName := strings.SplitN(r.PathValue("component"), ".", 2)
|
baseName, extension := r.PathValue("baseName"), r.PathValue("extension")
|
||||||
baseName, extension := splitName[0], splitName[1]
|
|
||||||
if strings.HasPrefix(r.URL.Path, "/stack") {
|
if strings.HasPrefix(r.URL.Path, "/stack") {
|
||||||
return handleMedia(w, r, "https://i.stack.imgur.com/"+strings.ReplaceAll(baseName, "stack/", "")+"."+extension)
|
return handleMedia(w, r, "https://i.stack.imgur.com/"+baseName+extension)
|
||||||
} else {
|
} else {
|
||||||
return handleMedia(w, r, "https://i.imgur.com/"+baseName+"."+extension)
|
return handleMedia(w, r, "https://i.imgur.com/"+baseName+extension)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,9 +72,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 RenderError(w, r, 404)
|
return utils.RenderError(w, r, 404)
|
||||||
} else if res.StatusCode == 429 {
|
} else if res.StatusCode == 429 {
|
||||||
return RenderError(w, r, 429)
|
return utils.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 RenderError(w, r, 429)
|
return utils.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 RenderError(w, r, 404)
|
return utils.RenderError(w, r, 404)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
10
pages/rss.go
10
pages/rss.go
@@ -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 RenderError(w, r, 429)
|
return utils.RenderError(w, r, 429)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -114,21 +114,21 @@ func handleFeed(w http.ResponseWriter, r *http.Request, instance string, feed *f
|
|||||||
feed.Items = append(feed.Items, item)
|
feed.Items = append(feed.Items, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", mime.TypeByExtension("."+r.PathValue("type")))
|
w.Header().Set("Content-Type", mime.TypeByExtension(r.PathValue("type")))
|
||||||
switch r.PathValue("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
|
||||||
}
|
}
|
||||||
w.Write([]byte(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
|
||||||
}
|
}
|
||||||
w.Write([]byte(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
|
||||||
|
|||||||
@@ -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 RenderError(w, r, 429)
|
return utils.RenderError(w, r, 429)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if tag.Display == "" {
|
if tag.Display == "" {
|
||||||
return RenderError(w, r, 404)
|
return utils.RenderError(w, r, 404)
|
||||||
}
|
}
|
||||||
|
|
||||||
return render.Render(w, "tag", map[string]any{
|
return render.Render(w, "tag", map[string]any{
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ func HandleTrending(w http.ResponseWriter, r *http.Request) error {
|
|||||||
|
|
||||||
section := r.URL.Query().Get("section")
|
section := r.URL.Query().Get("section")
|
||||||
switch section {
|
switch section {
|
||||||
case "hot", "new", "top":
|
case "hot", "new", "top", "random":
|
||||||
default:
|
default:
|
||||||
section = "hot"
|
section = "hot"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 RenderError(w, r, 429)
|
return utils.RenderError(w, r, 429)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if user.Username == "" {
|
if user.Username == "" {
|
||||||
return RenderError(w, r, 404)
|
return utils.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 RenderError(w, r, 429)
|
return utils.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 RenderError(w, r, 429)
|
return utils.RenderError(w, r, 429)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if user.Username == "" {
|
if user.Username == "" {
|
||||||
return RenderError(w, r, 404)
|
return utils.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 RenderError(w, r, 429)
|
return utils.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 RenderError(w, r, 429)
|
return utils.RenderError(w, r, 429)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if user.Username == "" {
|
if user.Username == "" {
|
||||||
return RenderError(w, r, 404)
|
return utils.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 RenderError(w, r, 429)
|
return utils.RenderError(w, r, 429)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -1,21 +1,10 @@
|
|||||||
package render
|
package render
|
||||||
|
|
||||||
import (
|
import "github.com/mailgun/raymond/v2"
|
||||||
"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,
|
|
||||||
"sanitizeDescription": sanitizeDescription,
|
|
||||||
"sanitizeComment": sanitizeComment,
|
|
||||||
}
|
}
|
||||||
raymond.RegisterHelpers(funcmap)
|
raymond.RegisterHelpers(funcmap)
|
||||||
}
|
}
|
||||||
@@ -26,19 +15,3 @@ 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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
package render
|
package render
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
@@ -63,14 +64,46 @@ func Initialize(views fs.FS) {
|
|||||||
func (r *renderer) Render(out io.Writer, name string, bind map[string]any) error {
|
func (r *renderer) Render(out io.Writer, name string, bind map[string]any) error {
|
||||||
tmpl := r.templates[name]
|
tmpl := r.templates[name]
|
||||||
if tmpl == nil {
|
if tmpl == nil {
|
||||||
return fmt.Errorf("render: template %s does not exist", name)
|
return re(fmt.Errorf("render: template %s does not exist", name))
|
||||||
}
|
}
|
||||||
parsed, err := tmpl.Exec(bind)
|
parsed, err := tmpl.Exec(bind)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("render: %w", err)
|
return re(fmt.Errorf("render: %w", err))
|
||||||
}
|
}
|
||||||
if _, err = out.Write([]byte(parsed)); err != nil {
|
if _, err = out.Write([]byte(parsed)); err != nil {
|
||||||
return fmt.Errorf("render: %w", err)
|
return re(fmt.Errorf("render: %w", err))
|
||||||
}
|
}
|
||||||
return err
|
return re(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var reSentinel = &struct{}{}
|
||||||
|
|
||||||
|
type RendererError struct {
|
||||||
|
u *struct{}
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func re(err error) error {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return RendererError{reSentinel, err}
|
||||||
|
}
|
||||||
|
func (re RendererError) Error() string {
|
||||||
|
return re.Error()
|
||||||
|
}
|
||||||
|
func (re RendererError) Unwrap() error {
|
||||||
|
e1, ok := re.err.(interface{ Unwrap() error })
|
||||||
|
if ok {
|
||||||
|
return e1.Unwrap()
|
||||||
|
}
|
||||||
|
e2, ok := re.err.(interface{ Unwrap() []error })
|
||||||
|
if ok {
|
||||||
|
return errors.Join(e2.Unwrap()...)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func IsRendererError(err error) bool {
|
||||||
|
re, ok := err.(RendererError)
|
||||||
|
return ok && re.u == reSentinel
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
package render
|
|
||||||
|
|
||||||
import (
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/microcosm-cc/bluemonday"
|
|
||||||
"gitlab.com/golang-commonmark/linkify"
|
|
||||||
)
|
|
||||||
|
|
||||||
var imgurRe = regexp.MustCompile(`https?://imgur\.com/(gallery|a)?/(.*)`)
|
|
||||||
var imgurRe2 = regexp.MustCompile(`https?://imgur\.com/(.*)`)
|
|
||||||
var imgRe = regexp.MustCompile(`https?://i\.imgur\.com/(.*)\.(png|gif|jpe?g|webp)`)
|
|
||||||
var vidRe = regexp.MustCompile(`https?://i\.imgur\.com/(.*)\.(mp4|webm)`)
|
|
||||||
var vidFormatRe = regexp.MustCompile(`\.(mp4|webm)`)
|
|
||||||
var iImgurRe = regexp.MustCompile(`https?://i\.imgur\.com`)
|
|
||||||
|
|
||||||
func sanitizeDescription(src string) string {
|
|
||||||
src = strings.ReplaceAll(src, "\n", "<br>")
|
|
||||||
return bluemonday.UGCPolicy().Sanitize(src)
|
|
||||||
}
|
|
||||||
func sanitizeComment(src string) string {
|
|
||||||
src = strings.ReplaceAll(src, "\n", "<br>")
|
|
||||||
|
|
||||||
for _, match := range imgRe.FindAllString(src, -1) {
|
|
||||||
img := iImgurRe.ReplaceAllString(match, "")
|
|
||||||
img = `<img src="` + img + `" class="comment__media" loading="lazy"/>`
|
|
||||||
src = strings.Replace(src, match, img, 1)
|
|
||||||
}
|
|
||||||
for _, match := range vidRe.FindAllString(src, -1) {
|
|
||||||
vid := iImgurRe.ReplaceAllString(match, "")
|
|
||||||
vid = `<video class="comment__media" controls loop preload="none" poster="` + vidFormatRe.ReplaceAllString(vid, ".webp") + `"><source type="` + strings.Split(vid, ".")[1] + `" src="` + vid + `" /></video>`
|
|
||||||
src = strings.Replace(src, match, vid, 1)
|
|
||||||
}
|
|
||||||
for _, l := range linkify.Links(src) {
|
|
||||||
origLink := (src)[l.Start:l.End]
|
|
||||||
link := `<a href="` + origLink + `">` + origLink + `</a>`
|
|
||||||
src = strings.Replace(src, origLink, link, 1)
|
|
||||||
}
|
|
||||||
src = imgurRe.ReplaceAllString(src, "/$1/$2")
|
|
||||||
src = imgurRe2.ReplaceAllString(src, "/$1")
|
|
||||||
|
|
||||||
p := bluemonday.UGCPolicy()
|
|
||||||
p.AllowImages()
|
|
||||||
p.AllowElements("video", "source")
|
|
||||||
p.AllowAttrs("src", "tvpe").OnElements("source")
|
|
||||||
p.AllowAttrs("controls", "loop", "preload", "poster").OnElements("video")
|
|
||||||
p.AllowAttrs("class", "loading").OnElements("img", "video")
|
|
||||||
p.RequireNoReferrerOnLinks(true)
|
|
||||||
p.RequireNoFollowOnLinks(true)
|
|
||||||
p.RequireCrossOriginAnonymous(true)
|
|
||||||
return p.Sanitize(src)
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package pages
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -8,7 +8,6 @@ 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) {
|
||||||
@@ -22,7 +21,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 !utils.Accepts(r, "text/html") && r.PathValue("extension") != "" {
|
if !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")
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -10,42 +9,6 @@ import (
|
|||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetJSONNew(url string) (json.RawMessage, error) {
|
|
||||||
req, err := http.NewRequest("GET", url, nil)
|
|
||||||
if err != nil {
|
|
||||||
return json.RawMessage{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
SetReqHeaders(req)
|
|
||||||
|
|
||||||
client := http.Client{}
|
|
||||||
res, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return json.RawMessage{}, err
|
|
||||||
}
|
|
||||||
rateLimitRemaining := res.Header.Get("X-RateLimit-UserRemaining")
|
|
||||||
if rateLimitRemaining != "" {
|
|
||||||
ratelimit, _ := strconv.Atoi(rateLimitRemaining)
|
|
||||||
if ratelimit <= 0 {
|
|
||||||
return json.RawMessage{}, fmt.Errorf("ratelimited by imgur")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := io.ReadAll(res.Body)
|
|
||||||
if err != nil {
|
|
||||||
return json.RawMessage{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch res.StatusCode {
|
|
||||||
case 200:
|
|
||||||
return body, nil
|
|
||||||
case 429:
|
|
||||||
return json.RawMessage{}, fmt.Errorf("ratelimited by imgur")
|
|
||||||
default:
|
|
||||||
return json.RawMessage{}, fmt.Errorf("received status %s, expected 200 OK.\n%s", res.Status, string(body))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetJSON(url string) (gjson.Result, error) {
|
func GetJSON(url string) (gjson.Result, error) {
|
||||||
req, err := http.NewRequest("GET", url, nil)
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
package utils
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/url"
|
|
||||||
)
|
|
||||||
|
|
||||||
func RewriteUrl(link string) (string, error) {
|
|
||||||
url, err := url.Parse(link)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
switch url.Host {
|
|
||||||
case "", "imgur.com", "www.imgur.com", "i.imgur.com":
|
|
||||||
return url.Path, nil
|
|
||||||
case "i.stack.imgur.com":
|
|
||||||
return "/stack" + url.Path, nil
|
|
||||||
}
|
|
||||||
return link, nil
|
|
||||||
}
|
|
||||||
@@ -1,16 +1,8 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
|
import "path/filepath"
|
||||||
|
|
||||||
func SplitNameExt(path string) (name, ext string) {
|
func SplitNameExt(path string) (name, ext string) {
|
||||||
name, ext = path, ""
|
ext = filepath.Ext(path)
|
||||||
for range 5 {
|
return path[:len(path)-len(ext)], ext
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
<div class="postMeta">
|
<div class="postMeta">
|
||||||
<div class="postDetails">
|
<div class="postDetails">
|
||||||
{{#if post.TItle}}
|
{{#if post.TItle}}
|
||||||
<a href="/{{post.ID}}">
|
<a href="/{{post.Id}}">
|
||||||
<h3>{{post.Title}}</h3>
|
<h3>{{post.Title}}</h3>
|
||||||
</a>
|
</a>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
@@ -35,10 +35,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-center">
|
<div class="flex flex-center">
|
||||||
<a href="/{{post.ID}}">
|
<a href="/{{post.Id}}">
|
||||||
<img src="/static/img/rimgo.svg" width="32px" height="32px" class="logo">
|
<img src="/static/img/rimgo.svg" width="32px" height="32px" class="logo">
|
||||||
</a>
|
</a>
|
||||||
<a href="/{{post.ID}}">
|
<a href="/{{post.Id}}">
|
||||||
rimgo
|
rimgo
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,10 +19,10 @@
|
|||||||
<div class="postMeta">
|
<div class="postMeta">
|
||||||
<a href="/{{id}}.mp4" download="{{id}}.mp4">download</a>
|
<a href="/{{id}}.mp4" download="{{id}}.mp4">download</a>
|
||||||
<div class="flex flex-center">
|
<div class="flex flex-center">
|
||||||
<a href="/{{post.ID}}">
|
<a href="/{{post.Id}}">
|
||||||
<img src="/static/img/rimgo.svg" width="32px" height="32px" class="logo">
|
<img src="/static/img/rimgo.svg" width="32px" height="32px" class="logo">
|
||||||
</a>
|
</a>
|
||||||
<a href="/{{post.ID}}">
|
<a href="/{{post.Id}}">
|
||||||
rimgo
|
rimgo
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,25 +1,25 @@
|
|||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
{{#noteq this.Account.Username "[deleted]"}}
|
{{#noteq this.User.Username "[deleted]"}}
|
||||||
<img src="{{rewriteUrl(this.Account.Avatar)}}" class="rounded-full" width="24" height="24" loading="lazy">
|
<img src="{{this.User.Avatar}}" class="rounded-full" width="24" height="24" loading="lazy">
|
||||||
<a href="/user/{{this.Account.Username}}">
|
<a href="/user/{{this.User.Username}}">
|
||||||
<p class="whitespace-nowrap text-ellipsis overflow-hidden"><b>{{this.Account.Username}}</b></p>
|
<p class="whitespace-nowrap text-ellipsis overflow-hidden"><b>{{this.User.Username}}</b></p>
|
||||||
</a>
|
</a>
|
||||||
{{/noteq}}
|
{{/noteq}}
|
||||||
{{#equal this.Account.Username "[deleted]"}}
|
{{#equal this.User.Username "[deleted]"}}
|
||||||
<p class="whitespace-nowrap text-ellipsis overflow-hidden"><b>[deleted]</b></p>
|
<p class="whitespace-nowrap text-ellipsis overflow-hidden"><b>[deleted]</b></p>
|
||||||
{{/equal}}
|
{{/equal}}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p>{{{sanitizeComment(this.Comment)}}}</p>
|
<p>{{{this.Comment}}}</p>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<span title="{{this.CreatedAt}}">{{relTime(this.CreatedAt)}}</span>
|
<span title="{{this.CreatedAt}}">{{this.RelTime}}</span>
|
||||||
{{#ifNonZeroTime this.DeletedAt}}
|
{{#if this.DeletedAt}}
|
||||||
<span class="text-md">(deleted {{this.DeletedAt}})</span>
|
<span class="text-md">(deleted {{this.DeletedAt}})</span>
|
||||||
{{/ifNonZeroTime}}
|
{{/if}}
|
||||||
|
|
|
|
||||||
<img class="invert icon" src="/static/icons/PhArrowFatUp.svg" alt="Likes" width="24px" height="24px"> {{this.UpvoteCount}}
|
<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.DownvoteCount}}
|
<img class="invert icon" src="/static/icons/PhArrowFatDown.svg" alt="Dislikes" width="24px" height="24px"> {{this.Downvotes}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{#if this.Comments}}
|
{{#if this.Comments}}
|
||||||
|
|||||||
@@ -3,18 +3,18 @@
|
|||||||
<img class="object-cover block w-full h-[300px] sm:w-[120px] sm:h-[140px] rounded-lg rounded-b-none sm:rounded-b-lg" src="{{this.Post.Cover.Url}}" alt="">
|
<img class="object-cover block w-full h-[300px] sm:w-[120px] sm:h-[140px] rounded-lg rounded-b-none sm:rounded-b-lg" src="{{this.Post.Cover.Url}}" alt="">
|
||||||
<div class="flex flex-col gap-2 bg-slate-600 p-4 rounded-lg rounded-t-none sm:rounded-t-lg w-full">
|
<div class="flex flex-col gap-2 bg-slate-600 p-4 rounded-lg rounded-t-none sm:rounded-t-lg w-full">
|
||||||
<div class="flex flex-col h-full">
|
<div class="flex flex-col h-full">
|
||||||
<p class="md-container">{{{sanitizeComment(this.Comment)}}}</p>
|
<p class="md-container">{{{this.Comment}}}</p>
|
||||||
<div class="grow"></div>
|
<div class="grow"></div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<span title="{{this.CreatedAt}}">{{relTime(this.CreatedAt)}}</span>
|
<span title="{{this.CreatedAt}}">{{this.RelTime}}</span>
|
||||||
{{#ifNonZeroTime this.DeletedAt}}
|
{{#if this.DeletedAt}}
|
||||||
<span class="text-md">(deleted {{this.DeletedAt}})</span>
|
<span class="text-md">(deleted {{this.DeletedAt}})</span>
|
||||||
{{/ifNonZeroTime}}
|
{{/if}}
|
||||||
|
|
|
|
||||||
<img class="invert icon" src="/static/icons/PhArrowFatUp.svg" alt="Likes" width="24px" height="24px">
|
<img class="invert icon" src="/static/icons/PhArrowFatUp.svg" alt="Likes" width="24px" height="24px">
|
||||||
{{this.UpvoteCount}}
|
{{this.Upvotes}}
|
||||||
<img class="invert icon" src="/static/icons/PhArrowFatDown.svg" alt="Dislikes" width="24px" height="24px">
|
<img class="invert icon" src="/static/icons/PhArrowFatDown.svg" alt="Dislikes" width="24px" height="24px">
|
||||||
{{this.DownvoteCount}}
|
{{this.Downvotes}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,25 +2,25 @@
|
|||||||
<div class="bg-slate-600 rounded-lg">
|
<div class="bg-slate-600 rounded-lg">
|
||||||
{{#equal Cover.Type "video"}}
|
{{#equal Cover.Type "video"}}
|
||||||
<video controls loop poster="/{{Cover.Id}}.webp" preload="none" width="100%" height="100%">
|
<video controls loop poster="/{{Cover.Id}}.webp" preload="none" width="100%" height="100%">
|
||||||
<source src="{{rewriteUrl(Cover.Url)}}" type="video/mp4" />
|
<source src="{{Cover.Url}}" type="video/mp4" />
|
||||||
</video>
|
</video>
|
||||||
{{/equal}}
|
{{/equal}}
|
||||||
{{#equal Cover.Type "image"}}
|
{{#equal Cover.Type "image"}}
|
||||||
<img src="{{rewriteUrl(Cover.Url)}}" loading="lazy" width="100%" height="100%">
|
<img src="{{Cover.Url}}" loading="lazy" width="100%" height="100%">
|
||||||
{{/equal}}
|
{{/equal}}
|
||||||
<p class="m-2 text-ellipsis whitespace-nowrap overflow-hidden">{{Title}}</p>
|
<p class="m-2 text-ellipsis whitespace-nowrap overflow-hidden">{{Title}}</p>
|
||||||
<div class="flex gap-2 p-2">
|
<div class="flex gap-2 p-2">
|
||||||
<div class="flex gap-1">
|
<div class="flex gap-1">
|
||||||
<img class="invert icon" src="/static/icons/PhArrowFatUp.svg" alt="Points" width="18px" height="18px">
|
<img class="invert icon" src="/static/icons/PhArrowFatUp.svg" alt="Points" width="18px" height="18px">
|
||||||
{{PointCount}}
|
{{Points}}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-1">
|
<div class="flex gap-1">
|
||||||
<img class="invert icon" src="/static/icons/PhChat.svg" alt="Comments" width="18px" height="18px">
|
<img class="invert icon" src="/static/icons/PhChat.svg" alt="Comments" width="18px" height="18px">
|
||||||
{{CommentCount}}
|
{{Comments}}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-1">
|
<div class="flex gap-1">
|
||||||
<img class="invert icon" src="/static/icons/PhEye.svg" alt="Views" width="18px" height="18px">
|
<img class="invert icon" src="/static/icons/PhEye.svg" alt="Views" width="18px" height="18px">
|
||||||
{{ViewCount}}
|
{{Views}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
<div class="flex flex-col gap-2 md:flex-row md:gap-4 md:items-center">
|
<div class="flex flex-col gap-2 md:flex-row md:gap-4 md:items-center">
|
||||||
{{#if post.User.Username}}
|
{{#if post.User.Username}}
|
||||||
<a href="/user/{{post.User.Username}}" class="flex gap-2 items-center">
|
<a href="/user/{{post.User.Username}}" class="flex gap-2 items-center">
|
||||||
<img src="{{rewriteUrl(post.User.Avatar)}}" class="rounded-full" width="36" height="36" />
|
<img src="{{post.User.Avatar}}" class="rounded-full" width="36" height="36" />
|
||||||
<p>
|
<p>
|
||||||
<b>{{post.User.Username}}</b>
|
<b>{{post.User.Username}}</b>
|
||||||
</p>
|
</p>
|
||||||
@@ -38,16 +38,16 @@
|
|||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
<div class="flex flex-center gap-2">
|
<div class="flex flex-center gap-2">
|
||||||
<img class="icon invert" src="/static/icons/PhEye.svg" alt="Views" width="24px" height="24px">
|
<img class="icon invert" src="/static/icons/PhEye.svg" alt="Views" width="24px" height="24px">
|
||||||
<p>{{post.ViewCount}}</p>
|
<p>{{post.Views}}</p>
|
||||||
</div>
|
</div>
|
||||||
{{#if post.SharedWithCommunity}}
|
{{#if post.SharedWithCommunity}}
|
||||||
<div class="flex flex-center gap-2">
|
<div class="flex flex-center gap-2">
|
||||||
<img class="icon invert" src="/static/icons/PhArrowFatUp.svg" alt="Likes" width="24px" height="24px">
|
<img class="icon invert" src="/static/icons/PhArrowFatUp.svg" alt="Likes" width="24px" height="24px">
|
||||||
<p>{{post.UpvoteCount}}</p>
|
<p>{{post.Upvotes}}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-center gap-2">
|
<div class="flex flex-center gap-2">
|
||||||
<img class="icon invert" src="/static/icons/PhArrowFatDown.svg" alt="Dislikes" width="24px" height="24px">
|
<img class="icon invert" src="/static/icons/PhArrowFatDown.svg" alt="Dislikes" width="24px" height="24px">
|
||||||
<p>{{post.DownvoteCount}}</p>
|
<p>{{post.Downvotes}}</p>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
@@ -66,16 +66,16 @@
|
|||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#equal this.Type "image"}}
|
{{#equal this.Type "image"}}
|
||||||
<img class="my-2 max-h-96 object-contain" src="{{rewriteUrl(this.Url)}}" loading="lazy">
|
<img class="my-2 max-h-96 object-contain" src="{{this.Url}}" loading="lazy">
|
||||||
{{/equal}}
|
{{/equal}}
|
||||||
{{#equal this.Type "video"}}
|
{{#equal this.Type "video"}}
|
||||||
<video class="my-2 max-h-96 object-contain" controls loop>
|
<video class="my-2 max-h-96 object-contain" controls loop>
|
||||||
<source type="{{this.MimeType}}" src="{{rewriteUrl(this.Url)}}" />
|
<source type="{{this.MimeType}}" src="{{this.Url}}" />
|
||||||
</video>
|
</video>
|
||||||
{{/equal}}
|
{{/equal}}
|
||||||
|
|
||||||
{{#if this.Metadata.Description}}
|
{{#if this.Description}}
|
||||||
<p>{{{sanitizeDescription(this.Metadata.Description)}}}</p>
|
<p>{{{this.Description}}}</p>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</div>
|
</div>
|
||||||
@@ -84,12 +84,12 @@
|
|||||||
<div class="flex gap-2 my-2 flex-wrap">
|
<div class="flex gap-2 my-2 flex-wrap">
|
||||||
<style nonce="{{nonce}}">
|
<style nonce="{{nonce}}">
|
||||||
{{#each post.tags}}
|
{{#each post.tags}}
|
||||||
.{{this.BackgroundID}} { background-image: url('{{this.Background}}') }
|
.{{this.BackgroundId}} { background-image: url('{{this.Background}}') }
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</style>
|
</style>
|
||||||
{{#each post.tags}}
|
{{#each post.tags}}
|
||||||
<a href="/t/{{this.Tag}}">
|
<a href="/t/{{this.Tag}}">
|
||||||
<div class="rounded-md p-4 min-w-[110px] bg-slate-500 {{this.BackgroundID}}">
|
<div class="rounded-md p-4 min-w-[110px] bg-slate-500 {{this.BackgroundId}}">
|
||||||
<p class="font-bold text-white text-center">
|
<p class="font-bold text-white text-center">
|
||||||
{{#if tag.Display}}
|
{{#if tag.Display}}
|
||||||
{{this.Display}}
|
{{this.Display}}
|
||||||
@@ -110,7 +110,7 @@
|
|||||||
<input id="comments__expandBtn" type="checkbox" checked>
|
<input id="comments__expandBtn" type="checkbox" checked>
|
||||||
<label class="comments__expandBtn__label my-2 py-4 border-solid border-t-2 border-slate-400"
|
<label class="comments__expandBtn__label my-2 py-4 border-solid border-t-2 border-slate-400"
|
||||||
for="comments__expandBtn">
|
for="comments__expandBtn">
|
||||||
<h3 class="text-xl font-bold">Comments ({{post.CommentCount}})</h3>
|
<h3 class="text-xl font-bold">Comments ({{post.Comments}})</h3>
|
||||||
<span class="text-xl font-bold"></span>
|
<span class="text-xl font-bold"></span>
|
||||||
</label>
|
</label>
|
||||||
<div class="comments flex flex-col gap-2">
|
<div class="comments flex flex-col gap-2">
|
||||||
|
|||||||
@@ -30,20 +30,35 @@
|
|||||||
<a href="?section=hot&sort={{sort}}"><b>Hot</b></a>
|
<a href="?section=hot&sort={{sort}}"><b>Hot</b></a>
|
||||||
<a href="?section=new&sort={{sort}}">New</a>
|
<a href="?section=new&sort={{sort}}">New</a>
|
||||||
<a href="?section=top&sort={{sort}}">Top</a>
|
<a href="?section=top&sort={{sort}}">Top</a>
|
||||||
|
<a href="?section=random&sort=random">Random</a>
|
||||||
{{/equal}}
|
{{/equal}}
|
||||||
{{#equal section "new"}}
|
{{#equal section "new"}}
|
||||||
<a href="?section=hot&sort={{sort}}">Hot</a>
|
<a href="?section=hot&sort={{sort}}">Hot</a>
|
||||||
<a href="?section=new&sort={{sort}}"><b>New</b></a>
|
<a href="?section=new&sort={{sort}}"><b>New</b></a>
|
||||||
<a href="?section=top&sort={{sort}}">Top</a>
|
<a href="?section=top&sort={{sort}}">Top</a>
|
||||||
|
<a href="?section=random&sort=random">Random</a>
|
||||||
{{/equal}}
|
{{/equal}}
|
||||||
{{#equal section "top"}}
|
{{#equal section "top"}}
|
||||||
<a href="?section=hot&sort={{sort}}">Hot</a>
|
<a href="?section=hot&sort={{sort}}">Hot</a>
|
||||||
<a href="?section=new&sort={{sort}}">New</a>
|
<a href="?section=new&sort={{sort}}">New</a>
|
||||||
<a href="?section=top&sort={{sort}}"><b>Top</b></a>
|
<a href="?section=top&sort={{sort}}"><b>Top</b></a>
|
||||||
|
<a href="?section=random&sort=random">Random</a>
|
||||||
|
{{/equal}}
|
||||||
|
{{#equal section "random"}}
|
||||||
|
<a href="?section=hot&sort={{sort}}">Hot</a>
|
||||||
|
<a href="?section=new&sort={{sort}}">New</a>
|
||||||
|
<a href="?section=top&sort={{sort}}">Top</a>
|
||||||
|
<a href="?section=random&sort=random"><b>Random</b></a>
|
||||||
{{/equal}}
|
{{/equal}}
|
||||||
</div>
|
</div>
|
||||||
<hr class="sm:hidden my-2" />
|
<hr class="sm:hidden my-2" />
|
||||||
<div class="flex flex-col sm:items-end">
|
<div class="flex flex-col sm:items-end">
|
||||||
|
{{#equal section "random"}}
|
||||||
|
<a href="?section=hot&sort=popular">Popular</a>
|
||||||
|
<a href="?section=hot&sort=newest">Newest</a>
|
||||||
|
<a href="?section=hot&sort=best">Best</a>
|
||||||
|
{{/equal}}
|
||||||
|
{{#noteq section "random"}}
|
||||||
{{#equal sort "popular"}}
|
{{#equal sort "popular"}}
|
||||||
<a href="?section={{section}}&sort=popular"><b>Popular</b></a>
|
<a href="?section={{section}}&sort=popular"><b>Popular</b></a>
|
||||||
<a href="?section={{section}}&sort=newest">Newest</a>
|
<a href="?section={{section}}&sort=newest">Newest</a>
|
||||||
@@ -59,6 +74,7 @@
|
|||||||
<a href="?section={{section}}&sort=newest">Newest</a>
|
<a href="?section={{section}}&sort=newest">Newest</a>
|
||||||
<a href="?section={{section}}&sort=best"><b>Best</b></a>
|
<a href="?section={{section}}&sort=best"><b>Best</b></a>
|
||||||
{{/equal}}
|
{{/equal}}
|
||||||
|
{{/noteq}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
Reference in New Issue
Block a user