package website import ( "fmt" "html/template" "io" "io/fs" "net/http" "regexp" "sort" "strings" "time" "git.handmade.network/hmn/hmn/src/db" "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" ) // This will skip the common path prefix for fishbowl files. // We unfortunately need to do this because we want to use http.FileServer, // but _that_ needs an http.FS, but _that_ needs an fs.FS... var fishbowlFS = utils.Must1(fs.Sub(templates.FishbowlFS, "src/fishbowls")) var fishbowlHTTPFS = http.StripPrefix("/fishbowl", http.FileServer(http.FS(fishbowlFS))) type fishbowlInfo struct { Slug string Title, Description string // The description is used for OpenGraph, so it must be plain text, no HTML. Month time.Month Year int ContentsPath string } var fishbowls = [...]fishbowlInfo{ { Slug: "internet-os", Title: "The future of operating systems in an Internet world", Month: time.May, Year: 2020, }, { Slug: "metaprogramming", Title: "Compile-time introspection and metaprogramming", Month: time.June, Year: 2020, }, { Slug: "lisp-jam", Title: "Lessons from the LISP Jam", Month: time.August, Year: 2020, }, { Slug: "parallel-programming", Title: "Approaches to parallel programming", Month: time.November, Year: 2020, }, { Slug: "skimming", Title: "Code skimmability as the root cause for bad code structure decisions", // real snappy, this one Month: time.January, Year: 2021, }, { Slug: "config", Title: "How to design to avoid configuration", Month: time.March, Year: 2021, }, { Slug: "simplicity-performance", Title: "The relationship of simplicity and performance", Month: time.May, Year: 2021, }, { Slug: "teaching-software", Title: "How software development is taught", Description: "The Handmade Network exists because we are unhappy with the software status quo. To a large extent, this is because of how software development is taught. What are the good parts of software education today, what are the flaws, and how might we change things to improve the state of software?", Month: time.June, Year: 2021, }, { Slug: "flexible-software", Title: "How to design flexible software", Description: "We previously held a fishbowl about how to design to avoid configuration. But when you can't avoid configuration, how do you do it well? And if we want our software to be flexible, what other options do we have besides configuration? What other ways are there to make software flexible?", Month: time.December, Year: 2021, }, { Slug: "oop", Title: "What, if anything, is OOP?", Description: "Is object-oriented programming bad? Is it good? What even is it, anyway? This fishbowl explores OOP more carefully—what is the essence of it, what are the good parts, why did it take over the world, and why do we criticize it so much?", Month: time.May, Year: 2022, ContentsPath: "oop/OOP.html", }, } func FishbowlIndex(c *RequestContext) ResponseData { type fishbowlTmpl struct { Fishbowl fishbowlInfo Url string Valid bool date time.Time } type tmpl struct { templates.BaseData Fishbowls []fishbowlTmpl } tmplData := tmpl{ BaseData: getBaseData(c, "Fishbowls", []templates.Breadcrumb{ {Name: "Fishbowls", Url: hmnurl.BuildFishbowlIndex()}, }), } var fishbowlTmpls []fishbowlTmpl for _, f := range fishbowls { fishbowlTmpls = append(fishbowlTmpls, fishbowlTmpl{ Fishbowl: f, Url: hmnurl.BuildFishbowl(f.Slug), Valid: f.ContentsPath != "", date: time.Date(f.Year, f.Month, 0, 0, 0, 0, 0, time.UTC), }) } sort.Slice(fishbowlTmpls, func(i, j int) bool { return fishbowlTmpls[j].date.Before(fishbowlTmpls[i].date) // reverse }) tmplData.Fishbowls = fishbowlTmpls var res ResponseData err := res.WriteTemplate("fishbowl_index.html", tmplData, c.Perf) if err != nil { return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to render fishbowl index page")) } return res } func Fishbowl(c *RequestContext) ResponseData { slug := c.PathParams["slug"] var info fishbowlInfo // Only serve up valid fishbowls (with content) exists := false for _, fishbowl := range fishbowls { if fishbowl.Slug == slug && fishbowl.ContentsPath != "" { exists = true info = fishbowl } } if !exists { return FourOhFour(c) } // Ensure trailing slash (it matters for relative URLs in the HTML) if !strings.HasSuffix(c.URL().Path, "/") { return c.Redirect(c.URL().Path+"/", http.StatusFound) } type FishbowlData struct { templates.BaseData Slug string Info fishbowlInfo Contents template.HTML } contentsFile := utils.Must1(fishbowlFS.Open(info.ContentsPath)) contents := string(utils.Must1(io.ReadAll(contentsFile))) contents, err := linkifyDiscordContent(c, c.Conn, contents) if err != nil { return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to linkify fishbowl content")) } tmpl := FishbowlData{ BaseData: getBaseData(c, info.Title, []templates.Breadcrumb{ {Name: "Fishbowls", Url: hmnurl.BuildFishbowlIndex()}, {Name: info.Title, Url: hmnurl.BuildFishbowl(slug)}, }), Slug: slug, Info: info, Contents: template.HTML(contents), } tmpl.BaseData.OpenGraphItems = append(tmpl.BaseData.OpenGraphItems, templates.OpenGraphItem{ Property: "og:description", Value: info.Description, }) var res ResponseData err = res.WriteTemplate("fishbowl.html", tmpl, c.Perf) if err != nil { return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to render fishbowl index page")) } return res } func FishbowlFiles(c *RequestContext) ResponseData { var res ResponseData fishbowlHTTPFS.ServeHTTP(&res, c.Req) AddCORSHeaders(c, &res) return res } var reFishbowlDiscordUserId = regexp.MustCompile(`data-user-id="(\d+)"`) var reFishbowlDiscordAuthorHeader = regexp.MustCompile(`(?s:(
.*?)()(.*?)(.*?)())`) func linkifyDiscordContent(c *RequestContext, dbConn db.ConnOrTx, content string) (string, error) { discordUserIdSet := make(map[string]struct{}) userIdMatches := reFishbowlDiscordUserId.FindAllStringSubmatch(content, -1) for _, m := range userIdMatches { discordUserIdSet[m[1]] = struct{}{} } discordUserIds := make([]string, 0, len(discordUserIdSet)) for id := range discordUserIdSet { discordUserIds = append(discordUserIds, id) } hmnUsers, err := hmndata.FetchUsers(c.Context(), dbConn, c.CurrentUser, hmndata.UsersQuery{ DiscordUserIDs: discordUserIds, }) if err != nil { return "", err } return reFishbowlDiscordAuthorHeader.ReplaceAllStringFunc(content, func(s string) string { m := reFishbowlDiscordAuthorHeader.FindStringSubmatch(s) discordUserID := m[4] var matchingUser *models.User for _, u := range hmnUsers { if u.DiscordUser.UserID == discordUserID { matchingUser = u break } } if matchingUser == nil { return s } else { link := fmt.Sprintf(``, hmnurl.BuildUserProfile(matchingUser.Username)) return m[1] + link + m[2] + "" + m[3] + link + m[5] + "" + m[6] } }), nil }