7 Commits

Author SHA1 Message Date
orangix
1e82ac8745 move {prev,next}InTag to bottom 2024-02-06 01:42:08 +01:00
orangix
0171d76fae add previous button 2024-02-06 01:41:28 +01:00
video-prize-ranch
337796b9be Update Justfile (#170)
Reviewed-on: https://codeberg.org/rimgo/rimgo/pulls/170
Co-authored-by: video-prize-ranch <cb.8a3w5@simplelogin.co>
Co-committed-by: video-prize-ranch <cb.8a3w5@simplelogin.co>
2024-02-05 22:57:33 +00:00
orangix
927ea20fad next button for tagged posts (#169)
No previous button because that would be annoying to implement serverside with pagination etc.

Closes #115

Fixed conflict with #154, #155, #156

Co-authored-by: video-prize-ranch <cb.8a3w5@simplelogin.co>
Reviewed-on: https://codeberg.org/rimgo/rimgo/pulls/169
Reviewed-by: video-prize-ranch <video-prize-ranch@noreply.codeberg.org>
Co-authored-by: orangix <orangix@noreply.codeberg.org>
Co-committed-by: orangix <orangix@noreply.codeberg.org>
2024-02-05 22:47:04 +00:00
orangix
7433265991 Merge pull request 'Errors in image format' (#168) from feature/image-errors into main
Reviewed-on: https://codeberg.org/rimgo/rimgo/pulls/168
Reviewed-by: orangix <orangix@noreply.codeberg.org>
2024-02-05 21:28:29 +00:00
video-prize-ranch
fb82afc7dd Only show image errors on image direct links 2024-02-05 16:26:54 -05:00
video-prize-ranch
1579e59dca Errors in image format (closes #165) 2024-02-04 21:24:55 -05:00
15 changed files with 220 additions and 125 deletions

View File

@@ -4,4 +4,9 @@ build:
dev:
tailwindcss -i static/tailwind.css -o static/app.css -m -w &
go run github.com/cosmtrek/air@latest -c .air.toml
go run github.com/cosmtrek/air@latest -c .air.toml
tag-vpr:
podman pull codeberg.org/rimgo/rimgo:latest
podman image tag codeberg.org/rimgo/rimgo:latest codeberg.org/video-prize-ranch/rimgo:latest
podman push codeberg.org/video-prize-ranch/rimgo:latest

View File

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

View File

@@ -33,6 +33,8 @@ type Submission struct {
Comments int64
Views int64
IsAlbum bool
tagstring string
}
func (client *Client) FetchUser(username string) (User, error) {

18
main.go
View File

@@ -52,14 +52,7 @@ func main() {
code = e.Code
}
err = ctx.Status(code).Render("errors/error", fiber.Map{
"err": err,
})
if err != nil {
return ctx.Status(fiber.StatusInternalServerError).SendString("Internal Server Error")
}
return nil
return utils.RenderError(ctx, code)
},
})
@@ -77,14 +70,23 @@ func main() {
app.Get("/errors/429", func(c *fiber.Ctx) error {
return c.Render("errors/429", nil)
})
app.Get("/errors/429/img", func(c *fiber.Ctx) error {
return c.Redirect("/static/img/error-429.png")
})
app.Get("/errors/404", func(c *fiber.Ctx) error {
return c.Render("errors/404", nil)
})
app.Get("/errors/404/img", func(c *fiber.Ctx) error {
return c.Redirect("/static/img/error-404.png")
})
app.Get("/errors/error", func(c *fiber.Ctx) error {
return c.Render("errors/error", fiber.Map{
"err": "Test error",
})
})
app.Get("/errors/error/img", func(c *fiber.Ctx) error {
return c.Redirect("/static/img/error-generic.png")
})
} else {
app.Use("/static", filesystem.New(filesystem.Config{
MaxAge: 2592000,

View File

@@ -23,10 +23,10 @@ func HandleEmbed(c *fiber.Ctx) error {
post, err = ApiClient.FetchMedia(c.Params("postID"))
}
if err != nil && err.Error() == "ratelimited by imgur" {
return c.Status(429).Render("errors/429", nil)
return utils.RenderError(c, 429)
}
if err != nil && post.Id == "" && strings.Contains(err.Error(), "404") {
return c.Status(404).Render("errors/404", nil)
return utils.RenderError(c, 404)
}
if err != nil {
return err

View File

@@ -11,22 +11,25 @@ import (
func HandleMedia(c *fiber.Ctx) error {
c.Set("Cache-Control", "public,max-age=31557600")
c.Set("Content-Security-Policy", "default-src 'none'; style-src 'self'; img-src 'self'")
if strings.HasPrefix(c.Path(), "/stack") {
return handleMedia(c, "https://i.stack.imgur.com/" + strings.ReplaceAll(c.Params("baseName"), "stack/", "") + "." + c.Params("extension"))
return handleMedia(c, "https://i.stack.imgur.com/"+strings.ReplaceAll(c.Params("baseName"), "stack/", "")+"."+c.Params("extension"))
} else {
return handleMedia(c, "https://i.imgur.com/" + c.Params("baseName") + "." + c.Params("extension"))
return handleMedia(c, "https://i.imgur.com/"+c.Params("baseName")+"."+c.Params("extension"))
}
}
func HandleUserCover(c *fiber.Ctx) error {
c.Set("Cache-Control", "public,max-age=604800")
return handleMedia(c, "https://imgur.com/user/" + c.Params("userID") + "/cover?maxwidth=2560")
};
c.Set("Content-Security-Policy", "default-src 'none'")
return handleMedia(c, "https://imgur.com/user/"+c.Params("userID")+"/cover?maxwidth=2560")
}
func HandleUserAvatar(c *fiber.Ctx) error {
c.Set("Cache-Control", "public,max-age=604800")
return handleMedia(c, "https://imgur.com/user/" + c.Params("userID") + "/avatar")
};
c.Set("Content-Security-Policy", "default-src 'none'")
return handleMedia(c, "https://imgur.com/user/"+c.Params("userID")+"/avatar")
}
func handleMedia(c *fiber.Ctx, url string) error {
utils.SetHeaders(c)
@@ -45,7 +48,7 @@ func handleMedia(c *fiber.Ctx, url string) error {
if err != nil {
return err
}
utils.SetReqHeaders(req)
if c.Get("Range") != "" {
@@ -57,23 +60,18 @@ func handleMedia(c *fiber.Ctx, url string) error {
return err
}
c.Status(res.StatusCode)
if res.StatusCode == 404 {
return c.Render("errors/404", fiber.Map{
"path": c.Path(),
})
if res.StatusCode == 404 || strings.Contains(res.Request.URL.String(), "error/404") {
return utils.RenderError(c, 404)
} else if res.StatusCode == 429 {
return c.Render("errors/429", fiber.Map{
"path": c.Path(),
})
return utils.RenderError(c, 429)
}
c.Set("Accept-Ranges", "bytes")
c.Set("Content-Type", res.Header.Get("Content-Type"));
c.Set("Content-Type", res.Header.Get("Content-Type"))
c.Set("Content-Length", res.Header.Get("Content-Length"))
if res.Header.Get("Content-Range") != "" {
c.Set("Content-Range", res.Header.Get("Content-Range"))
}
return c.SendStream(res.Body)
}
}

View File

@@ -3,6 +3,7 @@ package pages
import (
"crypto/rand"
"fmt"
"strconv"
"strings"
"codeberg.org/rimgo/rimgo/api"
@@ -26,14 +27,12 @@ func HandlePost(c *fiber.Ctx) error {
post, err = ApiClient.FetchMedia(c.Params("postID"))
}
if err != nil && err.Error() == "ratelimited by imgur" {
return c.Status(429).Render("errors/429", fiber.Map{
"path": c.Path(),
})
return utils.RenderError(c, 429)
}
if err != nil && post.Id == "" && strings.Contains(err.Error(), "404") {
return c.Status(404).Render("errors/404", nil)
return utils.RenderError(c, 404)
}
if err != nil {
if err != nil {
return err
}
@@ -58,9 +57,69 @@ func HandlePost(c *fiber.Ctx) error {
}
c.Set("Content-Security-Policy", csp)
var prev, next string
tagParam := strings.Split(c.Query("tag"), ".")
if len(tagParam) == 4 {
tag, sort, page, index := tagParam[0], tagParam[1], tagParam[2], tagParam[3]
prev = prevInTag(ApiClient, tag, sort, page, index)
next = nextInTag(ApiClient, tag, sort, page, index)
}
return c.Render("post", fiber.Map{
"post": post,
"prev": prev,
"next": next,
"comments": comments,
"nonce": nonce,
})
}
// Cursed function
func prevInTag(client *api.Client, tagname, sort, page, I string) string {
i, err := strconv.Atoi(I)
if err != nil || i < 0 {
return ""
}
if i == 0 {
// Don't go before the first in tag
if page == "1" {
return ""
}
pagen, err := strconv.Atoi(page)
if err != nil || pagen < 0 {
return ""
}
pagen--
page = strconv.Itoa(pagen)
}
tag, err := client.FetchTag(tagname, sort, page)
if err != nil {
return ""
}
if i == 0 {
return tag.Posts[len(tag.Posts)-1].Link
}
return tag.Posts[i-1].Link
}
// Cursed function
func nextInTag(client *api.Client, tagname, sort, page, I string) string {
i, err := strconv.Atoi(I)
if err != nil || i < 0 {
return ""
}
tag, err := client.FetchTag(tagname, sort, page)
if err != nil {
return ""
}
if i >= len(tag.Posts)-1 {
pageNumber, _ := strconv.Atoi(page)
tagn, err := client.FetchTag(tagname, sort, strconv.Itoa(pageNumber+1))
if err != nil {
return ""
}
return tagn.Posts[0].Link
}
return tag.Posts[i+1].Link
}

View File

@@ -9,6 +9,7 @@ import (
func HandlePrivacy(c *fiber.Ctx) error {
utils.SetHeaders(c)
c.Set("X-Frame-Options", "DENY")
c.Set("Content-Security-Policy", "default-src 'none'; form-action 'self'; style-src 'self'; img-src 'self'; manifest-src 'self'; block-all-mixed-content")
return c.Render("privacy", fiber.Map{
"config": utils.Config,

View File

@@ -25,21 +25,19 @@ func HandleTag(c *fiber.Ctx) error {
tag, err := ApiClient.FetchTag(c.Params("tag"), c.Query("sort"), page)
if err != nil && err.Error() == "ratelimited by imgur" {
return c.Status(429).Render("errors/429", fiber.Map{
"path": c.Path(),
})
return utils.RenderError(c, 429)
}
if err != nil {
return err
}
if tag.Display == "" {
return c.Status(404).Render("errors/404", nil)
return utils.RenderError(c, 404)
}
return c.Render("tag", fiber.Map{
"tag": tag,
"page": page,
"nextPage": pageNumber + 1,
"prevPage": pageNumber - 1,
"tag": tag,
"page": page,
"nextPage": pageNumber + 1,
"prevPage": pageNumber - 1,
})
}

View File

@@ -25,23 +25,19 @@ func HandleUser(c *fiber.Ctx) error {
user, err := ApiClient.FetchUser(c.Params("userID"))
if err != nil && err.Error() == "ratelimited by imgur" {
return c.Status(429).Render("errors/429", fiber.Map{
"path": c.Path(),
})
return utils.RenderError(c, 429)
}
if err != nil {
return err
}
if user.Username == "" {
return c.Status(404).Render("errors/404", nil)
return utils.RenderError(c, 404)
}
submissions, err := ApiClient.FetchSubmissions(c.Params("userID"), "newest", page)
if err != nil && err.Error() == "ratelimited by imgur" {
c.Status(429)
return c.Render("errors/429", fiber.Map{
"path": c.Path(),
})
return utils.RenderError(c, 429)
}
if err != nil {
return err
@@ -64,23 +60,19 @@ func HandleUserComments(c *fiber.Ctx) error {
user, err := ApiClient.FetchUser(c.Params("userID"))
if err != nil && err.Error() == "ratelimited by imgur" {
return c.Status(429).Render("errors/429", fiber.Map{
"path": c.Path(),
})
return utils.RenderError(c, 429)
}
if err != nil {
return err
}
if user.Username == "" {
return c.Status(404).Render("errors/404", nil)
return utils.RenderError(c, 404)
}
comments, err := ApiClient.FetchUserComments(c.Params("userID"))
if err != nil && err.Error() == "ratelimited by imgur" {
c.Status(429)
return c.Render("errors/429", fiber.Map{
"path": c.Path(),
})
return utils.RenderError(c, 429)
}
if err != nil {
return err
@@ -110,23 +102,18 @@ func HandleUserFavorites(c *fiber.Ctx) error {
user, err := ApiClient.FetchUser(c.Params("userID"))
if err != nil && err.Error() == "ratelimited by imgur" {
return c.Status(429).Render("errors/429", fiber.Map{
"path": c.Path(),
})
return utils.RenderError(c, 429)
}
if err != nil {
return err
}
if user.Username == "" {
return c.Status(404).Render("errors/404", nil)
return utils.RenderError(c, 404)
}
favorites, err := ApiClient.FetchUserFavorites(c.Params("userID"), "newest", page)
if err != nil && err.Error() == "ratelimited by imgur" {
c.Status(429)
return c.Render("errors/429", fiber.Map{
"path": c.Path(),
})
return utils.RenderError(c, 429)
}
if err != nil {
return err

BIN
static/img/error-404.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
static/img/error-429.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

25
utils/error.go Normal file
View File

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

View File

@@ -4,7 +4,7 @@
<head>
<title>
{{#if post.Title}}
{{post.Title}} -
{{post.Title}} -
{{/if}}
rimgo
</title>
@@ -23,33 +23,47 @@
<h1 class="text-3xl font-bold">{{post.Title}}</h1>
<p>{{post.CreatedAt}}</p>
</header>
<main>
<div class="flex flex-col gap-2 md:flex-row md:gap-4 md:items-center my-4">
{{#if post.User.Username}}
<a href="/user/{{post.User.Username}}" class="flex gap-2 items-center">
<img src="{{post.User.Avatar}}" class="rounded-full" width="36" height="36" />
<p>
<b>{{post.User.Username}}</b>
</p>
</a>
{{/if}}
<div class="flex gap-2 items-center">
<div class="flex flex-center gap-2">
<img class="icon invert" src="/static/icons/PhEye.svg" alt="Views" width="24px" height="24px">
<p>{{post.Views}}</p>
</div>
{{#if post.SharedWithCommunity}}
<div class="flex flex-center gap-2">
<img class="icon invert" src="/static/icons/PhArrowFatUp.svg" alt="Likes" width="24px" height="24px">
<p>{{post.Upvotes}}</p>
</div>
<div class="flex flex-center gap-2">
<img class="icon invert" src="/static/icons/PhArrowFatDown.svg" alt="Dislikes" width="24px" height="24px">
<p>{{post.Downvotes}}</p>
</div>
<div class="flex flex-col sm:flex-row my-4 w-full justify-between">
<div class="flex flex-col gap-2 md:flex-row md:gap-4 md:items-center">
{{#if post.User.Username}}
<a href="/user/{{post.User.Username}}" class="flex gap-2 items-center">
<img src="{{post.User.Avatar}}" class="rounded-full" width="36" height="36" />
<p>
<b>{{post.User.Username}}</b>
</p>
</a>
{{/if}}
<div class="flex gap-2 items-center">
<div class="flex flex-center gap-2">
<img class="icon invert" src="/static/icons/PhEye.svg" alt="Views" width="24px" height="24px">
<p>{{post.Views}}</p>
</div>
{{#if post.SharedWithCommunity}}
<div class="flex flex-center gap-2">
<img class="icon invert" src="/static/icons/PhArrowFatUp.svg" alt="Likes" width="24px" height="24px">
<p>{{post.Upvotes}}</p>
</div>
<div class="flex flex-center gap-2">
<img class="icon invert" src="/static/icons/PhArrowFatDown.svg" alt="Dislikes" width="24px" height="24px">
<p>{{post.Downvotes}}</p>
</div>
{{/if}}
</div>
</div>
{{#noteq next ""}}
<div class="flex">
{{#noteq prev ""}}
<a href="{{prev}}">
<button title="Previous" class="px-3 py-2 rounded-l-lg bg-slate-600">&lt;</button>
</a>
{{/noteq}}
<a class="[&:only-child>button]:rounded-lg" href="{{next}}">
<button class="px-3 py-2 rounded-r-lg bg-green-400 text-gray-800">Next &gt;</button>
</a>
</div>
{{/noteq}}
</div>
<div class="flex flex-center flex-col break-words">
@@ -70,7 +84,7 @@
{{#if this.Description}}
<p>{{{this.Description}}}</p>
{{/if}}
{{/each}}
{{/each}}
</div>
{{#if post.tags}}
@@ -78,7 +92,7 @@
<style nonce="{{nonce}}">
{{#each post.tags}}
.{{this.BackgroundId}} { background-image: url('{{this.Background}}') }
{{/each}}
{{/each}}
</style>
{{#each post.tags}}
<a href="/t/{{this.Tag}}">
@@ -101,7 +115,8 @@
{{#if comments}}
<div>
<input id="comments__expandBtn" type="checkbox" checked>
<label class="comments__expandBtn__label my-2 py-4 border-solid border-t-2 border-slate-400" for="comments__expandBtn">
<label class="comments__expandBtn__label my-2 py-4 border-solid border-t-2 border-slate-400"
for="comments__expandBtn">
<h3 class="text-xl font-bold">Comments ({{post.Comments}})</h3>
<span class="text-xl font-bold"></span>
</label>