Eric Bower
·
15 Nov 24
api.go
1package prose
2
3import (
4 "bytes"
5 "fmt"
6 "html/template"
7 "net/http"
8 "net/url"
9 "os"
10 "strconv"
11 "strings"
12 "time"
13
14 "slices"
15
16 "github.com/gorilla/feeds"
17 "github.com/picosh/pico/db"
18 "github.com/picosh/pico/db/postgres"
19 "github.com/picosh/pico/imgs"
20 "github.com/picosh/pico/shared"
21 "github.com/picosh/pico/shared/storage"
22 "github.com/picosh/utils"
23)
24
25type PageData struct {
26 Site shared.SitePageData
27}
28
29type PostItemData struct {
30 URL template.URL
31 BlogURL template.URL
32 Username string
33 Title string
34 Description string
35 PublishAtISO string
36 PublishAt string
37 UpdatedAtISO string
38 UpdatedTimeAgo string
39 Padding string
40}
41
42type BlogPageData struct {
43 Site shared.SitePageData
44 PageTitle string
45 URL template.URL
46 RSSURL template.URL
47 Username string
48 Readme *ReadmeTxt
49 Header *HeaderTxt
50 Posts []PostItemData
51 HasCSS bool
52 WithStyles bool
53 CssURL template.URL
54 HasFilter bool
55}
56
57type ReadPageData struct {
58 Site shared.SitePageData
59 NextPage string
60 PrevPage string
61 Posts []PostItemData
62 Tags []string
63 HasFilter bool
64}
65
66type PostPageData struct {
67 Site shared.SitePageData
68 PageTitle string
69 URL template.URL
70 BlogURL template.URL
71 BlogName string
72 Slug string
73 Title string
74 Description string
75 Username string
76 Contents template.HTML
77 PublishAtISO string
78 PublishAt string
79 HasCSS bool
80 WithStyles bool
81 CssURL template.URL
82 Tags []string
83 Image template.URL
84 ImageCard string
85 Footer template.HTML
86 Favicon template.URL
87 Unlisted bool
88 Diff template.HTML
89}
90
91type HeaderTxt struct {
92 Title string
93 Bio string
94 Nav []shared.Link
95 HasLinks bool
96 Layout string
97 Image template.URL
98 ImageCard string
99 Favicon template.URL
100 WithStyles bool
101}
102
103type ReadmeTxt struct {
104 HasText bool
105 Contents template.HTML
106}
107
108func GetPostTitle(post *db.Post) string {
109 if post.Description == "" {
110 return post.Title
111 }
112
113 return fmt.Sprintf("%s: %s", post.Title, post.Description)
114}
115
116func GetBlogName(username string) string {
117 return fmt.Sprintf("%s's blog", username)
118}
119
120func blogStyleHandler(w http.ResponseWriter, r *http.Request) {
121 username := shared.GetUsernameFromRequest(r)
122 dbpool := shared.GetDB(r)
123 logger := shared.GetLogger(r)
124 cfg := shared.GetCfg(r)
125
126 user, err := dbpool.FindUserForName(username)
127 if err != nil {
128 logger.Info("blog not found", "user", username)
129 http.Error(w, "blog not found", http.StatusNotFound)
130 return
131 }
132 styles, err := dbpool.FindPostWithFilename("_styles.css", user.ID, cfg.Space)
133 if err != nil {
134 logger.Info("css not found", "user", username)
135 http.Error(w, "css not found", http.StatusNotFound)
136 return
137 }
138
139 w.Header().Add("Content-Type", "text/css")
140
141 _, err = w.Write([]byte(styles.Text))
142 if err != nil {
143 logger.Error(err.Error())
144 http.Error(w, "server error", 500)
145 }
146}
147
148func blogHandler(w http.ResponseWriter, r *http.Request) {
149 username := shared.GetUsernameFromRequest(r)
150 dbpool := shared.GetDB(r)
151 logger := shared.GetLogger(r)
152 cfg := shared.GetCfg(r)
153
154 user, err := dbpool.FindUserForName(username)
155 if err != nil {
156 logger.Info("blog not found", "user", username)
157 http.Error(w, "blog not found", http.StatusNotFound)
158 return
159 }
160
161 tag := r.URL.Query().Get("tag")
162 pager := &db.Pager{Num: 250, Page: 0}
163 var posts []*db.Post
164 var p *db.Paginate[*db.Post]
165 if tag == "" {
166 p, err = dbpool.FindPostsForUser(pager, user.ID, cfg.Space)
167 } else {
168 p, err = dbpool.FindUserPostsByTag(pager, tag, user.ID, cfg.Space)
169 }
170 posts = p.Data
171
172 byUpdated := strings.Contains(r.URL.Path, "live")
173 if byUpdated {
174 slices.SortFunc(posts, func(a *db.Post, b *db.Post) int {
175 return b.UpdatedAt.Compare(*a.UpdatedAt)
176 })
177 }
178
179 if err != nil {
180 logger.Error(err.Error())
181 http.Error(w, "could not fetch posts for blog", http.StatusInternalServerError)
182 return
183 }
184
185 ts, err := shared.RenderTemplate(cfg, []string{
186 cfg.StaticPath("html/blog-default.partial.tmpl"),
187 cfg.StaticPath("html/blog-aside.partial.tmpl"),
188 cfg.StaticPath("html/blog.page.tmpl"),
189 })
190
191 curl := shared.CreateURLFromRequest(cfg, r)
192
193 if err != nil {
194 logger.Error(err.Error())
195 http.Error(w, err.Error(), http.StatusInternalServerError)
196 return
197 }
198
199 headerTxt := &HeaderTxt{
200 Title: GetBlogName(username),
201 Bio: "",
202 Layout: "default",
203 ImageCard: "summary",
204 WithStyles: true,
205 }
206 readmeTxt := &ReadmeTxt{}
207
208 readme, err := dbpool.FindPostWithFilename("_readme.md", user.ID, cfg.Space)
209 if err == nil {
210 parsedText, err := shared.ParseText(readme.Text)
211 if err != nil {
212 logger.Error(err.Error())
213 }
214 headerTxt.Bio = parsedText.Description
215 headerTxt.Layout = parsedText.Layout
216 headerTxt.Image = template.URL(parsedText.Image)
217 headerTxt.ImageCard = parsedText.ImageCard
218 headerTxt.WithStyles = parsedText.WithStyles
219 headerTxt.Favicon = template.URL(parsedText.Favicon)
220 if parsedText.Title != "" {
221 headerTxt.Title = parsedText.Title
222 }
223
224 headerTxt.Nav = []shared.Link{}
225 for _, nav := range parsedText.Nav {
226 u, _ := url.Parse(nav.URL)
227 finURL := nav.URL
228 if !u.IsAbs() {
229 finURL = cfg.FullPostURL(
230 curl,
231 readme.Username,
232 nav.URL,
233 )
234 }
235 headerTxt.Nav = append(headerTxt.Nav, shared.Link{
236 URL: finURL,
237 Text: nav.Text,
238 })
239 }
240
241 readmeTxt.Contents = template.HTML(parsedText.Html)
242 if len(readmeTxt.Contents) > 0 {
243 readmeTxt.HasText = true
244 }
245 }
246
247 hasCSS := false
248 _, err = dbpool.FindPostWithFilename("_styles.css", user.ID, cfg.Space)
249 if err == nil {
250 hasCSS = true
251 }
252
253 postCollection := make([]PostItemData, 0, len(posts))
254 for _, post := range posts {
255 p := PostItemData{
256 URL: template.URL(cfg.FullPostURL(curl, post.Username, post.Slug)),
257 BlogURL: template.URL(cfg.FullBlogURL(curl, post.Username)),
258 Title: utils.FilenameToTitle(post.Filename, post.Title),
259 PublishAt: post.PublishAt.Format(time.DateOnly),
260 PublishAtISO: post.PublishAt.Format(time.RFC3339),
261 UpdatedTimeAgo: utils.TimeAgo(post.UpdatedAt),
262 UpdatedAtISO: post.UpdatedAt.Format(time.RFC3339),
263 }
264 postCollection = append(postCollection, p)
265 }
266
267 data := BlogPageData{
268 Site: *cfg.GetSiteData(),
269 PageTitle: headerTxt.Title,
270 URL: template.URL(cfg.FullBlogURL(curl, username)),
271 RSSURL: template.URL(cfg.RssBlogURL(curl, username, tag)),
272 Readme: readmeTxt,
273 Header: headerTxt,
274 Username: username,
275 Posts: postCollection,
276 HasCSS: hasCSS,
277 CssURL: template.URL(cfg.CssURL(username)),
278 HasFilter: tag != "",
279 WithStyles: headerTxt.WithStyles,
280 }
281
282 err = ts.Execute(w, data)
283 if err != nil {
284 logger.Error(err.Error())
285 http.Error(w, err.Error(), http.StatusInternalServerError)
286 }
287}
288
289func postRawHandler(w http.ResponseWriter, r *http.Request) {
290 username := shared.GetUsernameFromRequest(r)
291 subdomain := shared.GetSubdomain(r)
292 cfg := shared.GetCfg(r)
293
294 var slug string
295 if !cfg.IsSubdomains() || subdomain == "" {
296 slug, _ = url.PathUnescape(shared.GetField(r, 1))
297 } else {
298 slug, _ = url.PathUnescape(shared.GetField(r, 0))
299 }
300 slug = strings.TrimSuffix(slug, "/")
301
302 dbpool := shared.GetDB(r)
303 logger := shared.GetLogger(r)
304
305 user, err := dbpool.FindUserForName(username)
306 if err != nil {
307 logger.Info("blog not found", "user", username)
308 http.Error(w, "blog not found", http.StatusNotFound)
309 return
310 }
311
312 post, err := dbpool.FindPostWithSlug(slug, user.ID, cfg.Space)
313 if err != nil {
314 logger.Info("post not found")
315 http.Error(w, "post not found", http.StatusNotFound)
316 return
317 }
318
319 w.Header().Add("Content-Type", "text/plain")
320
321 _, err = w.Write([]byte(post.Text))
322 if err != nil {
323 logger.Error(err.Error())
324 http.Error(w, "server error", 500)
325 }
326}
327
328func postHandler(w http.ResponseWriter, r *http.Request) {
329 username := shared.GetUsernameFromRequest(r)
330 subdomain := shared.GetSubdomain(r)
331 cfg := shared.GetCfg(r)
332
333 var slug string
334 if !cfg.IsSubdomains() || subdomain == "" {
335 slug, _ = url.PathUnescape(shared.GetField(r, 1))
336 } else {
337 slug, _ = url.PathUnescape(shared.GetField(r, 0))
338 }
339 slug = strings.TrimSuffix(slug, "/")
340
341 dbpool := shared.GetDB(r)
342 logger := shared.GetLogger(r)
343
344 user, err := dbpool.FindUserForName(username)
345 if err != nil {
346 logger.Info("blog not found", "user", username)
347 http.Error(w, "blog not found", http.StatusNotFound)
348 return
349 }
350
351 blogName := GetBlogName(username)
352 curl := shared.CreateURLFromRequest(cfg, r)
353
354 favicon := ""
355 ogImage := ""
356 ogImageCard := ""
357 hasCSS := false
358 withStyles := true
359 var data PostPageData
360
361 css, err := dbpool.FindPostWithFilename("_styles.css", user.ID, cfg.Space)
362 if err == nil {
363 if len(css.Text) > 0 {
364 hasCSS = true
365 }
366 }
367
368 footer, err := dbpool.FindPostWithFilename("_footer.md", user.ID, cfg.Space)
369 var footerHTML template.HTML
370 if err == nil {
371 footerParsed, err := shared.ParseText(footer.Text)
372 if err != nil {
373 logger.Error(err.Error())
374 }
375 footerHTML = template.HTML(footerParsed.Html)
376 }
377
378 // we need the blog name from the readme unfortunately
379 readme, err := dbpool.FindPostWithFilename("_readme.md", user.ID, cfg.Space)
380 if err == nil {
381 readmeParsed, err := shared.ParseText(readme.Text)
382 if err != nil {
383 logger.Error(err.Error())
384 }
385 if readmeParsed.MetaData.Title != "" {
386 blogName = readmeParsed.MetaData.Title
387 }
388 withStyles = readmeParsed.WithStyles
389 ogImage = readmeParsed.Image
390 ogImageCard = readmeParsed.ImageCard
391 favicon = readmeParsed.Favicon
392 }
393
394 diff := ""
395 post, err := dbpool.FindPostWithSlug(slug, user.ID, cfg.Space)
396 if err == nil {
397 parsedText, err := shared.ParseText(post.Text)
398 if err != nil {
399 logger.Error(err.Error())
400 }
401
402 if parsedText.Image != "" {
403 ogImage = parsedText.Image
404 }
405
406 if parsedText.ImageCard != "" {
407 ogImageCard = parsedText.ImageCard
408 }
409
410 unlisted := false
411 if post.Hidden || post.PublishAt.After(time.Now()) {
412 unlisted = true
413 }
414
415 data = PostPageData{
416 Site: *cfg.GetSiteData(),
417 PageTitle: GetPostTitle(post),
418 URL: template.URL(cfg.FullPostURL(curl, post.Username, post.Slug)),
419 BlogURL: template.URL(cfg.FullBlogURL(curl, username)),
420 Description: post.Description,
421 Title: utils.FilenameToTitle(post.Filename, post.Title),
422 Slug: post.Slug,
423 PublishAt: post.PublishAt.Format(time.DateOnly),
424 PublishAtISO: post.PublishAt.Format(time.RFC3339),
425 Username: username,
426 BlogName: blogName,
427 Contents: template.HTML(parsedText.Html),
428 HasCSS: hasCSS,
429 CssURL: template.URL(cfg.CssURL(username)),
430 Tags: parsedText.Tags,
431 Image: template.URL(ogImage),
432 ImageCard: ogImageCard,
433 Favicon: template.URL(favicon),
434 Footer: footerHTML,
435 Unlisted: unlisted,
436 Diff: template.HTML(diff),
437 WithStyles: withStyles,
438 }
439 } else {
440 // TODO: HACK to support imgs slugs inside prose
441 // We definitely want to kill this feature in time
442 imgPost, err := imgs.FindImgPost(r, user, slug)
443 if err == nil && imgPost != nil {
444 imgs.ImgRequest(w, r)
445 return
446 }
447
448 notFound, err := dbpool.FindPostWithFilename("_404.md", user.ID, cfg.Space)
449 contents := template.HTML("Oops! we can't seem to find this post.")
450 title := "Post not found"
451 desc := "Post not found"
452 if err == nil {
453 notFoundParsed, err := shared.ParseText(notFound.Text)
454 if err != nil {
455 logger.Error(err.Error())
456 }
457 if notFoundParsed.MetaData.Title != "" {
458 title = notFoundParsed.MetaData.Title
459 }
460 if notFoundParsed.MetaData.Description != "" {
461 desc = notFoundParsed.MetaData.Description
462 }
463 ogImage = notFoundParsed.Image
464 ogImageCard = notFoundParsed.ImageCard
465 favicon = notFoundParsed.Favicon
466 contents = template.HTML(notFoundParsed.Html)
467 }
468
469 data = PostPageData{
470 Site: *cfg.GetSiteData(),
471 BlogURL: template.URL(cfg.FullBlogURL(curl, username)),
472 PageTitle: title,
473 Description: desc,
474 Title: title,
475 PublishAt: time.Now().Format(time.DateOnly),
476 PublishAtISO: time.Now().Format(time.RFC3339),
477 Username: username,
478 BlogName: blogName,
479 HasCSS: hasCSS,
480 CssURL: template.URL(cfg.CssURL(username)),
481 Image: template.URL(ogImage),
482 ImageCard: ogImageCard,
483 Favicon: template.URL(favicon),
484 Footer: footerHTML,
485 Contents: contents,
486 Unlisted: true,
487 }
488 logger.Info("post not found", "user", username, "slug", slug)
489 w.WriteHeader(http.StatusNotFound)
490 }
491
492 ts, err := shared.RenderTemplate(cfg, []string{
493 cfg.StaticPath("html/post.page.tmpl"),
494 })
495
496 if err != nil {
497 http.Error(w, err.Error(), http.StatusInternalServerError)
498 }
499
500 err = ts.Execute(w, data)
501 if err != nil {
502 logger.Error(err.Error())
503 http.Error(w, err.Error(), http.StatusInternalServerError)
504 }
505}
506
507func readHandler(w http.ResponseWriter, r *http.Request) {
508 dbpool := shared.GetDB(r)
509 logger := shared.GetLogger(r)
510 cfg := shared.GetCfg(r)
511
512 page, _ := strconv.Atoi(r.URL.Query().Get("page"))
513 tag := r.URL.Query().Get("tag")
514 var pager *db.Paginate[*db.Post]
515 var err error
516 if tag == "" {
517 pager, err = dbpool.FindAllPosts(&db.Pager{Num: 30, Page: page}, cfg.Space)
518 } else {
519 pager, err = dbpool.FindPostsByTag(&db.Pager{Num: 30, Page: page}, tag, cfg.Space)
520 }
521
522 if err != nil {
523 logger.Error(err.Error())
524 http.Error(w, err.Error(), http.StatusInternalServerError)
525 return
526 }
527
528 ts, err := shared.RenderTemplate(cfg, []string{
529 cfg.StaticPath("html/read.page.tmpl"),
530 })
531
532 if err != nil {
533 http.Error(w, err.Error(), http.StatusInternalServerError)
534 }
535
536 nextPage := ""
537 if page < pager.Total-1 {
538 nextPage = fmt.Sprintf("/read?page=%d", page+1)
539 if tag != "" {
540 nextPage = fmt.Sprintf("%s&tag=%s", nextPage, tag)
541 }
542 }
543
544 prevPage := ""
545 if page > 0 {
546 prevPage = fmt.Sprintf("/read?page=%d", page-1)
547 if tag != "" {
548 prevPage = fmt.Sprintf("%s&tag=%s", prevPage, tag)
549 }
550 }
551
552 tags, err := dbpool.FindPopularTags(cfg.Space)
553 if err != nil {
554 logger.Error(err.Error())
555 }
556
557 data := ReadPageData{
558 Site: *cfg.GetSiteData(),
559 NextPage: nextPage,
560 PrevPage: prevPage,
561 Tags: tags,
562 HasFilter: tag != "",
563 }
564
565 curl := shared.NewCreateURL(cfg)
566 for _, post := range pager.Data {
567 item := PostItemData{
568 URL: template.URL(cfg.FullPostURL(curl, post.Username, post.Slug)),
569 BlogURL: template.URL(cfg.FullBlogURL(curl, post.Username)),
570 Title: utils.FilenameToTitle(post.Filename, post.Title),
571 Description: post.Description,
572 Username: post.Username,
573 PublishAt: post.PublishAt.Format(time.DateOnly),
574 PublishAtISO: post.PublishAt.Format(time.RFC3339),
575 UpdatedTimeAgo: utils.TimeAgo(post.UpdatedAt),
576 UpdatedAtISO: post.UpdatedAt.Format(time.RFC3339),
577 }
578 data.Posts = append(data.Posts, item)
579 }
580
581 err = ts.Execute(w, data)
582 if err != nil {
583 logger.Error(err.Error())
584 http.Error(w, err.Error(), http.StatusInternalServerError)
585 }
586}
587
588func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
589 username := shared.GetUsernameFromRequest(r)
590 dbpool := shared.GetDB(r)
591 logger := shared.GetLogger(r)
592 cfg := shared.GetCfg(r)
593
594 user, err := dbpool.FindUserForName(username)
595 if err != nil {
596 logger.Info("rss feed not found", "user", username)
597 http.Error(w, "rss feed not found", http.StatusNotFound)
598 return
599 }
600
601 tag := r.URL.Query().Get("tag")
602 pager := &db.Pager{Num: 10, Page: 0}
603 var posts []*db.Post
604 var p *db.Paginate[*db.Post]
605 if tag == "" {
606 p, err = dbpool.FindPostsForUser(pager, user.ID, cfg.Space)
607 } else {
608 p, err = dbpool.FindUserPostsByTag(pager, tag, user.ID, cfg.Space)
609 }
610 posts = p.Data
611
612 if err != nil {
613 logger.Error(err.Error())
614 http.Error(w, err.Error(), http.StatusInternalServerError)
615 return
616 }
617
618 ts, err := template.ParseFiles(cfg.StaticPath("html/rss.page.tmpl"))
619 if err != nil {
620 logger.Error(err.Error())
621 http.Error(w, err.Error(), http.StatusInternalServerError)
622 return
623 }
624
625 headerTxt := &HeaderTxt{
626 Title: GetBlogName(username),
627 }
628
629 readme, err := dbpool.FindPostWithFilename("_readme.md", user.ID, cfg.Space)
630 if err == nil {
631 parsedText, err := shared.ParseText(readme.Text)
632 if err != nil {
633 logger.Error(err.Error())
634 }
635 if parsedText.Title != "" {
636 headerTxt.Title = parsedText.Title
637 }
638
639 if parsedText.Description != "" {
640 headerTxt.Bio = parsedText.Description
641 }
642 }
643
644 curl := shared.CreateURLFromRequest(cfg, r)
645 blogUrl := cfg.FullBlogURL(curl, username)
646
647 byUpdated := strings.Contains(r.URL.Path, "live")
648 if byUpdated {
649 slices.SortFunc(posts, func(a *db.Post, b *db.Post) int {
650 return b.UpdatedAt.Compare(*a.UpdatedAt)
651 })
652 }
653
654 feed := &feeds.Feed{
655 Id: blogUrl,
656 Title: headerTxt.Title,
657 Link: &feeds.Link{Href: blogUrl},
658 Description: headerTxt.Bio,
659 Author: &feeds.Author{Name: username},
660 Created: *user.CreatedAt,
661 }
662
663 var feedItems []*feeds.Item
664 for _, post := range posts {
665 if slices.Contains(cfg.HiddenPosts, post.Filename) {
666 continue
667 }
668 parsed, err := shared.ParseText(post.Text)
669 if err != nil {
670 logger.Error(err.Error())
671 }
672
673 footer, err := dbpool.FindPostWithFilename("_footer.md", user.ID, cfg.Space)
674 var footerHTML string
675 if err == nil {
676 footerParsed, err := shared.ParseText(footer.Text)
677 if err != nil {
678 logger.Error(err.Error())
679 }
680 footerHTML = footerParsed.Html
681 }
682
683 var tpl bytes.Buffer
684 data := &PostPageData{
685 Contents: template.HTML(parsed.Html + footerHTML),
686 }
687 if err := ts.Execute(&tpl, data); err != nil {
688 continue
689 }
690
691 realUrl := cfg.FullPostURL(curl, post.Username, post.Slug)
692 feedId := realUrl
693
694 if byUpdated {
695 feedId = fmt.Sprintf("%s:%s", realUrl, post.UpdatedAt.Format(time.RFC3339))
696 }
697
698 item := &feeds.Item{
699 Id: feedId,
700 Title: utils.FilenameToTitle(post.Filename, post.Title),
701 Link: &feeds.Link{Href: realUrl},
702 Content: tpl.String(),
703 Updated: *post.UpdatedAt,
704 Created: *post.CreatedAt,
705 Description: post.Description,
706 }
707
708 if post.Description != "" {
709 item.Description = post.Description
710 }
711
712 feedItems = append(feedItems, item)
713 }
714 feed.Items = feedItems
715
716 rss, err := feed.ToAtom()
717 if err != nil {
718 logger.Error(err.Error())
719 http.Error(w, "Could not generate atom rss feed", http.StatusInternalServerError)
720 }
721
722 w.Header().Add("Content-Type", "application/atom+xml; charset=utf-8")
723 _, err = w.Write([]byte(rss))
724 if err != nil {
725 logger.Error(err.Error())
726 }
727}
728
729func rssHandler(w http.ResponseWriter, r *http.Request) {
730 dbpool := shared.GetDB(r)
731 logger := shared.GetLogger(r)
732 cfg := shared.GetCfg(r)
733
734 pager, err := dbpool.FindAllPosts(&db.Pager{Num: 25, Page: 0}, cfg.Space)
735 if err != nil {
736 logger.Error(err.Error())
737 http.Error(w, err.Error(), http.StatusInternalServerError)
738 return
739 }
740
741 ts, err := template.ParseFiles(cfg.StaticPath("html/rss.page.tmpl"))
742 if err != nil {
743 logger.Error(err.Error())
744 http.Error(w, err.Error(), http.StatusInternalServerError)
745 return
746 }
747
748 feed := &feeds.Feed{
749 Title: fmt.Sprintf("%s discovery feed", cfg.Domain),
750 Link: &feeds.Link{Href: cfg.ReadURL()},
751 Description: fmt.Sprintf("%s latest posts", cfg.Domain),
752 Author: &feeds.Author{Name: cfg.Domain},
753 Created: time.Now(),
754 }
755
756 curl := shared.CreateURLFromRequest(cfg, r)
757
758 var feedItems []*feeds.Item
759 for _, post := range pager.Data {
760 parsed, err := shared.ParseText(post.Text)
761 if err != nil {
762 logger.Error(err.Error())
763 }
764
765 var tpl bytes.Buffer
766 data := &PostPageData{
767 Contents: template.HTML(parsed.Html),
768 }
769 if err := ts.Execute(&tpl, data); err != nil {
770 continue
771 }
772
773 realUrl := cfg.FullPostURL(curl, post.Username, post.Slug)
774 if !curl.Subdomain && !curl.UsernameInRoute {
775 realUrl = fmt.Sprintf("%s://%s%s", cfg.Protocol, r.Host, realUrl)
776 }
777
778 item := &feeds.Item{
779 Id: realUrl,
780 Title: post.Title,
781 Link: &feeds.Link{Href: realUrl},
782 Content: tpl.String(),
783 Created: *post.PublishAt,
784 Updated: *post.UpdatedAt,
785 Description: post.Description,
786 Author: &feeds.Author{Name: post.Username},
787 }
788
789 if post.Description != "" {
790 item.Description = post.Description
791 }
792
793 feedItems = append(feedItems, item)
794 }
795 feed.Items = feedItems
796
797 rss, err := feed.ToAtom()
798 if err != nil {
799 logger.Error(err.Error())
800 http.Error(w, "Could not generate atom rss feed", http.StatusInternalServerError)
801 }
802
803 w.Header().Add("Content-Type", "application/atom+xml; charset=utf-8")
804 _, err = w.Write([]byte(rss))
805 if err != nil {
806 logger.Error(err.Error())
807 }
808}
809
810func serveFile(file string, contentType string) http.HandlerFunc {
811 return func(w http.ResponseWriter, r *http.Request) {
812 logger := shared.GetLogger(r)
813 cfg := shared.GetCfg(r)
814
815 contents, err := os.ReadFile(cfg.StaticPath(fmt.Sprintf("public/%s", file)))
816 if err != nil {
817 logger.Error(err.Error())
818 http.Error(w, "file not found", 404)
819 }
820 w.Header().Add("Content-Type", contentType)
821
822 _, err = w.Write(contents)
823 if err != nil {
824 logger.Error(err.Error())
825 http.Error(w, "server error", 500)
826 }
827 }
828}
829
830func createStaticRoutes() []shared.Route {
831 return []shared.Route{
832 shared.NewRoute("GET", "/main.css", serveFile("main.css", "text/css")),
833 shared.NewRoute("GET", "/smol.css", serveFile("smol.css", "text/css")),
834 shared.NewRoute("GET", "/smol-v2.css", serveFile("smol-v2.css", "text/css")),
835 shared.NewRoute("GET", "/syntax.css", serveFile("syntax.css", "text/css")),
836 shared.NewRoute("GET", "/card.png", serveFile("card.png", "image/png")),
837 shared.NewRoute("GET", "/favicon-16x16.png", serveFile("favicon-16x16.png", "image/png")),
838 shared.NewRoute("GET", "/favicon-32x32.png", serveFile("favicon-32x32.png", "image/png")),
839 shared.NewRoute("GET", "/apple-touch-icon.png", serveFile("apple-touch-icon.png", "image/png")),
840 shared.NewRoute("GET", "/favicon.ico", serveFile("favicon.ico", "image/x-icon")),
841 shared.NewRoute("GET", "/robots.txt", serveFile("robots.txt", "text/plain")),
842 }
843}
844
845func createMainRoutes(staticRoutes []shared.Route) []shared.Route {
846 routes := []shared.Route{
847 shared.NewRoute("GET", "/", readHandler),
848 shared.NewRoute("GET", "/read", readHandler),
849 shared.NewRoute("GET", "/check", shared.CheckHandler),
850 shared.NewRoute("GET", "/rss", rssHandler),
851 }
852
853 routes = append(
854 routes,
855 staticRoutes...,
856 )
857
858 return routes
859}
860
861func createSubdomainRoutes(staticRoutes []shared.Route) []shared.Route {
862 routes := []shared.Route{
863 shared.NewRoute("GET", "/", blogHandler),
864 shared.NewRoute("GET", "/live", blogHandler),
865 shared.NewRoute("GET", "/_styles.css", blogStyleHandler),
866 shared.NewRoute("GET", "/rss", rssBlogHandler),
867 shared.NewRoute("GET", "/live/rss", rssBlogHandler),
868 shared.NewRoute("GET", "/rss.xml", rssBlogHandler),
869 shared.NewRoute("GET", "/atom.xml", rssBlogHandler),
870 shared.NewRoute("GET", "/feed.xml", rssBlogHandler),
871 shared.NewRoute("GET", "/atom", rssBlogHandler),
872 shared.NewRoute("GET", "/blog/index.xml", rssBlogHandler),
873 }
874
875 routes = append(
876 routes,
877 staticRoutes...,
878 )
879
880 routes = append(
881 routes,
882 shared.NewRoute("GET", "/raw/(.+)", postRawHandler),
883 shared.NewRoute("GET", "/([^/]+)/(.+)", imgs.ImgRequest),
884 shared.NewRoute("GET", "/(.+.(?:jpg|jpeg|png|gif|webp|svg))$", imgs.ImgRequest),
885 shared.NewRoute("GET", "/i", imgs.ImgsListHandler),
886 shared.NewRoute("GET", "/(.+)", postHandler),
887 )
888
889 return routes
890}
891
892func StartApiServer() {
893 cfg := NewConfigSite()
894 dbpool := postgres.NewDB(cfg.DbURL, cfg.Logger)
895 defer dbpool.Close()
896 logger := cfg.Logger
897
898 var st storage.StorageServe
899 var err error
900 if cfg.MinioURL == "" {
901 st, err = storage.NewStorageFS(cfg.StorageDir)
902 } else {
903 st, err = storage.NewStorageMinio(cfg.MinioURL, cfg.MinioUser, cfg.MinioPass)
904 }
905
906 if err != nil {
907 logger.Error(err.Error())
908 }
909
910 staticRoutes := createStaticRoutes()
911
912 if cfg.Debug {
913 staticRoutes = shared.CreatePProfRoutes(staticRoutes)
914 }
915
916 mainRoutes := createMainRoutes(staticRoutes)
917 subdomainRoutes := createSubdomainRoutes(staticRoutes)
918
919 apiConfig := &shared.ApiConfig{
920 Cfg: cfg,
921 Dbpool: dbpool,
922 Storage: st,
923 }
924 handler := shared.CreateServe(mainRoutes, subdomainRoutes, apiConfig)
925 router := http.HandlerFunc(handler)
926
927 portStr := fmt.Sprintf(":%s", cfg.Port)
928 logger.Info(
929 "Starting server on port",
930 "port", cfg.Port,
931 "domain", cfg.Domain,
932 )
933
934 logger.Error(http.ListenAndServe(portStr, router).Error())
935}