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 }