diff --git a/.gitignore b/.gitignore index e97999f..a5d457c 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ vendor/ dbclones/ coverage.out public/media/ +cinera/*/ +cinera/cinera.conf +annotations/ diff --git a/cinera/cinera.conf.sample b/cinera/cinera.conf.sample new file mode 100755 index 0000000..46cf710 --- /dev/null +++ b/cinera/cinera.conf.sample @@ -0,0 +1,12 @@ +export CINERA_REPO_PATH=/home/handmade/src/handmade-dev/cinera/Annotation-System +export CINERA_HMML_PATH=/home/handmade/src/handmade-dev/cinera/cinera_handmade.network +export CINERA_ASSETS_PATH=/home/handmade/src/handmade-dev/hmdev/static/annotations +export CINERA_OUTPUT_PATH=/home/handmade/src/handmade-dev/annotations +export DOMAIN=handmade.local +export SCHEME=https +export CINERA_MONIT_GROUP=cinera +export CINERA_SCRIPT_PATH=/home/handmade/src/handmade-dev/cinera +export ANNOTATIONS_USER=handmade + +# NOTE(asaf): Known-working version as of 2021-08-26 +export CINERA_VERSION=6da970d48ca2cee861b7fe2d8f4d7ed6ca9ccce1 diff --git a/cinera/cinera_hmn.conf b/cinera/cinera_hmn.conf new file mode 100755 index 0000000..942ef6c --- /dev/null +++ b/cinera/cinera_hmn.conf @@ -0,0 +1,74 @@ +// vim:ft=c: + +include = "$CINERA_HMML_PATH/cinera_includes_hero_people.conf"; +include = "$CINERA_HMML_PATH/cinera_includes_riscy_people.conf"; +include = "$CINERA_HMML_PATH/cinera_includes_bitwise_people.conf"; + +db_location = "data/cinera.db"; +cache_dir = "data/cache"; + +ignore_privacy = "true"; +hmml_dir = "$CINERA_HMML_PATH/$owner/$origin/$project"; + +base_dir = "$CINERA_OUTPUT_PATH/$lineage"; +base_url = "$SCHEME://$origin.$DOMAIN"; + +assets_root_dir = "$CINERA_ASSETS_PATH"; +assets_root_url = "$SCHEME://$DOMAIN/public/annotations"; +default_medium = "programming"; + +player_location = "episode/$project"; +global_theme = "global"; +theme = "$origin"; + +// player_template = "$CINERA_SCRIPT_PATH/cinera_template_episode.html"; + +project = "hero" { + include = "$CINERA_HMML_PATH/cinera_includes_hero_media.conf"; + owner = "cmuratori"; + unit = "Day"; + title = ""; + + project = "misc" { + unit = ""; + title = "Miscellaneous"; + } + project = "intro-to-c" { + title = "Introduction to C"; + } + project = "code" { + title = "Handmade Hero"; + } + project = "chat" { + title = "Handmade Chat"; + } + project = "ray" { + title = "Handmade Ray"; + } +} + +project = "riscy" { + include = "$CINERA_HMML_PATH/cinera_includes_riscy_media.conf"; + base_dir = "$CINERA_OUTPUT_PATH/$origin/$project"; + owner = "miotatsu"; + unit = "Day"; + title = "Riscy Business"; + + project = "risc" { + title = "Riscellaneous"; + } + project = "coad" { + title = "Computer Organisation and Architecture"; + } + project = "reader" { + title = "Risc-V Book Club"; + } +} + +project = "bitwise" { + include = "$CINERA_HMML_PATH/cinera_includes_bitwise_media.conf"; + base_dir = "$CINERA_OUTPUT_PATH/$origin/$project"; + owner = "pervognsen"; + unit = "Day"; + title = "Bitwise"; +} diff --git a/cinera/how_to_run_locally.md b/cinera/how_to_run_locally.md new file mode 100644 index 0000000..63a7bf5 --- /dev/null +++ b/cinera/how_to_run_locally.md @@ -0,0 +1,15 @@ +# How to run Cinera locally + +0. Install prerequisites: + a. `libcurl4-openssl-dev` + b. `byacc` + c. `flex` + +1. Copy `cinera/cinera.conf.sample` to `cinera/cinera.conf` and edit to match your local system +2. Run `cinera/user_update_cinera.sh` +3. Run `cinera/update_annotations.sh` +4. From the cinera dir run: `./run_local.sh` +5. Once it's done processing everything (you should see "Monitoring + file system for new, edited and deleted .hmml and asset files") + You can shut it down with ctrl+c. + diff --git a/cinera/run_local.sh b/cinera/run_local.sh new file mode 100755 index 0000000..e1ec890 --- /dev/null +++ b/cinera/run_local.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +cd "$(dirname "$0")" + +[ -e "./cinera.conf" ] || exit +. ./cinera.conf + +$CINERA_REPO_PATH/cinera/cinera -c cinera_hmn.conf + diff --git a/cinera/setup.sh b/cinera/setup.sh new file mode 100755 index 0000000..7365617 --- /dev/null +++ b/cinera/setup.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +if [ ! -e "cinera.conf" ]; then + echo "Can't find cinera.conf" + exit +fi +. cinera.conf + +./update_cinera.sh +./update_annotations.sh + +[ -d "data" ] || mkdir data diff --git a/cinera/start.sh b/cinera/start.sh new file mode 100755 index 0000000..5b04256 --- /dev/null +++ b/cinera/start.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +cd "$(dirname "$0")" + +[ -e "./cinera.conf" ] || exit +. ./cinera.conf + +nohup $CINERA_REPO_PATH/cinera/cinera -c cinera_hmn.conf > data/cinera.log 2>&1 & +echo $! > data/cinera.pid + diff --git a/cinera/stop.sh b/cinera/stop.sh new file mode 100755 index 0000000..271a2f2 --- /dev/null +++ b/cinera/stop.sh @@ -0,0 +1,3 @@ +#!/bin/bash +cd "$(dirname "$0")" +kill $(cat data/cinera.pid) diff --git a/cinera/update_annotations.sh b/cinera/update_annotations.sh new file mode 100755 index 0000000..b5cabb4 --- /dev/null +++ b/cinera/update_annotations.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +if [ ! -e "cinera.conf" ]; then + echo "Can't find cinera.conf" + exit +fi +. cinera.conf + +if [ ! -d $CINERA_HMML_PATH ]; then + git clone git@gitssh.handmade.network:Annotation-Pushers/cinera_handmade.network.git $CINERA_HMML_PATH +fi + +if [ ! -d $CINERA_HMML_PATH ]; then + echo "Failed to clone annotation repo" + exit +fi + +cd $CINERA_HMML_PATH +git pull + +cp -av $CINERA_HMML_PATH/cmuratori/hero/cinera__hero.css $CINERA_ASSETS_PATH/ +cp -av $CINERA_HMML_PATH/miotatsu/riscy/cinera__riscy.css $CINERA_ASSETS_PATH/ +cp -av $CINERA_HMML_PATH/pervognsen/bitwise/cinera__bitwise.css $CINERA_ASSETS_PATH/ diff --git a/cinera/update_cinera.sh b/cinera/update_cinera.sh new file mode 100755 index 0000000..8a7b882 --- /dev/null +++ b/cinera/update_cinera.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +if [ ! -e "cinera.conf" ]; then + echo "Can't find cinera.conf" + exit +fi +. cinera.conf + +monit -g $CINERA_MONIT_GROUP stop + +CMD="cd $CINERA_SCRIPT_PATH; ./user_update_cinera.sh" + +su - $ANNOTATIONS_USER -c "$CMD" + +monit -g $CINERA_MONIT_GROUP start diff --git a/cinera/user_update_cinera.sh b/cinera/user_update_cinera.sh new file mode 100755 index 0000000..a933dfc --- /dev/null +++ b/cinera/user_update_cinera.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +if [ ! -e "cinera.conf" ]; then + echo "Can't find cinera.conf" + exit +fi +. cinera.conf + +if [ ! -d $CINERA_REPO_PATH ]; then + git clone git@gitssh.handmade.network:Annotation-Pushers/Annotation-System.git $CINERA_REPO_PATH +fi + +if [ ! -d $CINERA_REPO_PATH ]; then + echo "Failed to clone cinera" + exit +fi + + +cd $CINERA_REPO_PATH +git pull +if [[ -z "${CINERA_VERSION}" ]]; then + git checkout master +else + git checkout $CINERA_VERSION +fi +cd $CINERA_REPO_PATH/hmmlib +make +cp hmml.a hmmlib.h ../cinera +cd $CINERA_REPO_PATH/cinera +`$SHELL cinera.c` + +if [ ! -d $CINERA_ASSETS_PATH ]; then + mkdir $CINERA_ASSETS_PATH +fi + +cp -av $CINERA_REPO_PATH/cinera/*.{css,js,png} $CINERA_ASSETS_PATH diff --git a/monitrc.sample b/monitrc.sample new file mode 100644 index 0000000..60bb90f --- /dev/null +++ b/monitrc.sample @@ -0,0 +1,54 @@ +SET DAEMON 5 +SET LOGFILE /var/log/monit.log +SET STATEFILE /var/lib/monit/state +SET HTTPD UNIXSOCKET /var/run/monit.sock + allow user:pass +SET MAILSERVER + box.handmadedev.org + PORT 587 + USERNAME "noreply@handmadedev.org" + PASSWORD "[FILL THIS IN]" + USING tlsv1 +SET MAIL-FORMAT { + from: noreply@handmadedev.org +reply-to: noreply@handmadedev.org + subject: $SERVICE $EVENT at $DATE + message: Monit $ACTION $SERVICE at $DATE on $HOST: $DESCRIPTION +} +SET ALERT team@handmadedev.org only on { nonexist, instance } + +CHECK PROCESS beta_cinera PIDFILE /home/hmn-beta/srv/cinera/data/cinera.pid + GROUP cinera_beta + START PROGRAM = "/home/hmn-beta/srv/cinera/start.sh" AS UID "annotations" + STOP PROGRAM = "/home/hmn-beta/srv/cinera/stop.sh" AS UID "annotations" + MODE PASSIVE + +CHECK PROCESS live_cinera PIDFILE /home/hmn-live/srv/cinera/data/cinera.pid + GROUP cinera_live + START PROGRAM = "/home/hmn-live/srv/cinera/start.sh" AS UID "annotations" + STOP PROGRAM = "/home/hmn-live/srv/cinera/stop.sh" AS UID "annotations" + MODE PASSIVE + +CHECK PROCESS beta_discord_history PIDFILE /home/hmn-beta/discordhistory.pid + GROUP discord_history_beta + START PROGRAM = "/home/hmn-beta/start_discord_history.sh" AS UID "hmn-beta" + STOP PROGRAM = "/bin/bash -c '/bin/kill `/bin/cat /home/hmn-beta/discordhistory.pid`'" + MODE ACTIVE + +CHECK PROCESS beta_discord_bot PIDFILE /home/hmn-beta/discordbot.pid + GROUP discord_bot_beta + START PROGRAM = "/home/hmn-beta/start_discord_bot.sh" AS UID "hmn-beta" + STOP PROGRAM = "/bin/bash -c '/bin/kill `/bin/cat /home/hmn-beta/discordbot.pid`'" + MODE ACTIVE + +CHECK PROCESS live_discord_history PIDFILE /home/hmn-live/discordhistory.pid + GROUP discord_history_live + START PROGRAM = "/home/hmn-live/start_discord_history.sh" AS UID "hmn-live" + STOP PROGRAM = "/bin/bash -c '/bin/kill `/bin/cat /home/hmn-live/discordhistory.pid`'" + MODE ACTIVE + +CHECK PROCESS live_discord_bot PIDFILE /home/hmn-live/discordbot.pid + GROUP discord_bot_live + START PROGRAM = "/home/hmn-live/start_discord_bot.sh" AS UID "hmn-live" + STOP PROGRAM = "/bin/bash -c '/bin/kill `/bin/cat /home/hmn-live/discordbot.pid`'" + MODE ACTIVE diff --git a/src/config/config.go.example b/src/config/config.go.example index e64727d..5e04211 100644 --- a/src/config/config.go.example +++ b/src/config/config.go.example @@ -42,4 +42,8 @@ var Config = HMNConfig{ AssetsPathPrefix: "", AssetsPublicUrlRoot: "", }, + EpisodeGuide: EpisodeGuide{ + CineraOutputPath: "./annotations/", + Projects: map[string]string{"hero": "code", "riscy": "riscy", "bitwise": "bitwise"}, + }, } diff --git a/src/config/types.go b/src/config/types.go index 19c0195..bd25fa6 100644 --- a/src/config/types.go +++ b/src/config/types.go @@ -26,6 +26,7 @@ type HMNConfig struct { Email EmailConfig DigitalOcean DigitalOceanConfig Discord DiscordConfig + EpisodeGuide EpisodeGuide } type PostgresConfig struct { @@ -76,6 +77,11 @@ type DiscordConfig struct { LibraryChannelID string } +type EpisodeGuide struct { + CineraOutputPath string + Projects map[string]string // NOTE(asaf): Maps from slugs to default episode guide topic +} + func (info PostgresConfig) DSN() string { return fmt.Sprintf("user=%s password=%s host=%s port=%d dbname=%s", info.User, info.Password, info.Hostname, info.Port, info.DbName) } diff --git a/src/hmnurl/hmnurl_test.go b/src/hmnurl/hmnurl_test.go index 711fe17..ff47260 100644 --- a/src/hmnurl/hmnurl_test.go +++ b/src/hmnurl/hmnurl_test.go @@ -276,6 +276,18 @@ func TestLibraryResource(t *testing.T) { AssertSubdomain(t, BuildLibraryResource("hero", 1), "hero") } +func TestEpisodeGuide(t *testing.T) { + AssertRegexMatch(t, BuildEpisodeList("hero", ""), RegexEpisodeList, map[string]string{"topic": ""}) + AssertRegexMatch(t, BuildEpisodeList("hero", "code"), RegexEpisodeList, map[string]string{"topic": "code"}) + AssertSubdomain(t, BuildEpisodeList("hero", "code"), "hero") + + AssertRegexMatch(t, BuildEpisode("hero", "code", "day001"), RegexEpisode, map[string]string{"topic": "code", "episode": "day001"}) + AssertSubdomain(t, BuildEpisode("hero", "code", "day001"), "hero") + + AssertRegexMatch(t, BuildCineraIndex("hero", "code"), RegexCineraIndex, map[string]string{"topic": "code"}) + AssertSubdomain(t, BuildCineraIndex("hero", "code"), "hero") +} + func TestProjectCSS(t *testing.T) { AssertRegexMatch(t, BuildProjectCSS("000000"), RegexProjectCSS, nil) } diff --git a/src/hmnurl/urls.go b/src/hmnurl/urls.go index b5b16bc..38b128c 100644 --- a/src/hmnurl/urls.go +++ b/src/hmnurl/urls.go @@ -554,6 +554,38 @@ func BuildLibraryResource(projectSlug string, resourceId int) string { return ProjectUrl(builder.String(), nil, projectSlug) } +/* +* Episode Guide + */ + +var RegexEpisodeList = regexp.MustCompile(`^/episode(/(?P[^/]+))?$`) + +func BuildEpisodeList(projectSlug string, topic string) string { + defer CatchPanic() + + var builder strings.Builder + builder.WriteString("/episode") + if topic != "" { + builder.WriteString("/") + builder.WriteString(topic) + } + return ProjectUrl(builder.String(), nil, projectSlug) +} + +var RegexEpisode = regexp.MustCompile(`^/episode/(?P[^/]+)/(?P[^/]+)$`) + +func BuildEpisode(projectSlug string, topic string, episode string) string { + defer CatchPanic() + return ProjectUrl(fmt.Sprintf("/episode/%s/%s", topic, episode), nil, projectSlug) +} + +var RegexCineraIndex = regexp.MustCompile(`^/(?P[^/]+).index$`) + +func BuildCineraIndex(projectSlug string, topic string) string { + defer CatchPanic() + return ProjectUrl(fmt.Sprintf("/%s.index", topic), nil, projectSlug) +} + /* * Discord OAuth */ diff --git a/src/templates/src/episode.html b/src/templates/src/episode.html new file mode 100644 index 0000000..d344947 --- /dev/null +++ b/src/templates/src/episode.html @@ -0,0 +1,16 @@ +{{ template "base.html" . }} + +{{ define "extrahead" }} + + + + + + +{{ end }} + +{{ define "content" }} +
+ {{ .Content }} +
+{{ end }} diff --git a/src/templates/src/episode_list.html b/src/templates/src/episode_list.html new file mode 100644 index 0000000..87946a9 --- /dev/null +++ b/src/templates/src/episode_list.html @@ -0,0 +1,111 @@ +{{ template "base.html" . }} + +{{ define "extrahead" }} + + + + + + +{{ end }} + +{{ define "content" }} +
+
+

Annotations for the topic: {{ .CurrentTopic }}

+ {{ .Content }} +
+ +
+{{ end }} diff --git a/src/templates/src/include/header.html b/src/templates/src/include/header.html index 2bed50a..016cbc9 100644 --- a/src/templates/src/include/header.html +++ b/src/templates/src/include/header.html @@ -58,7 +58,7 @@ Mission {{ end }} {{/* {% if project.default_annotation_category %} */}} - {{ if false }} + {{ if .Header.EpisodeGuideUrl }} Episode Guide {{ end }} {{ if .Header.EditUrl }} diff --git a/src/templates/templates.go b/src/templates/templates.go index 9efde09..256c537 100644 --- a/src/templates/templates.go +++ b/src/templates/templates.go @@ -105,6 +105,9 @@ var HMNTemplateFuncs = template.FuncMap{ } return a }, + "strjoin": func(strs ...string) string { + return strings.Join(strs, "") + }, "absolutedate": func(t time.Time) string { return t.UTC().Format("January 2, 2006, 3:04pm") }, diff --git a/src/website/episode_guide.go b/src/website/episode_guide.go new file mode 100644 index 0000000..4585b6b --- /dev/null +++ b/src/website/episode_guide.go @@ -0,0 +1,197 @@ +package website + +import ( + "fmt" + "html/template" + "io/fs" + "net/http" + "os" + "path" + "regexp" + "strings" + + "git.handmade.network/hmn/hmn/src/config" + "git.handmade.network/hmn/hmn/src/hmnurl" + "git.handmade.network/hmn/hmn/src/templates" +) + +func CineraIndex(c *RequestContext) ResponseData { + topic := c.PathParams["topic"] + slug := c.CurrentProject.Slug + + _, foundTopic := topicsForProject(slug, topic) + if foundTopic == "" { + return FourOhFour(c) + } + + indexPath := path.Join(config.Config.EpisodeGuide.CineraOutputPath, slug, foundTopic, fmt.Sprintf("%s.index", foundTopic)) + + content, err := os.ReadFile(indexPath) + if err != nil { + return FourOhFour(c) + } + + var res ResponseData + res.Write(content) + + return res +} + +var episodeListRegex = regexp.MustCompile(`(?ism)^.*(?P.*).*$`) + +type EpisodeListData struct { + templates.BaseData + Content template.HTML + CurrentTopic string + Topics []templates.Link +} + +func EpisodeList(c *RequestContext) ResponseData { + topic := c.PathParams["topic"] + slug := c.CurrentProject.Slug + + defaultTopic, hasEpisodeGuide := config.Config.EpisodeGuide.Projects[slug] + + if !hasEpisodeGuide { + return c.Redirect(hmnurl.BuildProjectHomepage(slug), http.StatusSeeOther) + } + + if topic == "" { + return c.Redirect(hmnurl.BuildEpisodeList(slug, defaultTopic), http.StatusSeeOther) + } + + allTopics, foundTopic := topicsForProject(slug, topic) + if foundTopic == "" { + return FourOhFour(c) + } + + htmlPath := path.Join(config.Config.EpisodeGuide.CineraOutputPath, slug, foundTopic, "index.html") + + htmlContent, err := os.ReadFile(htmlPath) + if err != nil { + return FourOhFour(c) + } + matches := episodeListRegex.FindStringSubmatch(string(htmlContent)) + if matches == nil || matches[episodeListRegex.SubexpIndex("guide")] == "" { + c.Logger.Error().Str("Filename", htmlPath).Msg("Episode guide index.html can't be parsed.") + return FourOhFour(c) + } + guide := matches[episodeListRegex.SubexpIndex("guide")] + + var topicLinks []templates.Link + for _, t := range allTopics { + url := "" + if t != foundTopic { + url = hmnurl.BuildEpisodeList(slug, t) + } + topicLinks = append(topicLinks, templates.Link{LinkText: t, Url: url}) + } + + var res ResponseData + baseData := getBaseData(c) + res.MustWriteTemplate("episode_list.html", EpisodeListData{ + BaseData: baseData, + Content: template.HTML(guide), + CurrentTopic: foundTopic, + Topics: topicLinks, + }, c.Perf) + return res +} + +var episodeTitleRegex = regexp.MustCompile(`(?ism)(?P.*?)</span>`) +var episodeContentRegex = regexp.MustCompile(`(?ism)<body>(?P<content>.*)</body>`) + +type EpisodeData struct { + templates.BaseData + Content template.HTML +} + +func Episode(c *RequestContext) ResponseData { + episode := c.PathParams["episode"] + topic := c.PathParams["topic"] + slug := c.CurrentProject.Slug + + _, hasEpisodeGuide := config.Config.EpisodeGuide.Projects[slug] + + if !hasEpisodeGuide { + return c.Redirect(hmnurl.BuildProjectHomepage(slug), http.StatusSeeOther) + } + + _, foundTopic := topicsForProject(slug, topic) + if foundTopic == "" { + return FourOhFour(c) + } + + foundEpisode := findEpisode(slug, foundTopic, episode) + if foundEpisode == "" { + return FourOhFour(c) + } + + htmlPath := path.Join(config.Config.EpisodeGuide.CineraOutputPath, slug, foundTopic, "episode", foundTopic, foundEpisode, "index.html") + htmlContent, err := os.ReadFile(htmlPath) + if err != nil { + return FourOhFour(c) + } + + titleMatches := episodeTitleRegex.FindStringSubmatch(string(htmlContent)) + title := fmt.Sprintf("%s Episode Guide", c.CurrentProject.Name) + if titleMatches != nil && titleMatches[episodeTitleRegex.SubexpIndex("title")] != "" { + title = fmt.Sprintf("%s | %s", titleMatches[episodeTitleRegex.SubexpIndex("title")], title) + } + + contentMatches := episodeContentRegex.FindStringSubmatch(string(htmlContent)) + if contentMatches == nil || contentMatches[episodeContentRegex.SubexpIndex("content")] == "" { + c.Logger.Error().Str("filename", htmlPath).Msg("Episode file can't be parsed.") + return FourOhFour(c) + } + content := contentMatches[episodeContentRegex.SubexpIndex("content")] + + var res ResponseData + baseData := getBaseData(c) + baseData.Title = title + res.MustWriteTemplate("episode.html", EpisodeData{ + BaseData: baseData, + Content: template.HTML(content), + }, c.Perf) + return res +} + +func topicsForProject(projectSlug string, requestedTopic string) ([]string, string) { + searchPath := path.Join(config.Config.EpisodeGuide.CineraOutputPath, projectSlug) + entries, err := fs.ReadDir(os.DirFS(searchPath), ".") + if err != nil { + return nil, "" + } + var allTopics []string + foundTopic := "" + for _, entry := range entries { + if entry.IsDir() { + t := entry.Name() + allTopics = append(allTopics, t) + if strings.ToLower(t) == strings.ToLower(requestedTopic) { + foundTopic = t + } + } + } + + return allTopics, foundTopic +} + +// NOTE(asaf): Assuming topic is valid. Please verify before calling. +func findEpisode(projectSlug string, topic string, requestedEpisode string) string { + searchPath := path.Join(config.Config.EpisodeGuide.CineraOutputPath, projectSlug, topic, "episode", topic) // NOTE(asaf): Yes. We have `topic` twice in the path. + entries, err := fs.ReadDir(os.DirFS(searchPath), ".") + if err != nil { + return "" + } + for _, entry := range entries { + if entry.IsDir() { + episode := entry.Name() + if strings.ToLower(episode) == strings.ToLower(requestedEpisode) { + return episode + } + } + } + + return "" +} diff --git a/src/website/requesthandling.go b/src/website/requesthandling.go index 7ec26a7..6ffbfe1 100644 --- a/src/website/requesthandling.go +++ b/src/website/requesthandling.go @@ -72,6 +72,11 @@ func (r *Router) ServeHTTP(rw http.ResponseWriter, req *http.Request) { continue } + path = strings.TrimSuffix(path, "/") + if path == "" { + path = "/" + } + match := route.Regex.FindStringSubmatch(path) if match == nil { continue diff --git a/src/website/routes.go b/src/website/routes.go index 963528c..58a86a9 100644 --- a/src/website/routes.go +++ b/src/website/routes.go @@ -192,6 +192,13 @@ func NewWebsiteRoutes(longRequestContext context.Context, conn *pgxpool.Pool, pe mainRoutes.GET(hmnurl.RegexUserProfile, UserProfile) mainRoutes.GET(hmnurl.RegexProjectNotApproved, ProjectHomepage) + mainRoutes.GET(hmnurl.RegexDiscordOAuthCallback, authMiddleware(DiscordOAuthCallback)) + mainRoutes.POST(hmnurl.RegexDiscordUnlink, authMiddleware(csrfMiddleware(DiscordUnlink))) + mainRoutes.POST(hmnurl.RegexDiscordShowcaseBacklog, authMiddleware(csrfMiddleware(DiscordShowcaseBacklog))) + + mainRoutes.GET(hmnurl.RegexUserSettings, authMiddleware(UserSettings)) + mainRoutes.POST(hmnurl.RegexUserSettings, authMiddleware(csrfMiddleware(UserSettingsSave))) + // NOTE(asaf): Any-project routes: mainRoutes.GET(hmnurl.RegexForumNewThread, authMiddleware(ForumNewThread)) mainRoutes.POST(hmnurl.RegexForumNewThreadSubmit, authMiddleware(csrfMiddleware(ForumNewThreadSubmit))) @@ -228,12 +235,9 @@ func NewWebsiteRoutes(longRequestContext context.Context, conn *pgxpool.Pool, pe mainRoutes.GET(hmnurl.RegexPodcastEpisode, PodcastEpisode) mainRoutes.GET(hmnurl.RegexPodcastRSS, PodcastRSS) - mainRoutes.GET(hmnurl.RegexDiscordOAuthCallback, authMiddleware(DiscordOAuthCallback)) - mainRoutes.POST(hmnurl.RegexDiscordUnlink, authMiddleware(csrfMiddleware(DiscordUnlink))) - mainRoutes.POST(hmnurl.RegexDiscordShowcaseBacklog, authMiddleware(csrfMiddleware(DiscordShowcaseBacklog))) - - mainRoutes.GET(hmnurl.RegexUserSettings, authMiddleware(UserSettings)) - mainRoutes.POST(hmnurl.RegexUserSettings, authMiddleware(csrfMiddleware(UserSettingsSave))) + mainRoutes.GET(hmnurl.RegexEpisodeList, EpisodeList) + mainRoutes.GET(hmnurl.RegexEpisode, Episode) + mainRoutes.GET(hmnurl.RegexCineraIndex, CineraIndex) mainRoutes.GET(hmnurl.RegexProjectCSS, ProjectCSS) mainRoutes.GET(hmnurl.RegexEditorPreviewsJS, func(c *RequestContext) ResponseData { @@ -261,6 +265,12 @@ func getBaseData(c *RequestContext) templates.BaseData { notices := getNoticesFromCookie(c) + episodeGuideUrl := "" + defaultTopic, hasAnnotations := config.Config.EpisodeGuide.Projects[c.CurrentProject.Slug] + if hasAnnotations { + episodeGuideUrl = hmnurl.BuildEpisodeList(c.CurrentProject.Slug, defaultTopic) + } + return templates.BaseData{ Theme: c.Theme, @@ -289,7 +299,7 @@ func getBaseData(c *RequestContext) templates.BaseData { ForumsUrl: hmnurl.BuildForum(c.CurrentProject.Slug, nil, 1), LibraryUrl: hmnurl.BuildLibrary(c.CurrentProject.Slug), ManifestoUrl: hmnurl.BuildManifesto(), - EpisodeGuideUrl: hmnurl.BuildHomepage(), // TODO(asaf) + EpisodeGuideUrl: episodeGuideUrl, EditUrl: "", SearchActionUrl: "https://duckduckgo.com", },