hmn/src/website/feed.go

255 lines
7.2 KiB
Go

package website
import (
"fmt"
"math"
"net/http"
"strings"
"time"
"git.handmade.network/hmn/hmn/src/hmndata"
"git.handmade.network/hmn/hmn/src/hmnurl"
"git.handmade.network/hmn/hmn/src/models"
"git.handmade.network/hmn/hmn/src/oops"
"git.handmade.network/hmn/hmn/src/templates"
"git.handmade.network/hmn/hmn/src/utils"
"github.com/google/uuid"
)
type FeedData struct {
templates.BaseData
Posts []templates.PostListItem
Pagination templates.Pagination
AtomFeedUrl string
MarkAllReadUrl string
}
const feedPostsPerPage = 30
var feedThreadTypes = []models.ThreadType{
models.ThreadTypeForumPost,
models.ThreadTypeProjectBlogPost,
}
func Feed(c *RequestContext) ResponseData {
numPosts, err := hmndata.CountPosts(c, c.Conn, c.CurrentUser, hmndata.PostsQuery{
ThreadTypes: feedThreadTypes,
})
if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, err)
}
numPages := int(math.Ceil(float64(numPosts) / feedPostsPerPage))
page, numPages, ok := getPageInfo(c.PathParams["page"], numPosts, feedPostsPerPage)
if !ok {
return c.Redirect(hmnurl.BuildFeed(), http.StatusSeeOther)
}
pagination := templates.Pagination{
Current: page,
Total: numPages,
FirstUrl: hmnurl.BuildFeed(),
LastUrl: hmnurl.BuildFeedWithPage(numPages),
NextUrl: hmnurl.BuildFeedWithPage(utils.IntClamp(1, page+1, numPages)),
PreviousUrl: hmnurl.BuildFeedWithPage(utils.IntClamp(1, page-1, numPages)),
}
posts, err := fetchAllPosts(c, (page-1)*feedPostsPerPage, feedPostsPerPage)
if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch feed posts"))
}
baseData := getBaseDataAutocrumb(c, "Feed")
baseData.BodyClasses = append(baseData.BodyClasses, "feed")
var res ResponseData
res.MustWriteTemplate("feed.html", FeedData{
BaseData: baseData,
AtomFeedUrl: hmnurl.BuildAtomFeed(),
MarkAllReadUrl: c.UrlContext.BuildForumMarkRead(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 []templates.Project
Snippets []templates.TimelineItem
}
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()
posts, err := fetchAllPosts(c, 0, itemsPerFeed)
if err != nil {
return c.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":
feedData.Title = "New Projects | Site-wide | Handmade.Network"
feedData.Subtitle = feedData.Title
feedData.FeedType = FeedTypeProjects
feedData.FeedID = FeedIDProjects
feedData.AtomFeedUrl = hmnurl.BuildAtomFeedForProjects()
feedData.FeedUrl = hmnurl.BuildProjectIndex(1)
c.Perf.StartBlock("SQL", "Fetching projects")
_, hasAll := c.Req.URL.Query()["all"]
if hasAll {
itemsPerFeed = 100000
}
projectsAndStuff, err := hmndata.FetchProjects(c, c.Conn, nil, hmndata.ProjectsQuery{
Limit: itemsPerFeed,
Types: hmndata.OfficialProjects,
OrderBy: "date_approved DESC",
})
if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch feed projects"))
}
for _, p := range projectsAndStuff {
templateProject := templates.ProjectToTemplate(&p.Project, hmndata.UrlContextForProject(&p.Project).BuildHomepage())
templateProject.UUID = uuid.NewSHA1(uuid.NameSpaceURL, []byte(templateProject.Url)).URN()
for _, owner := range p.Owners {
templateProject.Owners = append(templateProject.Owners, templates.UserToTemplate(owner, ""))
}
feedData.Projects = append(feedData.Projects, templateProject)
}
c.Perf.EndBlock()
updated := time.Now()
if len(feedData.Projects) > 0 {
updated = feedData.Projects[0].DateApproved
}
feedData.Updated = updated
case "showcase":
feedData.Title = "Showcase | Site-wide | Handmade.Network"
feedData.Subtitle = feedData.Title
feedData.FeedType = FeedTypeShowcase
feedData.FeedID = FeedIDShowcase
feedData.AtomFeedUrl = hmnurl.BuildAtomFeedForShowcase()
feedData.FeedUrl = hmnurl.BuildShowcase()
snippets, err := hmndata.FetchSnippets(c, c.Conn, c.CurrentUser, hmndata.SnippetQuery{
Limit: itemsPerFeed,
})
if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch snippets"))
}
for _, s := range snippets {
timelineItem := SnippetToTimelineItem(&s.Snippet, s.Asset, s.DiscordMessage, s.Projects, s.Owner, c.Theme, false)
feedData.Snippets = append(feedData.Snippets, timelineItem)
}
c.Perf.EndBlock()
updated := time.Now()
if len(feedData.Snippets) > 0 {
updated = feedData.Snippets[0].Date
}
feedData.Updated = updated
default:
return FourOhFour(c)
}
}
var res ResponseData
res.MustWriteTemplate("atom.xml", feedData, c.Perf)
return res
}
func fetchAllPosts(c *RequestContext, offset int, limit int) ([]templates.PostListItem, error) {
postsAndStuff, err := hmndata.FetchPosts(c, c.Conn, c.CurrentUser, hmndata.PostsQuery{
ThreadTypes: feedThreadTypes,
Limit: limit,
Offset: offset,
SortDescending: true,
})
if err != nil {
return nil, err
}
c.Perf.StartBlock("SQL", "Fetch subforum tree")
subforumTree := models.GetFullSubforumTree(c, c.Conn)
lineageBuilder := models.MakeSubforumLineageBuilder(subforumTree)
c.Perf.EndBlock()
c.Perf.StartBlock("FEED", "Build post items")
var postItems []templates.PostListItem
for _, postAndStuff := range postsAndStuff {
postItem := MakePostListItem(
lineageBuilder,
&postAndStuff.Project,
&postAndStuff.Thread,
&postAndStuff.Post,
postAndStuff.Author,
postAndStuff.Unread,
true,
c.Theme,
)
postItem.UUID = uuid.NewSHA1(uuid.NameSpaceURL, []byte(postItem.Url)).URN()
postItem.LastEditDate = postAndStuff.CurrentVersion.Date
postItems = append(postItems, postItem)
}
c.Perf.EndBlock()
return postItems, nil
}