Added atom feed and a few other modifications
This commit is contained in:
parent
60b5d07d00
commit
5d9b628144
|
@ -18,6 +18,16 @@ type Q struct {
|
||||||
Value string
|
Value string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func QFromURL(u *url.URL) []Q {
|
||||||
|
var result []Q
|
||||||
|
for key, values := range u.Query() {
|
||||||
|
for _, v := range values {
|
||||||
|
result = append(result, Q{Name: key, Value: v})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
var baseUrlParsed url.URL
|
var baseUrlParsed url.URL
|
||||||
var cacheBust string
|
var cacheBust string
|
||||||
var isTest bool
|
var isTest bool
|
||||||
|
|
|
@ -50,6 +50,8 @@ func TestSiteMap(t *testing.T) {
|
||||||
|
|
||||||
func TestAtomFeed(t *testing.T) {
|
func TestAtomFeed(t *testing.T) {
|
||||||
AssertRegexMatch(t, BuildAtomFeed(), RegexAtomFeed, nil)
|
AssertRegexMatch(t, BuildAtomFeed(), RegexAtomFeed, nil)
|
||||||
|
AssertRegexMatch(t, BuildAtomFeedForProjects(), RegexAtomFeed, map[string]string{"feedtype": "projects"})
|
||||||
|
AssertRegexMatch(t, BuildAtomFeedForShowcase(), RegexAtomFeed, map[string]string{"feedtype": "showcase"})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoginAction(t *testing.T) {
|
func TestLoginAction(t *testing.T) {
|
||||||
|
|
|
@ -57,13 +57,6 @@ func BuildSiteMap() string {
|
||||||
return Url("/sitemap", nil)
|
return Url("/sitemap", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
var RegexAtomFeed = regexp.MustCompile("^/atom$")
|
|
||||||
|
|
||||||
func BuildAtomFeed() string {
|
|
||||||
defer CatchPanic()
|
|
||||||
return Url("/atom", nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// QUESTION(ben): Can we change these routes?
|
// QUESTION(ben): Can we change these routes?
|
||||||
|
|
||||||
var RegexLoginAction = regexp.MustCompile("^/login$")
|
var RegexLoginAction = regexp.MustCompile("^/login$")
|
||||||
|
@ -176,6 +169,23 @@ func BuildFeedWithPage(page int) string {
|
||||||
return Url("/feed/"+strconv.Itoa(page), nil)
|
return Url("/feed/"+strconv.Itoa(page), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var RegexAtomFeed = regexp.MustCompile("^/atom(/(?P<feedtype>.+))?$")
|
||||||
|
|
||||||
|
func BuildAtomFeed() string {
|
||||||
|
defer CatchPanic()
|
||||||
|
return Url("/atom", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildAtomFeedForProjects() string {
|
||||||
|
defer CatchPanic()
|
||||||
|
return Url("/atom/projects", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildAtomFeedForShowcase() string {
|
||||||
|
defer CatchPanic()
|
||||||
|
return Url("/atom/showcase", nil)
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Podcast
|
* Podcast
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -46,14 +46,16 @@ var ZerologStackMarshaler = func(err error) interface{} {
|
||||||
if asOops, ok := err.(*Error); ok {
|
if asOops, ok := err.(*Error); ok {
|
||||||
return asOops.Stack
|
return asOops.Stack
|
||||||
}
|
}
|
||||||
return nil
|
// NOTE(asaf): If we got here, it means zerolog is trying to output a non-oops error.
|
||||||
|
// We remove this call and the zerolog caller from the stack.
|
||||||
|
return Trace()[2:]
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(wrapped error, format string, args ...interface{}) error {
|
func New(wrapped error, format string, args ...interface{}) error {
|
||||||
return &Error{
|
return &Error{
|
||||||
Message: fmt.Sprintf(format, args...),
|
Message: fmt.Sprintf(format, args...),
|
||||||
Wrapped: wrapped,
|
Wrapped: wrapped,
|
||||||
Stack: Trace(),
|
Stack: Trace()[1:], // NOTE(asaf): Remove the call to New from the stack
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -102,7 +102,7 @@ func UserToTemplate(u *models.User, currentTheme string) User {
|
||||||
Blurb: u.Blurb,
|
Blurb: u.Blurb,
|
||||||
Signature: u.Signature,
|
Signature: u.Signature,
|
||||||
AvatarUrl: avatar,
|
AvatarUrl: avatar,
|
||||||
ProfileUrl: hmnurl.Url("m/"+u.Username, nil),
|
ProfileUrl: hmnurl.BuildMember(u.Username),
|
||||||
|
|
||||||
DarkTheme: u.DarkTheme,
|
DarkTheme: u.DarkTheme,
|
||||||
Timezone: u.Timezone,
|
Timezone: u.Timezone,
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
{{ noescape "<?xml version=\"1.0\" encoding=\"utf-8\"?>" }}
|
||||||
|
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||||
|
<title type="text">{{ .Title }}</title>
|
||||||
|
<subtitle type="html">{{ .Subtitle }}</subtitle>
|
||||||
|
<link href="{{ .HomepageUrl }}"/>
|
||||||
|
<link rel="self" type="application/atom+xml" href="{{ .AtomFeedUrl }}"/>
|
||||||
|
<link rel="alternate" type="text/html" hreflang="en" href="{{ .FeedUrl }}"/>
|
||||||
|
<rights type="html">{{ .CopyrightStatement }}</rights>
|
||||||
|
<generator uri="{{ .HomepageUrl }}" version="{{ .SiteVersion }}">Handmade Network site engine v{{ .SiteVersion }}</generator>
|
||||||
|
<updated>{{ rfc3339 .Updated }}</updated>
|
||||||
|
<id>{{ .FeedID }}</id>
|
||||||
|
{{ if .Posts }}
|
||||||
|
{{ range .Posts }}
|
||||||
|
<entry>
|
||||||
|
<title>{{ if .PostTypePrefix }}{{ .PostTypePrefix }}: {{ end }}{{ .Title }}</title>
|
||||||
|
<link rel="alternate" type="text/html" href="{{ .Url }}" />
|
||||||
|
<id>{{ .UUID }}</id>
|
||||||
|
<published>{{ rfc3339 .Date }}</published>
|
||||||
|
<updated>{{ rfc3339 .LastEditDate }}</updated>
|
||||||
|
<author>
|
||||||
|
<name>{{ .User.Name }}</name>
|
||||||
|
<uri>{{ .User.ProfileUrl }}</uri>
|
||||||
|
</author>
|
||||||
|
<summary type="html">{{ .Preview }}</summary>
|
||||||
|
</entry>
|
||||||
|
{{ end }}
|
||||||
|
{{ else if .Projects }}
|
||||||
|
{{ range .Projects }}
|
||||||
|
{{ end }}
|
||||||
|
{{ else if .Snippets }}
|
||||||
|
{{ range .Snippets }}
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
</feed>
|
|
@ -20,7 +20,29 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{ range .Posts }}
|
{{ range .Posts }}
|
||||||
{{ template "post_list_item.html" . }}
|
<div class="post-list-item flex items-center ph3 pv2 {{ if .Unread }}unread{{ else }}read{{ end }} {{ .Classes }}">
|
||||||
|
<img class="avatar-icon mr2" src="{{ .User.AvatarUrl }}">
|
||||||
|
<div class="flex-grow-1 overflow-hidden">
|
||||||
|
<div class="breadcrumbs">
|
||||||
|
{{ range $i, $breadcrumb := .Breadcrumbs }}
|
||||||
|
{{ if gt $i 0 }} » {{ end }}
|
||||||
|
<a href="{{ $breadcrumb.Url }}">{{ $breadcrumb.Name }}</a>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
<div class="title nowrap truncate"><a href="{{ .Url }}" title="{{ .Preview }}">{{ if .PostTypePrefix }}{{ .PostTypePrefix }}: {{ end }}{{ .Title }}</a></div>
|
||||||
|
<div class="details">
|
||||||
|
<a class="user" href="{{ .User.ProfileUrl }}">{{ .User.Name }}</a> — {{ timehtml (relativedate .Date) .Date }}
|
||||||
|
</div>
|
||||||
|
{{ with .Preview }}
|
||||||
|
<div class="mt1">
|
||||||
|
{{ noescape . }}
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
<div class="goto">
|
||||||
|
<a href="{{ .Url }}">»</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
<div class="optionbar bottom">
|
<div class="optionbar bottom">
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -13,15 +13,10 @@ It should be called with PostListItem.
|
||||||
<a href="{{ $breadcrumb.Url }}">{{ $breadcrumb.Name }}</a>
|
<a href="{{ $breadcrumb.Url }}">{{ $breadcrumb.Name }}</a>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
<div class="title nowrap truncate"><a href="{{ .Url }}">{{ .Title }}</a></div>
|
<div class="title nowrap truncate"><a href="{{ .Url }}" title="{{ .Preview }}">{{ .Title }}</a></div>
|
||||||
<div class="details">
|
<div class="details">
|
||||||
<a class="user" href="{{ .User.ProfileUrl }}">{{ .User.Name }}</a> — {{ timehtml (relativedate .Date) .Date }}
|
<a class="user" href="{{ .User.ProfileUrl }}">{{ .User.Name }}</a> — {{ timehtml (relativedate .Date) .Date }}
|
||||||
</div>
|
</div>
|
||||||
{{ with .Content }}
|
|
||||||
<div class="mt2">
|
|
||||||
{{ . }}
|
|
||||||
</div>
|
|
||||||
{{ end }}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="goto">
|
<div class="goto">
|
||||||
<a href="{{ .Url }}">»</a>
|
<a href="{{ .Url }}">»</a>
|
||||||
|
|
|
@ -104,7 +104,7 @@
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
{{/*
|
{{/* TODO(asaf): Delete this section once we're done with the landing page
|
||||||
{% block columns %}
|
{% block columns %}
|
||||||
{% include "showcase/js_templates.html" %}
|
{% include "showcase/js_templates.html" %}
|
||||||
{% include "timeline/js_templates.html" %}
|
{% include "timeline/js_templates.html" %}
|
||||||
|
@ -268,7 +268,7 @@
|
||||||
{{/* Call this template with a LandingPageFeaturedPost. */}}
|
{{/* Call this template with a LandingPageFeaturedPost. */}}
|
||||||
<div class="flex items-start ph3 pv2 {{ if .Unread }}unread{{ else }}read{{ end }}">
|
<div class="flex items-start ph3 pv2 {{ if .Unread }}unread{{ else }}read{{ end }}">
|
||||||
<img class="avatar-icon mr2" src="{{ .User.AvatarUrl }}">
|
<img class="avatar-icon mr2" src="{{ .User.AvatarUrl }}">
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1 overflow-hidden">
|
||||||
<div class="title mb1"><a href="{{ .Url }}">{{ .Title }}</a></div>
|
<div class="title mb1"><a href="{{ .Url }}">{{ .Title }}</a></div>
|
||||||
<div class="details">
|
<div class="details">
|
||||||
<a class="user" href="{{ .User.ProfileUrl }}">{{ .User.Name }}</a> — {{ timehtml (relativedate .Date) .Date }}
|
<a class="user" href="{{ .User.ProfileUrl }}">{{ .User.Name }}</a> — {{ timehtml (relativedate .Date) .Date }}
|
||||||
|
@ -277,7 +277,7 @@
|
||||||
<div>
|
<div>
|
||||||
{{ .Content }}
|
{{ .Content }}
|
||||||
</div>
|
</div>
|
||||||
<div class="excerpt-fade absolute w-100 h4 bottom-0"></div>
|
<div class="excerpt-fade absolute w-100 h4 bottom-0 z-999"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt2">
|
<div class="mt2">
|
||||||
<a href="{{ .Url }}">Read More →</a>
|
<a href="{{ .Url }}">Read More →</a>
|
||||||
|
@ -285,3 +285,25 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "post_list_item" }}
|
||||||
|
{{/* Call this template with a PostListItem. */}}
|
||||||
|
<div class="post-list-item flex items-center ph3 pv2 {{ if .Unread }}unread{{ else }}read{{ end }} {{ .Classes }}">
|
||||||
|
<img class="avatar-icon mr2" src="{{ .User.AvatarUrl }}">
|
||||||
|
<div class="flex-grow-1 overflow-hidden">
|
||||||
|
<div class="breadcrumbs">
|
||||||
|
{{ range $i, $breadcrumb := .Breadcrumbs }}
|
||||||
|
{{ if gt $i 0 }} » {{ end }}
|
||||||
|
<a href="{{ $breadcrumb.Url }}">{{ $breadcrumb.Name }}</a>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
<div class="title nowrap truncate"><a href="{{ .Url }}" title="{{ .Preview }}">{{ .Title }}</a></div>
|
||||||
|
<div class="details">
|
||||||
|
<a class="user" href="{{ .User.ProfileUrl }}">{{ .User.Name }}</a> — {{ timehtml (relativedate .Date) .Date }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="goto">
|
||||||
|
<a href="{{ .Url }}">»</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"embed"
|
"embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/url"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -25,11 +24,7 @@ const (
|
||||||
var templateFs embed.FS
|
var templateFs embed.FS
|
||||||
var Templates map[string]*template.Template
|
var Templates map[string]*template.Template
|
||||||
|
|
||||||
var cachebust string
|
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
cachebust = fmt.Sprint(time.Now().Unix())
|
|
||||||
|
|
||||||
Templates = make(map[string]*template.Template)
|
Templates = make(map[string]*template.Template)
|
||||||
|
|
||||||
files, _ := templateFs.ReadDir("src")
|
files, _ := templateFs.ReadDir("src")
|
||||||
|
@ -53,6 +48,16 @@ func Init() {
|
||||||
logging.Fatal().Str("filename", f.Name()).Err(err).Msg("failed to parse template")
|
logging.Fatal().Str("filename", f.Name()).Err(err).Msg("failed to parse template")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Templates[f.Name()] = t
|
||||||
|
} else if strings.HasSuffix(f.Name(), ".xml") {
|
||||||
|
t := template.New(f.Name())
|
||||||
|
t = t.Funcs(sprig.FuncMap())
|
||||||
|
t = t.Funcs(HMNTemplateFuncs)
|
||||||
|
t, err := t.ParseFS(templateFs, "src/"+f.Name())
|
||||||
|
if err != nil {
|
||||||
|
logging.Fatal().Str("filename", f.Name()).Err(err).Msg("failed to parse template")
|
||||||
|
}
|
||||||
|
|
||||||
Templates[f.Name()] = t
|
Templates[f.Name()] = t
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -68,7 +73,10 @@ func names(ts []*template.Template) []string {
|
||||||
|
|
||||||
var HMNTemplateFuncs = template.FuncMap{
|
var HMNTemplateFuncs = template.FuncMap{
|
||||||
"absolutedate": func(t time.Time) string {
|
"absolutedate": func(t time.Time) string {
|
||||||
return t.Format("January 2, 2006, 3:04pm")
|
return t.UTC().Format("January 2, 2006, 3:04pm")
|
||||||
|
},
|
||||||
|
"rfc3339": func(t time.Time) string {
|
||||||
|
return t.UTC().Format(time.RFC3339)
|
||||||
},
|
},
|
||||||
"alpha": func(alpha float64, color noire.Color) noire.Color {
|
"alpha": func(alpha float64, color noire.Color) noire.Color {
|
||||||
color.Alpha = alpha
|
color.Alpha = alpha
|
||||||
|
@ -77,9 +85,6 @@ var HMNTemplateFuncs = template.FuncMap{
|
||||||
"brighten": func(amount float64, color noire.Color) noire.Color {
|
"brighten": func(amount float64, color noire.Color) noire.Color {
|
||||||
return color.Tint(amount)
|
return color.Tint(amount)
|
||||||
},
|
},
|
||||||
"cachebust": func() string {
|
|
||||||
return cachebust
|
|
||||||
},
|
|
||||||
"color2css": func(color noire.Color) template.CSS {
|
"color2css": func(color noire.Color) template.CSS {
|
||||||
return template.CSS(color.HTML())
|
return template.CSS(color.HTML())
|
||||||
},
|
},
|
||||||
|
@ -96,20 +101,6 @@ var HMNTemplateFuncs = template.FuncMap{
|
||||||
h, s, _, a := color.HSLA()
|
h, s, _, a := color.HSLA()
|
||||||
return noire.NewHSLA(h, s, lightness*100, a)
|
return noire.NewHSLA(h, s, lightness*100, a)
|
||||||
},
|
},
|
||||||
"projecturl": func(url string, proj interface{}) string {
|
|
||||||
return hmnurl.ProjectUrl(url, nil, getProjectSubdomain(proj))
|
|
||||||
},
|
|
||||||
"projecturlq": func(url string, proj interface{}, query string) string {
|
|
||||||
absUrl := hmnurl.ProjectUrl(url, nil, getProjectSubdomain(proj))
|
|
||||||
return fmt.Sprintf("%s?%s", absUrl, query)
|
|
||||||
},
|
|
||||||
"query": func(args ...string) string {
|
|
||||||
query := url.Values{}
|
|
||||||
for i := 0; i < len(args); i += 2 {
|
|
||||||
query.Set(args[i], args[i+1])
|
|
||||||
}
|
|
||||||
return query.Encode()
|
|
||||||
},
|
|
||||||
"relativedate": func(t time.Time) string {
|
"relativedate": func(t time.Time) string {
|
||||||
// TODO: Support relative future dates
|
// TODO: Support relative future dates
|
||||||
|
|
||||||
|
@ -161,9 +152,12 @@ var HMNTemplateFuncs = template.FuncMap{
|
||||||
return hmnurl.BuildTheme(filepath, theme, false)
|
return hmnurl.BuildTheme(filepath, theme, false)
|
||||||
},
|
},
|
||||||
"timehtml": func(formatted string, t time.Time) template.HTML {
|
"timehtml": func(formatted string, t time.Time) template.HTML {
|
||||||
iso := t.Format(time.RFC3339)
|
iso := t.UTC().Format(time.RFC3339)
|
||||||
return template.HTML(fmt.Sprintf(`<time datetime="%s">%s</time>`, iso, formatted))
|
return template.HTML(fmt.Sprintf(`<time datetime="%s">%s</time>`, iso, formatted))
|
||||||
},
|
},
|
||||||
|
"noescape": func(str string) template.HTML {
|
||||||
|
return template.HTML(str)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
type ErrInvalidHexColor struct {
|
type ErrInvalidHexColor struct {
|
||||||
|
|
|
@ -135,18 +135,37 @@ type BackgroundImage struct {
|
||||||
Size string // A valid CSS background-size value
|
Size string // A valid CSS background-size value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PostType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
PostTypeUnknown PostType = iota
|
||||||
|
PostTypeBlogPost
|
||||||
|
PostTypeBlogComment
|
||||||
|
PostTypeForumThread
|
||||||
|
PostTypeForumReply
|
||||||
|
PostTypeWikiCreate
|
||||||
|
PostTypeWikiTalk
|
||||||
|
PostTypeWikiEdit
|
||||||
|
PostTypeLibraryComment
|
||||||
|
)
|
||||||
|
|
||||||
// Data from post_list_item.html
|
// Data from post_list_item.html
|
||||||
type PostListItem struct {
|
type PostListItem struct {
|
||||||
Title string
|
Title string
|
||||||
Url string
|
Url string
|
||||||
|
UUID string
|
||||||
Breadcrumbs []Breadcrumb
|
Breadcrumbs []Breadcrumb
|
||||||
|
|
||||||
|
PostType PostType
|
||||||
|
PostTypePrefix string
|
||||||
|
|
||||||
User User
|
User User
|
||||||
Date time.Time
|
Date time.Time
|
||||||
|
|
||||||
Unread bool
|
Unread bool
|
||||||
Classes string
|
Classes string
|
||||||
Content string
|
Preview string
|
||||||
|
LastEditDate time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// Data from thread_list_item.html
|
// Data from thread_list_item.html
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
package website
|
package website
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
|
||||||
"git.handmade.network/hmn/hmn/src/db"
|
"git.handmade.network/hmn/hmn/src/db"
|
||||||
"git.handmade.network/hmn/hmn/src/hmnurl"
|
"git.handmade.network/hmn/hmn/src/hmnurl"
|
||||||
"git.handmade.network/hmn/hmn/src/models"
|
"git.handmade.network/hmn/hmn/src/models"
|
||||||
|
@ -76,9 +80,123 @@ func Feed(c *RequestContext) ResponseData {
|
||||||
currentUserId = &c.CurrentUser.ID
|
currentUserId = &c.CurrentUser.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.Perf.StartBlock("SQL", "Fetch category tree")
|
||||||
|
categoryTree := models.GetFullCategoryTree(c.Context(), c.Conn)
|
||||||
|
lineageBuilder := models.MakeCategoryLineageBuilder(categoryTree)
|
||||||
|
c.Perf.EndBlock()
|
||||||
|
|
||||||
|
posts, err := fetchAllPosts(c, lineageBuilder, currentUserId, howManyPostsToSkip, postsPerPage)
|
||||||
|
if err != nil {
|
||||||
|
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch feed posts"))
|
||||||
|
}
|
||||||
|
|
||||||
|
baseData := getBaseData(c)
|
||||||
|
baseData.BodyClasses = append(baseData.BodyClasses, "feed")
|
||||||
|
|
||||||
|
var res ResponseData
|
||||||
|
res.WriteTemplate("feed.html", FeedData{
|
||||||
|
BaseData: baseData,
|
||||||
|
|
||||||
|
AtomFeedUrl: hmnurl.BuildAtomFeed(),
|
||||||
|
MarkAllReadUrl: hmnurl.BuildMarkRead(0),
|
||||||
|
Posts: posts,
|
||||||
|
Pagination: pagination,
|
||||||
|
}, c.Perf)
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
type FeedType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
FeedTypeAll = iota
|
||||||
|
FeedTypeProjects
|
||||||
|
FeedTypeShowcase
|
||||||
|
)
|
||||||
|
|
||||||
|
// NOTE(asaf): UUID values copied from old website
|
||||||
|
var (
|
||||||
|
FeedIDAll = "urn:uuid:1084fd28-993a-4961-9011-39ddeaeb3711"
|
||||||
|
FeedIDProjects = "urn:uuid:cfad0d50-cbcf-11e7-82d7-db1d52543cc7"
|
||||||
|
FeedIDShowcase = "urn:uuid:37d29027-2892-5a21-b521-951246c7aa46"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AtomFeedData struct {
|
||||||
|
Title string
|
||||||
|
Subtitle string
|
||||||
|
|
||||||
|
HomepageUrl string
|
||||||
|
AtomFeedUrl string
|
||||||
|
FeedUrl string
|
||||||
|
|
||||||
|
CopyrightStatement string
|
||||||
|
SiteVersion string
|
||||||
|
Updated time.Time
|
||||||
|
FeedID string
|
||||||
|
|
||||||
|
FeedType FeedType
|
||||||
|
Posts []templates.PostListItem
|
||||||
|
Projects []int // TODO(asaf): Actually do this
|
||||||
|
Snippets []int // TODO(asaf): Actually do this
|
||||||
|
}
|
||||||
|
|
||||||
|
func AtomFeed(c *RequestContext) ResponseData {
|
||||||
|
itemsPerFeed := 25 // NOTE(asaf): Copied from old website
|
||||||
|
|
||||||
|
feedData := AtomFeedData{
|
||||||
|
HomepageUrl: hmnurl.BuildHomepage(),
|
||||||
|
|
||||||
|
CopyrightStatement: fmt.Sprintf("Copyright (C) 2014-%d Handmade.Network and its contributors", time.Now().Year()),
|
||||||
|
SiteVersion: "2.0",
|
||||||
|
}
|
||||||
|
|
||||||
|
feedType, hasType := c.PathParams["feedtype"]
|
||||||
|
if !hasType || len(feedType) == 0 {
|
||||||
|
feedData.Title = "New Threads, Blog Posts, Replies and Comments | Site-wide | Handmade.Network"
|
||||||
|
feedData.Subtitle = feedData.Title
|
||||||
|
feedData.FeedType = FeedTypeAll
|
||||||
|
feedData.FeedID = FeedIDAll
|
||||||
|
feedData.AtomFeedUrl = hmnurl.BuildAtomFeed()
|
||||||
|
feedData.FeedUrl = hmnurl.BuildFeed()
|
||||||
|
|
||||||
|
c.Perf.StartBlock("SQL", "Fetch category tree")
|
||||||
|
categoryTree := models.GetFullCategoryTree(c.Context(), c.Conn)
|
||||||
|
lineageBuilder := models.MakeCategoryLineageBuilder(categoryTree)
|
||||||
|
c.Perf.EndBlock()
|
||||||
|
|
||||||
|
posts, err := fetchAllPosts(c, lineageBuilder, nil, 0, itemsPerFeed)
|
||||||
|
if err != nil {
|
||||||
|
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch feed posts"))
|
||||||
|
}
|
||||||
|
feedData.Posts = posts
|
||||||
|
|
||||||
|
updated := time.Now()
|
||||||
|
if len(posts) > 0 {
|
||||||
|
updated = posts[0].Date
|
||||||
|
}
|
||||||
|
feedData.Updated = updated
|
||||||
|
} else {
|
||||||
|
switch strings.ToLower(feedType) {
|
||||||
|
case "projects":
|
||||||
|
// TODO(asaf): Implement this
|
||||||
|
case "showcase":
|
||||||
|
// TODO(asaf): Implement this
|
||||||
|
default:
|
||||||
|
return FourOhFour(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var res ResponseData
|
||||||
|
res.WriteTemplate("atom.xml", feedData, c.Perf)
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchAllPosts(c *RequestContext, lineageBuilder *models.CategoryLineageBuilder, currentUserID *int, offset int, limit int) ([]templates.PostListItem, error) {
|
||||||
c.Perf.StartBlock("SQL", "Fetch posts")
|
c.Perf.StartBlock("SQL", "Fetch posts")
|
||||||
type feedPostQuery struct {
|
type feedPostQuery struct {
|
||||||
Post models.Post `db:"post"`
|
Post models.Post `db:"post"`
|
||||||
|
PostVersion models.PostVersion `db:"version"`
|
||||||
Thread models.Thread `db:"thread"`
|
Thread models.Thread `db:"thread"`
|
||||||
Cat models.Category `db:"cat"`
|
Cat models.Category `db:"cat"`
|
||||||
Proj models.Project `db:"proj"`
|
Proj models.Project `db:"proj"`
|
||||||
|
@ -92,6 +210,7 @@ func Feed(c *RequestContext) ResponseData {
|
||||||
SELECT $columns
|
SELECT $columns
|
||||||
FROM
|
FROM
|
||||||
handmade_post AS post
|
handmade_post AS post
|
||||||
|
JOIN handmade_postversion AS version ON version.id = post.current_id
|
||||||
JOIN handmade_thread AS thread ON thread.id = post.thread_id
|
JOIN handmade_thread AS thread ON thread.id = post.thread_id
|
||||||
JOIN handmade_category AS cat ON cat.id = post.category_id
|
JOIN handmade_category AS cat ON cat.id = post.category_id
|
||||||
JOIN handmade_project AS proj ON proj.id = post.project_id
|
JOIN handmade_project AS proj ON proj.id = post.project_id
|
||||||
|
@ -112,21 +231,16 @@ func Feed(c *RequestContext) ResponseData {
|
||||||
ORDER BY postdate DESC
|
ORDER BY postdate DESC
|
||||||
LIMIT $3 OFFSET $4
|
LIMIT $3 OFFSET $4
|
||||||
`,
|
`,
|
||||||
currentUserId,
|
currentUserID,
|
||||||
[]models.CategoryKind{models.CatKindForum, models.CatKindBlog, models.CatKindWiki, models.CatKindLibraryResource},
|
[]models.CategoryKind{models.CatKindForum, models.CatKindBlog, models.CatKindWiki, models.CatKindLibraryResource},
|
||||||
postsPerPage,
|
limit,
|
||||||
howManyPostsToSkip,
|
offset,
|
||||||
)
|
)
|
||||||
c.Perf.EndBlock()
|
c.Perf.EndBlock()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch feed posts"))
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Perf.StartBlock("SQL", "Fetch category tree")
|
|
||||||
categoryTree := models.GetFullCategoryTree(c.Context(), c.Conn)
|
|
||||||
lineageBuilder := models.MakeCategoryLineageBuilder(categoryTree)
|
|
||||||
c.Perf.EndBlock()
|
|
||||||
|
|
||||||
c.Perf.StartBlock("FEED", "Build post items")
|
c.Perf.StartBlock("FEED", "Build post items")
|
||||||
var postItems []templates.PostListItem
|
var postItems []templates.PostListItem
|
||||||
for _, iPostResult := range posts.ToSlice() {
|
for _, iPostResult := range posts.ToSlice() {
|
||||||
|
@ -139,7 +253,7 @@ func Feed(c *RequestContext) ResponseData {
|
||||||
hasRead = true
|
hasRead = true
|
||||||
}
|
}
|
||||||
|
|
||||||
postItems = append(postItems, MakePostListItem(
|
postItem := MakePostListItem(
|
||||||
lineageBuilder,
|
lineageBuilder,
|
||||||
&postResult.Proj,
|
&postResult.Proj,
|
||||||
&postResult.Thread,
|
&postResult.Thread,
|
||||||
|
@ -149,22 +263,14 @@ func Feed(c *RequestContext) ResponseData {
|
||||||
!hasRead,
|
!hasRead,
|
||||||
true,
|
true,
|
||||||
c.Theme,
|
c.Theme,
|
||||||
))
|
)
|
||||||
|
|
||||||
|
postItem.UUID = uuid.NewSHA1(uuid.NameSpaceURL, []byte(postItem.Url)).URN()
|
||||||
|
postItem.LastEditDate = postResult.PostVersion.EditDate
|
||||||
|
|
||||||
|
postItems = append(postItems, postItem)
|
||||||
}
|
}
|
||||||
c.Perf.EndBlock()
|
c.Perf.EndBlock()
|
||||||
|
|
||||||
baseData := getBaseData(c)
|
return postItems, nil
|
||||||
baseData.BodyClasses = append(baseData.BodyClasses, "feed")
|
|
||||||
|
|
||||||
var res ResponseData
|
|
||||||
res.WriteTemplate("feed.html", FeedData{
|
|
||||||
BaseData: baseData,
|
|
||||||
|
|
||||||
AtomFeedUrl: hmnurl.BuildAtomFeed(),
|
|
||||||
MarkAllReadUrl: hmnurl.BuildMarkRead(0),
|
|
||||||
Posts: postItems,
|
|
||||||
Pagination: pagination,
|
|
||||||
}, c.Perf)
|
|
||||||
|
|
||||||
return res
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,24 @@ func UrlForGenericPost(post *models.Post, subforums []string, threadTitle string
|
||||||
return hmnurl.BuildProjectHomepage(projectSlug)
|
return hmnurl.BuildProjectHomepage(projectSlug)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var PostTypeMap = map[models.CategoryKind][]templates.PostType{
|
||||||
|
models.CatKindBlog: []templates.PostType{templates.PostTypeBlogPost, templates.PostTypeBlogComment},
|
||||||
|
models.CatKindForum: []templates.PostType{templates.PostTypeForumThread, templates.PostTypeForumReply},
|
||||||
|
models.CatKindWiki: []templates.PostType{templates.PostTypeWikiCreate, templates.PostTypeWikiTalk},
|
||||||
|
models.CatKindLibraryResource: []templates.PostType{templates.PostTypeLibraryComment, templates.PostTypeLibraryComment},
|
||||||
|
}
|
||||||
|
|
||||||
|
var PostTypePrefix = map[templates.PostType]string{
|
||||||
|
templates.PostTypeBlogPost: "New blog post",
|
||||||
|
templates.PostTypeBlogComment: "Blog comment",
|
||||||
|
templates.PostTypeForumThread: "New forum thread",
|
||||||
|
templates.PostTypeForumReply: "Forum reply",
|
||||||
|
templates.PostTypeWikiCreate: "New wiki page",
|
||||||
|
templates.PostTypeWikiTalk: "Wiki comment",
|
||||||
|
templates.PostTypeWikiEdit: "Wiki edit",
|
||||||
|
templates.PostTypeLibraryComment: "Library comment",
|
||||||
|
}
|
||||||
|
|
||||||
// NOTE(asaf): THIS DOESN'T HANDLE WIKI EDIT ITEMS. Wiki edits are PostTextVersions, not Posts.
|
// NOTE(asaf): THIS DOESN'T HANDLE WIKI EDIT ITEMS. Wiki edits are PostTextVersions, not Posts.
|
||||||
func MakePostListItem(lineageBuilder *models.CategoryLineageBuilder, project *models.Project, thread *models.Thread, post *models.Post, user *models.User, libraryResource *models.LibraryResource, unread bool, includeBreadcrumbs bool, currentTheme string) templates.PostListItem {
|
func MakePostListItem(lineageBuilder *models.CategoryLineageBuilder, project *models.Project, thread *models.Thread, post *models.Post, user *models.User, libraryResource *models.LibraryResource, unread bool, includeBreadcrumbs bool, currentTheme string) templates.PostListItem {
|
||||||
var result templates.PostListItem
|
var result templates.PostListItem
|
||||||
|
@ -42,6 +60,19 @@ func MakePostListItem(lineageBuilder *models.CategoryLineageBuilder, project *mo
|
||||||
libraryResourceId = libraryResource.ID
|
libraryResourceId = libraryResource.ID
|
||||||
}
|
}
|
||||||
result.Url = UrlForGenericPost(post, lineageBuilder.GetSubforumLineageSlugs(post.CategoryID), thread.Title, libraryResourceId, project.Slug)
|
result.Url = UrlForGenericPost(post, lineageBuilder.GetSubforumLineageSlugs(post.CategoryID), thread.Title, libraryResourceId, project.Slug)
|
||||||
|
result.Preview = post.Preview
|
||||||
|
|
||||||
|
postType := templates.PostTypeUnknown
|
||||||
|
postTypeOptions, found := PostTypeMap[post.CategoryKind]
|
||||||
|
if found {
|
||||||
|
var hasParent int
|
||||||
|
if post.ParentID != nil {
|
||||||
|
hasParent = 1
|
||||||
|
}
|
||||||
|
postType = postTypeOptions[hasParent]
|
||||||
|
}
|
||||||
|
result.PostType = postType
|
||||||
|
result.PostTypePrefix = PostTypePrefix[result.PostType]
|
||||||
|
|
||||||
if includeBreadcrumbs {
|
if includeBreadcrumbs {
|
||||||
result.Breadcrumbs = append(result.Breadcrumbs, templates.Breadcrumb{
|
result.Breadcrumbs = append(result.Breadcrumbs, templates.Breadcrumb{
|
||||||
|
|
|
@ -14,6 +14,7 @@ import (
|
||||||
|
|
||||||
"git.handmade.network/hmn/hmn/src/logging"
|
"git.handmade.network/hmn/hmn/src/logging"
|
||||||
"git.handmade.network/hmn/hmn/src/models"
|
"git.handmade.network/hmn/hmn/src/models"
|
||||||
|
"git.handmade.network/hmn/hmn/src/oops"
|
||||||
"git.handmade.network/hmn/hmn/src/perf"
|
"git.handmade.network/hmn/hmn/src/perf"
|
||||||
"git.handmade.network/hmn/hmn/src/templates"
|
"git.handmade.network/hmn/hmn/src/templates"
|
||||||
"github.com/jackc/pgx/v4/pgxpool"
|
"github.com/jackc/pgx/v4/pgxpool"
|
||||||
|
@ -249,7 +250,11 @@ func (rd *ResponseData) WriteTemplate(name string, data interface{}, rp *perf.Re
|
||||||
rp.StartBlock("TEMPLATE", name)
|
rp.StartBlock("TEMPLATE", name)
|
||||||
defer rp.EndBlock()
|
defer rp.EndBlock()
|
||||||
}
|
}
|
||||||
return templates.Templates[name].Execute(rd, data)
|
template, hasTemplate := templates.Templates[name]
|
||||||
|
if !hasTemplate {
|
||||||
|
panic(oops.New(nil, "Template not found: %s", name))
|
||||||
|
}
|
||||||
|
return template.Execute(rd, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ErrorResponse(status int, errs ...error) ResponseData {
|
func ErrorResponse(status int, errs ...error) ResponseData {
|
||||||
|
|
|
@ -73,7 +73,7 @@ func NewWebsiteRoutes(conn *pgxpool.Pool, perfCollector *perf.PerfCollector) htt
|
||||||
}
|
}
|
||||||
|
|
||||||
if !c.CurrentProject.IsHMN() {
|
if !c.CurrentProject.IsHMN() {
|
||||||
return c.Redirect(hmnurl.Url(c.URL().String(), nil), http.StatusMovedPermanently)
|
return c.Redirect(hmnurl.Url(c.URL().Path, hmnurl.QFromURL(c.URL())), http.StatusMovedPermanently)
|
||||||
}
|
}
|
||||||
|
|
||||||
return h(c)
|
return h(c)
|
||||||
|
@ -104,6 +104,7 @@ func NewWebsiteRoutes(conn *pgxpool.Pool, perfCollector *perf.PerfCollector) htt
|
||||||
staticPages.GET(hmnurl.RegexProjectSubmissionGuidelines, ProjectSubmissionGuidelines)
|
staticPages.GET(hmnurl.RegexProjectSubmissionGuidelines, ProjectSubmissionGuidelines)
|
||||||
|
|
||||||
mainRoutes.GET(hmnurl.RegexFeed, Feed)
|
mainRoutes.GET(hmnurl.RegexFeed, Feed)
|
||||||
|
mainRoutes.GET(hmnurl.RegexAtomFeed, AtomFeed)
|
||||||
|
|
||||||
// TODO(asaf): Trailing slashes break these
|
// TODO(asaf): Trailing slashes break these
|
||||||
mainRoutes.GET(hmnurl.RegexForumThread, ForumThread)
|
mainRoutes.GET(hmnurl.RegexForumThread, ForumThread)
|
||||||
|
|
Loading…
Reference in New Issue