diff --git a/api/comments.go b/api/comments.go index 4137e81..d85ad5a 100644 --- a/api/comments.go +++ b/api/comments.go @@ -75,7 +75,7 @@ func ParseComment(data gjson.Result) types.Comment { return types.Comment{ Comments: comments, User: types.User{ - Id: data.Get("account.id").String(), + Id: data.Get("account.id").Int(), Username: data.Get("account.username").String(), Avatar: userAvatar, }, diff --git a/api/f.ts b/api/f.ts index 8022f63..31380c0 100644 --- a/api/f.ts +++ b/api/f.ts @@ -1,16 +1,8 @@ -export const fetchUserInfo = async (userID: string): Promise => { - // https://api.imgur.com/account/v1/accounts/hughjaniss?client_id=${CLIENT_ID} - const response = await get( - `https://api.imgur.com/account/v1/accounts/${userID.toLowerCase()}?client_id=${CONFIG.imgur_client_id}&include=`, - ); - return JSON.parse(response.body); -} - export const fetchUserPosts = async (userID: string, sort: Sorting = 'newest'): Promise => { /* eslint-disable max-len */ // https://api.imgur.com/3/account/mombotnumber5/submissions/0/newest?album_previews=1&client_id=${CLIENT_ID} const response = await get( - `https://api.imgur.com/3/account/${userID.toLowerCase()}/submissions/0/${sort}?album_previews=1&client_id=${CONFIG.imgur_client_id}`, + ``, ); return JSON.parse(response.body).data; /* eslint-enable max-len */ diff --git a/api/user.go b/api/user.go new file mode 100644 index 0000000..278d4f7 --- /dev/null +++ b/api/user.go @@ -0,0 +1,91 @@ +package api + +import ( + "encoding/json" + "io" + "net/http" + "strings" + "sync" + "time" + + "codeberg.org/video-prize-ranch/rimgo/types" + "github.com/spf13/viper" + "github.com/tidwall/gjson" +) + +func FetchUser(username string) (types.User, error) { + res, err := http.Get("https://api.imgur.com/account/v1/accounts/" + username + "?client_id=" + viper.GetString("RIMGU_IMGUR_CLIENT_ID")) + if err != nil { + return types.User{}, err + } + + body, err := io.ReadAll(res.Body) + if err != nil { + return types.User{}, err + } + + var user types.User + err = json.Unmarshal(body, &user) + if err != nil { + return types.User{}, err + } + + user.Cover = strings.ReplaceAll(user.Cover, "https://imgur.com", "") + user.Avatar = strings.ReplaceAll(user.Avatar, "https://i.imgur.com", "") + + createdTime, _ := time.Parse("2006-01-02T15:04:05Z", user.CreatedAt) + user.CreatedAt = createdTime.Format("January 2, 2006") + + return user, nil +} + +func FetchSubmissions(username string, sort string, page string) ([]types.Submission, error) { + res, err := http.Get("https://api.imgur.com/3/account/" + username + "/submissions/" + page + "/" + sort + "?album_previews=1&client_id=" + viper.GetString("RIMGU_IMGUR_CLIENT_ID")) + if err != nil { + return []types.Submission{}, err + } + + body, err := io.ReadAll(res.Body) + if err != nil { + return []types.Submission{}, err + } + + data := gjson.Parse(string(body)) + + submissions := []types.Submission{} + + wg := sync.WaitGroup{} + data.Get("data").ForEach( + func(key, value gjson.Result) bool { + wg.Add(1) + + go func() { + defer wg.Done() + + cover := value.Get("images.#(id==\"" + value.Get("cover").String() + "\")") + + submissions = append(submissions, types.Submission{ + Id: value.Get("id").String(), + Link: strings.ReplaceAll(value.Get("link").String(), "https://imgur.com", ""), + Title: value.Get("title").String(), + Cover: types.Media{ + Id: cover.Get("id").String(), + Description: cover.Get("description").String(), + Type: strings.Split(cover.Get("type").String(), "/")[0], + Url: strings.ReplaceAll(cover.Get("link").String(), "https://i.imgur.com", ""), + }, + Points: cover.Get("points").Int(), + Upvotes: cover.Get("ups").Int(), + Downvotes: cover.Get("downs").Int(), + Comments: cover.Get("comment_count").Int(), + Views: cover.Get("views").Int(), + IsAlbum: cover.Get("is_album").Bool(), + }) + }() + + return true + }, + ) + wg.Wait() + return submissions, nil +} diff --git a/main.go b/main.go index 08a9f84..8c5ac66 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,7 @@ package main import ( - "log" + "fmt" "net/http" "codeberg.org/video-prize-ranch/rimgo/pages" @@ -28,7 +28,7 @@ func main() { err := viper.ReadInConfig() if err != nil { - log.Fatal(err) + fmt.Println(err) } engine := handlebars.NewFileSystem(http.FS(views.GetFiles()), ".hbs") @@ -53,8 +53,9 @@ func main() { app.Get("/:baseName.:extension", pages.HandleMedia) app.Get("/a/:galleryID", pages.HandleGallery) //app.Get("/t/:tagID", pages.HandleAlbum) - /*app.Get("/user/:userID", pages.HandleUser) - app.Get("/user/:userID/cover", pages.HandleUserCover)*/ + app.Get("/user/:userID", pages.HandleUser) + app.Get("/user/:userID/cover", pages.HandleUserCover) + app.Get("/user/:userID/avatar", pages.HandleUserAvatar) app.Get("/gallery/:galleryID", pages.HandleGallery) app.Listen(":" + viper.GetString("RIMGU_PORT")) diff --git a/pages/frontpage.go b/pages/frontpage.go index 0b14894..325f600 100644 --- a/pages/frontpage.go +++ b/pages/frontpage.go @@ -1,16 +1,14 @@ package pages -import "github.com/gofiber/fiber/v2" +import ( + "codeberg.org/video-prize-ranch/rimgo/utils" + "github.com/gofiber/fiber/v2" +) func FrontpageHandler(c *fiber.Ctx) error { - c.Set("Cache-Control", "public,max-age=1800") - c.Set("X-Frame-Options", "DENY") - c.Set("Referrer-Policy", "no-referrer") - c.Set("X-Content-Type-Options", "nosniff") - c.Set("X-Robots-Tag", "noindex, noimageindex, nofollow") - c.Set("Strict-Transport-Security", "max-age=31557600") - c.Set("Permissions-Policy", "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), interest-cohort=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()") - c.Set("Content-Security-Policy", "default-src 'none'; style-src 'self'; script-src 'none'; img-src 'self'; font-src 'self'; block-all-mixed-content; manifest-src 'self'") + utils.SetHeaders(c) + c.Set("Cache-Control", "public,max-age=31557600") + c.Set("Content-Security-Policy", "default-src 'none'; style-src 'self'; img-src 'self'; font-src 'self'; block-all-mixed-content") return c.Render("frontpage", fiber.Map{}) } \ No newline at end of file diff --git a/pages/gallery.go b/pages/gallery.go index f072e5f..cb0fb98 100644 --- a/pages/gallery.go +++ b/pages/gallery.go @@ -3,18 +3,13 @@ package pages import ( "codeberg.org/video-prize-ranch/rimgo/api" "codeberg.org/video-prize-ranch/rimgo/types" + "codeberg.org/video-prize-ranch/rimgo/utils" "github.com/gofiber/fiber/v2" ) func HandleGallery(c *fiber.Ctx) error { - c.Set("Cache-Control", "public,max-age=604800") - c.Set("X-Frame-Options", "DENY") - c.Set("Referrer-Policy", "no-referrer") - c.Set("X-Content-Type-Options", "nosniff") - c.Set("X-Robots-Tag", "noindex, noimageindex, nofollow") - c.Set("Strict-Transport-Security", "max-age=31557600") - c.Set("Permissions-Policy", "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), interest-cohort=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()") - c.Set("Content-Security-Policy", "default-src 'none'; media-src 'self'; style-src 'self'; script-src 'none'; img-src 'self'; font-src 'self'; block-all-mixed-content; manifest-src 'self'") + utils.SetHeaders(c) + c.Set("Content-Security-Policy", "default-src 'none'; media-src 'self'; style-src 'self'; img-src 'self'; font-src 'self'; block-all-mixed-content") album, err := api.FetchAlbum(c.Params("galleryID")) if err != nil { @@ -23,10 +18,13 @@ func HandleGallery(c *fiber.Ctx) error { comments := []types.Comment{} if album.SharedWithCommunity { + c.Set("Cache-Control", "public,max-age=604800") comments, err = api.FetchComments(c.Params("galleryID")) if err != nil { return err } + } else { + c.Set("Cache-Control", "public,max-age=31557600") } return c.Render("gallery", fiber.Map{ diff --git a/pages/h.ts b/pages/h.ts index 4734bcf..936f876 100644 --- a/pages/h.ts +++ b/pages/h.ts @@ -1,27 +1,3 @@ -export const handleUser = async (request: Hapi.Request, h: Hapi.ResponseToolkit) => { - // https://imgur.com/user/MomBotNumber5 - if (!CONFIG.use_api) { - return 'User page disabled. Rimgu administrator needs to enable API for this to work.'; - } - const userID = request.params.userID; - const user = await fetchUserInfo(userID); - const posts = await fetchUserPosts(userID); - return h.view('posts', { - posts, - user, - pageTitle: CONFIG.page_title, - util, - }); -}; - -export const handleUserCover = async (request: Hapi.Request, h: Hapi.ResponseToolkit) => { - const userID = request.params.userID; - const result = await fetchMedia(`/user/${userID}/cover?maxwidth=2560`); - const response = h.response(result.rawBody) - .header('Content-Type', result.headers["content-type"] || `image/jpeg`); - return response; -}; - export const handleTag = async (request: Hapi.Request, h: Hapi.ResponseToolkit) => { // https://imgur.com/t/funny if (!CONFIG.use_api) { diff --git a/pages/media.go b/pages/media.go index 6781863..1b959b8 100644 --- a/pages/media.go +++ b/pages/media.go @@ -4,17 +4,35 @@ import ( "net/http" "strconv" + "codeberg.org/video-prize-ranch/rimgo/utils" "github.com/gofiber/fiber/v2" ) func HandleMedia(c *fiber.Ctx) error { - res, err := http.Get("https://i.imgur.com/" + c.Params("baseName") + "." + c.Params("extension")) + c.Set("Cache-Control", "public,max-age=31557600") + 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") +}; + +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") +}; + +func handleMedia(c *fiber.Ctx, url string) error { + utils.SetHeaders(c) + c.Set("Content-Security-Policy", "default-src 'none'; media-src 'self'; style-src 'self'; img-src 'self'; font-src 'self'; block-all-mixed-content") + + res, err := http.Get(url) if err != nil { return err } c.Set("Content-Type", res.Header.Get("Content-Type")); contentLen, _ := strconv.Atoi(res.Header.Get("Content-Length")) - c.SendStream(res.Body, contentLen) - return nil + return c.SendStream(res.Body, contentLen) } \ No newline at end of file diff --git a/pages/user.go b/pages/user.go new file mode 100644 index 0000000..b6f21de --- /dev/null +++ b/pages/user.go @@ -0,0 +1,42 @@ +package pages + +import ( + "sync" + + "codeberg.org/video-prize-ranch/rimgo/api" + "codeberg.org/video-prize-ranch/rimgo/types" + "codeberg.org/video-prize-ranch/rimgo/utils" + "github.com/gofiber/fiber/v2" +) + +func HandleUser(c *fiber.Ctx) error { + utils.SetHeaders(c) + c.Set("Cache-Control", "public,max-age=604800") + c.Set("Content-Security-Policy", "default-src 'none'; media-src 'self'; style-src 'unsafe-inline' 'self'; img-src 'self'; font-src 'self'; block-all-mixed-content") + + wg := sync.WaitGroup{} + wg.Add(2) + user, err := types.User{}, error(nil) + go func() { + defer wg.Done() + user, err = api.FetchUser(c.Params("userID")) + }() + if err != nil { + return err + } + + submissions, err := []types.Submission{}, error(nil) + go func() { + defer wg.Done() + submissions, err = api.FetchSubmissions(c.Params("userID"), "newest", "0") + }() + if err != nil { + return err + } + + wg.Wait() + return c.Render("user", fiber.Map{ + "user": user, + "submissions": submissions, + }) +} diff --git a/static/css/user.css b/static/css/user.css new file mode 100644 index 0000000..c8e487b --- /dev/null +++ b/static/css/user.css @@ -0,0 +1,63 @@ +main { + margin: 0 12vw; +} + +h2, p { + margin: 0; +} + +.userMeta { + padding: 2vw; + border-radius: 8px; +} + +.userMeta__upper { + display: flex; + gap: 10px; + align-items: center; +} + +.userMeta__upper__bio { + margin-top: 1em; +} + +.pfp { + border-radius: 100%; +} + +.posts { + margin-top: 1em; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + grid-auto-rows: 1fr; + gap: 1rem; +} + +.post { + border-radius: 8px; + background-color: #3f3f3f; + font-weight: 400; +} + +img, +video:not(:fullscreen) { + object-fit: cover; + aspect-ratio: 1; +} + +.post__title { + margin: 0 6px; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} + +.post__meta { + padding: 6px; + display: flex; + gap: 10px; +} + +.post__meta .material-icons-outlined { + font-size: 1rem; +} \ No newline at end of file diff --git a/types/User.go b/types/User.go index 57d3624..1b1a82d 100644 --- a/types/User.go +++ b/types/User.go @@ -1,8 +1,24 @@ package types type User struct { - Id string - Username string - Avatar string - CreatedAt string + Id int64 `json:"id"` + Bio string `json:"bio"` + Username string `json:"username"` + Points int64 `json:"reputation_count"` + Cover string `json:"cover_url"` + Avatar string `json:"avatar_url"` + CreatedAt string `json:"created_at"` +} + +type Submission struct { + Id string + Title string + Link string + Cover Media + Points int64 + Upvotes int64 + Downvotes int64 + Comments int64 + Views int64 + IsAlbum bool } diff --git a/utils/setHeaders.go b/utils/setHeaders.go new file mode 100644 index 0000000..0d9e279 --- /dev/null +++ b/utils/setHeaders.go @@ -0,0 +1,12 @@ +package utils + +import "github.com/gofiber/fiber/v2" + +func SetHeaders(c *fiber.Ctx) { + c.Set("X-Frame-Options", "DENY") + c.Set("Referrer-Policy", "no-referrer") + c.Set("X-Content-Type-Options", "nosniff") + c.Set("X-Robots-Tag", "noindex, noimageindex, nofollow") + c.Set("Strict-Transport-Security", "max-age=31557600") + c.Set("Permissions-Policy", "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), interest-cohort=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()") +} \ No newline at end of file diff --git a/views/partials/comment.hbs b/views/partials/comment.hbs index 5ebeae3..297efad 100644 --- a/views/partials/comment.hbs +++ b/views/partials/comment.hbs @@ -1,14 +1,14 @@
{{{this.Comment}}} -

- {{this.RelTime}} +

+ {{this.RelTime}} {{#if this.DeletedAt}} (deleted {{this.DeletedAt}}) {{/if}} diff --git a/views/partials/post.hbs b/views/partials/post.hbs new file mode 100644 index 0000000..a186060 --- /dev/null +++ b/views/partials/post.hbs @@ -0,0 +1,32 @@ + +

+ {{#equal Cover.Type "video"}} + + {{/equal}} + {{#equal Cover.Type "image"}} + + {{/equal}} +

{{Title}}

+ +
+ \ No newline at end of file diff --git a/views/user.hbs b/views/user.hbs new file mode 100644 index 0000000..6b3c6ec --- /dev/null +++ b/views/user.hbs @@ -0,0 +1,39 @@ + + + + + {{user.Username}} - rimgo + + {{> partials/head }} + + + + + + + + {{> partials/header }} + +
+
+
+ +
+

{{user.Username}}

+

{{user.Points}} pts ยท {{user.CreatedAt}}

+
+
+

{{user.Bio}}

+
+ +
+ {{#each submissions}} + {{> partials/post }} + {{/each}} +
+
+ + {{> partials/footer }} + + + \ No newline at end of file