package view import ( "fmt" "html/template" "net/http" "os" "slices" "strconv" "strings" "arimelody-web/controller" "arimelody-web/model" "arimelody-web/templates" "github.com/gomarkdown/markdown" "github.com/gomarkdown/markdown/html" "github.com/gomarkdown/markdown/parser" ) type ( BlogView struct { Collections []*BlogViewPostCollection } BlogViewPostCollection struct { Name string Posts []*BlogPostView } BlogPostView struct { *model.BlogPost Author *model.Account Comments []*model.ThreadViewPost Likes int Boosts int BlueskyURL string MastodonURL string } ) var mdRenderer = html.NewRenderer(html.RendererOptions{ Flags: html.CommonFlags | html.HrefTargetBlank, }) func BlogHandler(app *model.AppState) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.Count(r.URL.Path, "/") > 1 { http.NotFound(w, r) return } if len(r.URL.Path) > 1 { ServeBlogPost(app, r.URL.Path[1:]).ServeHTTP(w, r) return } dbPosts, err := controller.GetBlogPosts(app.DB, true, -1, 0) if err != nil { if strings.Contains(err.Error(), "no rows") { http.NotFound(w, r) return } fmt.Fprintf(os.Stderr, "WARN: Failed to fetch blog posts: %v\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } collections := []*BlogViewPostCollection{} posts := []*BlogPostView{} collectionYear := 0 for i, post := range dbPosts { author, err := controller.GetAccountByID(app.DB, post.AuthorID) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve author of blog %s: %v\n", post.ID, err) continue } if i == 0 { collectionYear = post.CreatedAt.Year() } if post.CreatedAt.Year() != collectionYear || i == len(dbPosts) - 1 { if i == len(dbPosts) - 1 { posts = append(posts, &BlogPostView{ BlogPost: post, Author: author, }) } postsCopy := slices.Clone(posts) collections = append(collections, &BlogViewPostCollection{ Name: strconv.Itoa(collectionYear), Posts: postsCopy, }) posts = []*BlogPostView{} collectionYear = post.CreatedAt.Year() } posts = append(posts, &BlogPostView{ BlogPost: post, Author: author, }) } err = templates.BlogTemplate.Execute(w, BlogView{ Collections: collections, }) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Error rendering blog post: %v\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } }) } func ServeBlogPost(app *model.AppState, blogPostID string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { blog, err := controller.GetBlogPost(app.DB, blogPostID) if err != nil { if strings.Contains(err.Error(), "no rows") { http.NotFound(w, r) return } fmt.Fprintf(os.Stderr, "WARN: Failed to fetch blog post Bluesky thread: %v\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } if !blog.Visible { session, err := controller.GetSessionFromRequest(app, r) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve session: %v\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } if session == nil || session.Account == nil { http.NotFound(w, r) return } } author, err := controller.GetAccountByID(app.DB, blog.AuthorID) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to retrieve author of blog %s: %v\n", blog.ID, err) } // blog.Markdown += " " mdParser := parser.NewWithExtensions(parser.CommonExtensions | parser.AutoHeadingIDs) md := mdParser.Parse([]byte(blog.Markdown)) blog.HTML = template.HTML(markdown.Render(md, mdRenderer)) comments := []*model.ThreadViewPost{} likeCount := 0 boostCount := 0 var blueskyURL string var blueskyPost *model.ThreadViewPost if blog.BlueskyActorID != nil && blog.BlueskyPostID != nil { blueskyPost, err = controller.FetchThreadViewPost(*blog.BlueskyActorID, *blog.BlueskyPostID) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Failed to fetch blog post Bluesky thread: %v\n", err) } else { comments = append(comments, blueskyPost.Replies...) likeCount += blueskyPost.Post.LikeCount boostCount += blueskyPost.Post.RepostCount blueskyURL = fmt.Sprintf("https://bsky.app/profile/%s/post/%s", blueskyPost.Post.Author.Handle, *blog.BlueskyPostID) } } err = templates.BlogPostTemplate.Execute(w, BlogPostView{ BlogPost: blog, Author: author, Comments: comments, Likes: likeCount, Boosts: boostCount, BlueskyURL: blueskyURL, }) if err != nil { fmt.Fprintf(os.Stderr, "WARN: Error rendering blog post: %v\n", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } }) }