hmn/src/hmnurl/urls.go

1193 lines
30 KiB
Go
Raw Normal View History

2021-05-05 20:34:32 +00:00
package hmnurl
import (
"fmt"
2021-05-11 22:53:23 +00:00
"net/url"
2021-05-05 20:34:32 +00:00
"regexp"
"strconv"
"strings"
2021-05-06 04:04:58 +00:00
"git.handmade.network/hmn/hmn/src/logging"
"git.handmade.network/hmn/hmn/src/models"
2021-05-06 04:04:58 +00:00
"git.handmade.network/hmn/hmn/src/oops"
2021-05-05 20:34:32 +00:00
)
/*
Any function in this package whose name starts with Build is required to be covered by a test.
This helps ensure that we don't generate URLs that can't be routed.
*/
2021-08-08 20:05:52 +00:00
var RegexOldHome = regexp.MustCompile("^/home$")
var RegexHomepage = regexp.MustCompile("^/$")
2021-05-05 20:34:32 +00:00
func BuildHomepage() string {
return HMNProjectContext.BuildHomepage()
2021-05-05 20:34:32 +00:00
}
func (c *UrlContext) BuildHomepage() string {
return c.Url("/", nil)
2021-05-11 22:53:23 +00:00
}
var RegexShowcase = regexp.MustCompile("^/showcase$")
func BuildShowcase() string {
defer CatchPanic()
2021-05-11 22:53:23 +00:00
return Url("/showcase", nil)
}
var RegexStreams = regexp.MustCompile("^/streams$")
func BuildStreams() string {
defer CatchPanic()
2021-05-11 22:53:23 +00:00
return Url("/streams", nil)
}
var RegexWhenIsIt = regexp.MustCompile("^/whenisit$")
func BuildWhenIsIt() string {
defer CatchPanic()
return Url("/whenisit", nil)
}
2023-06-02 14:46:26 +00:00
var RegexNewsletterSignup = regexp.MustCompile("^/newsletter$")
func BuildNewsletterSignup() string {
defer CatchPanic()
return Url("/newsletter", nil)
}
2023-04-22 16:26:07 +00:00
var RegexJamsIndex = regexp.MustCompile("^/jams$")
func BuildJamsIndex() string {
defer CatchPanic()
return Url("/jams", nil)
}
2021-08-28 11:26:17 +00:00
var RegexJamIndex = regexp.MustCompile("^/jam$")
func BuildJamIndex() string {
defer CatchPanic()
return Url("/jam", nil)
}
2022-06-19 22:26:33 +00:00
var RegexJamIndex2021 = regexp.MustCompile("^/jam/2021$")
func BuildJamIndex2021() string {
defer CatchPanic()
return Url("/jam/2021", nil)
}
var RegexJamIndex2022 = regexp.MustCompile("^/jam/2022$")
func BuildJamIndex2022() string {
defer CatchPanic()
return Url("/jam/2022", nil)
}
2023-09-01 14:35:40 +00:00
var RegexJamFeed2022 = regexp.MustCompile("^/jam/2022/feed$")
func BuildJamFeed2022() string {
defer CatchPanic()
return Url("/jam/2022/feed", nil)
}
var RegexJamIndex2023 = regexp.MustCompile("^/jam/2023$")
func BuildJamIndex2023() string {
defer CatchPanic()
return Url("/jam/2023", nil)
}
var RegexJamFeed2023 = regexp.MustCompile("^/jam/2023/feed$")
func BuildJamFeed2023() string {
defer CatchPanic()
return Url("/jam/2023/feed", nil)
}
2023-03-07 17:37:01 +00:00
var RegexJamIndex2023_Visibility = regexp.MustCompile("^/jam/visibility-2023$")
2023-03-05 04:52:03 +00:00
2023-03-07 17:37:01 +00:00
func BuildJamIndex2023_Visibility() string {
2023-03-05 04:52:03 +00:00
defer CatchPanic()
2023-03-07 17:37:01 +00:00
return Url("/jam/visibility-2023", nil)
2023-03-05 04:52:03 +00:00
}
2023-04-06 18:54:14 +00:00
var RegexJamFeed2023_Visibility = regexp.MustCompile("^/jam/visibility-2023/feed$")
func BuildJamFeed2023_Visibility() string {
defer CatchPanic()
return Url("/jam/visibility-2023/feed", nil)
}
var RegexJamRecap2023_Visibility = regexp.MustCompile("^/jam/visibility-2023/recap$")
func BuildJamRecap2023_Visibility() string {
defer CatchPanic()
return Url("/jam/visibility-2023/recap", nil)
}
func BuildJamIndexAny(slug string) string {
defer CatchPanic()
return Url(fmt.Sprintf("/jam/%s", slug), nil)
}
2023-06-01 18:56:35 +00:00
var RegexTimeMachine = regexp.MustCompile("^/timemachine$")
2023-05-28 05:16:12 +00:00
func BuildTimeMachine() string {
defer CatchPanic()
2023-06-01 18:56:35 +00:00
return Url("/timemachine", nil)
2023-05-28 05:16:12 +00:00
}
2023-06-06 18:23:54 +00:00
var RegexTimeMachineSubmissions = regexp.MustCompile("^/timemachine/submissions$")
func BuildTimeMachineSubmissions() string {
defer CatchPanic()
return Url("/timemachine/submissions", nil)
}
2023-06-09 20:01:51 +00:00
func BuildTimeMachineSubmission(id int) string {
defer CatchPanic()
return UrlWithFragment("/timemachine/submissions", nil, strconv.Itoa(id))
}
var RegexTimeMachineAtomFeed = regexp.MustCompile("^/timemachine/submissions/atom$")
func BuildTimeMachineAtomFeed() string {
defer CatchPanic()
return Url("/timemachine/submissions/atom", nil)
}
2023-06-01 21:42:46 +00:00
var RegexTimeMachineForm = regexp.MustCompile("^/timemachine/submit$")
func BuildTimeMachineForm() string {
defer CatchPanic()
return Url("/timemachine/submit", nil)
}
var RegexTimeMachineFormDone = regexp.MustCompile("^/timemachine/thanks$")
func BuildTimeMachineFormDone() string {
defer CatchPanic()
return Url("/timemachine/thanks", nil)
}
// QUESTION(ben): Can we change these routes?
2021-05-11 22:53:23 +00:00
var RegexLoginAction = regexp.MustCompile("^/login$")
func BuildLoginAction(redirectTo string) string {
defer CatchPanic()
2021-05-11 22:53:23 +00:00
return Url("/login", []Q{{Name: "redirect", Value: redirectTo}})
}
2021-08-08 20:05:52 +00:00
var RegexLoginPage = regexp.MustCompile("^/login$")
2021-05-11 22:53:23 +00:00
func BuildLoginPage(redirectTo string) string {
defer CatchPanic()
var q []Q
if redirectTo != "" {
q = append(q, Q{Name: "redirect", Value: redirectTo})
}
return Url("/login", q)
2021-05-11 22:53:23 +00:00
}
Add Discord login (#106) This leverages our existing Discord OAuth implementation. Any users with a linked Discord account will be able to log in immediately. When logging in, we request the `email` scope in addition to `identity`, so existing users will be prompted one time to accept the new permissions. On subsequent logins, Discord will skip the prompt. When linking your Discord account to an existing HMN account, we continue to only request the `identity` scope, so we do not receive the user's Discord email. Both login and linking go through the same Discord OAuth callback. All flows through the callback try to achieve the same end goal: a logged-in HMN user with a linked Discord account. Linking works the same as it ever has. Login, however, is different because we do not have a session ID to use as the OAuth state. To account for this, I have added a `pending_login` table that stores a secure unique ID and the eventual destination URL. These pending logins expire after 10 minutes. When we receive the OAuth callback, we look up the pending login by the OAuth `state` and immediately delete it. The destination URL will be used to redirect the user to the right place. If we have a `discord_user` entry for the OAuth'd Discord user, we immediately log the user into the associated HMN account. This is the typical login case. If we do not have a `discord_user`, but there is exactly one HMN user with the same email address as the Discord user, we will link the two accounts and log into the HMN account. (It is possible for multiple HMN accounts to have the same email, because we don't have a uniqueness constraint there. We fail the login in this case rather than link to the wrong account.) Finally, if no associated HMN user exists, a new one will be created. It will use the Discord user's username, email, and avatar. This user will have no password, but they can set or reset a password through the usual flows. Co-authored-by: Ben Visness <bvisness@gmail.com> Reviewed-on: https://git.handmade.network/hmn/hmn/pulls/106
2023-05-06 19:38:50 +00:00
var RegexLoginWithDiscord = regexp.MustCompile("^/login-with-discord$")
func BuildLoginWithDiscord(redirectTo string) string {
defer CatchPanic()
return Url("/login-with-discord", []Q{{Name: "redirect", Value: redirectTo}})
}
2021-05-11 22:53:23 +00:00
var RegexLogoutAction = regexp.MustCompile("^/logout$")
func BuildLogoutAction(redir string) string {
defer CatchPanic()
if redir == "" {
redir = "/"
}
return Url("/logout", []Q{{"redirect", redir}})
2021-05-05 20:34:32 +00:00
}
2021-08-08 20:05:52 +00:00
var RegexRegister = regexp.MustCompile("^/register$")
2021-06-06 23:48:43 +00:00
func BuildRegister(destination string) string {
2021-06-06 23:48:43 +00:00
defer CatchPanic()
var query []Q
if destination != "" {
query = append(query, Q{"destination", destination})
}
return Url("/register", query)
2021-08-08 20:05:52 +00:00
}
var RegexRegistrationSuccess = regexp.MustCompile("^/registered_successfully$")
func BuildRegistrationSuccess() string {
defer CatchPanic()
return Url("/registered_successfully", nil)
}
var RegexEmailConfirmation = regexp.MustCompile("^/email_confirmation/(?P<username>[^/]+)/(?P<token>[^/]+)$")
2022-08-13 19:15:00 +00:00
func BuildEmailConfirmation(username, token string, destination string) string {
2021-08-08 20:05:52 +00:00
defer CatchPanic()
2022-08-13 19:15:00 +00:00
var query []Q
if destination != "" {
query = append(query, Q{"destination", destination})
}
return Url(fmt.Sprintf("/email_confirmation/%s/%s", url.PathEscape(username), token), query)
2021-08-08 20:05:52 +00:00
}
2021-08-17 05:18:04 +00:00
var RegexRequestPasswordReset = regexp.MustCompile("^/password_reset$")
2021-08-08 20:05:52 +00:00
2021-08-17 05:18:04 +00:00
func BuildRequestPasswordReset() string {
2021-08-08 20:05:52 +00:00
defer CatchPanic()
return Url("/password_reset", nil)
2021-06-06 23:48:43 +00:00
}
2021-08-17 05:18:04 +00:00
var RegexPasswordResetSent = regexp.MustCompile("^/password_reset/sent$")
func BuildPasswordResetSent() string {
defer CatchPanic()
return Url("/password_reset/sent", nil)
}
var RegexOldDoPasswordReset = regexp.MustCompile(`^_password_reset/(?P<username>[\w\ \.\,\-@\+\_]+)/(?P<token>[\d\w]+)[\/]?$`)
var RegexDoPasswordReset = regexp.MustCompile("^/password_reset/(?P<username>[^/]+)/(?P<token>[^/]+)$")
func BuildDoPasswordReset(username string, token string) string {
defer CatchPanic()
return Url(fmt.Sprintf("/password_reset/%s/%s", url.PathEscape(username), token), nil)
}
2021-05-11 22:53:23 +00:00
/*
* Static Pages
*/
var RegexManifesto = regexp.MustCompile("^/manifesto$")
2021-05-05 20:34:32 +00:00
func BuildManifesto() string {
defer CatchPanic()
2021-05-05 20:34:32 +00:00
return Url("/manifesto", nil)
}
var RegexAbout = regexp.MustCompile("^/about$")
2021-05-05 20:34:32 +00:00
func BuildAbout() string {
defer CatchPanic()
2021-05-05 20:34:32 +00:00
return Url("/about", nil)
}
var RegexFoundation = regexp.MustCompile("^/foundation$")
func BuildFoundation() string {
defer CatchPanic()
return Url("/foundation", nil)
}
var RegexCommunicationGuidelines = regexp.MustCompile("^/communication-guidelines$")
2021-05-05 20:34:32 +00:00
func BuildCommunicationGuidelines() string {
defer CatchPanic()
2021-05-05 20:34:32 +00:00
return Url("/communication-guidelines", nil)
}
var RegexContactPage = regexp.MustCompile("^/contact$")
2021-05-05 20:34:32 +00:00
func BuildContactPage() string {
defer CatchPanic()
2021-05-05 20:34:32 +00:00
return Url("/contact", nil)
}
var RegexMonthlyUpdatePolicy = regexp.MustCompile("^/monthly-update-policy$")
2021-05-05 20:34:32 +00:00
func BuildMonthlyUpdatePolicy() string {
defer CatchPanic()
2021-05-05 20:34:32 +00:00
return Url("/monthly-update-policy", nil)
}
var RegexProjectSubmissionGuidelines = regexp.MustCompile("^/project-guidelines$")
2021-05-05 20:34:32 +00:00
func BuildProjectSubmissionGuidelines() string {
defer CatchPanic()
2021-05-05 20:34:32 +00:00
return Url("/project-guidelines", nil)
}
var RegexConferences = regexp.MustCompile("^/conferences$")
func BuildConferences() string {
defer CatchPanic()
return Url("/conferences", nil)
}
/*
* Volunteer/Staff Roles
*/
var RegexStaffRolesIndex = regexp.MustCompile(`^/roles$`)
func BuildStaffRolesIndex() string {
defer CatchPanic()
return Url("/roles", nil)
}
var RegexStaffRole = regexp.MustCompile(`^/roles/(?P<slug>[^/]+)$`)
func BuildStaffRole(slug string) string {
defer CatchPanic()
return Url(fmt.Sprintf("/roles/%s", slug), nil)
}
2021-05-11 22:53:23 +00:00
/*
2021-06-22 09:50:40 +00:00
* User
2021-05-11 22:53:23 +00:00
*/
2021-06-22 09:50:40 +00:00
var RegexUserProfile = regexp.MustCompile(`^/m/(?P<username>[^/]+)$`)
2021-05-11 22:53:23 +00:00
2021-06-22 09:50:40 +00:00
func BuildUserProfile(username string) string {
defer CatchPanic()
2021-05-11 22:53:23 +00:00
if len(username) == 0 {
panic(oops.New(nil, "Username must not be blank"))
}
2021-12-21 04:24:05 +00:00
return Url("/m/"+username, nil)
2021-08-08 20:05:52 +00:00
}
var RegexUserSettings = regexp.MustCompile(`^/settings$`)
2021-08-17 05:18:04 +00:00
func BuildUserSettings(section string) string {
return UrlWithFragment("/settings", nil, section)
2021-05-11 22:53:23 +00:00
}
2021-09-24 00:12:46 +00:00
/*
* Admin
*/
var RegexAdminAtomFeed = regexp.MustCompile(`^/admin/atom$`)
func BuildAdminAtomFeed() string {
defer CatchPanic()
return Url("/admin/atom", nil)
}
var RegexAdminApprovalQueue = regexp.MustCompile(`^/admin/approvals$`)
func BuildAdminApprovalQueue() string {
defer CatchPanic()
return Url("/admin/approvals", nil)
}
var RegexAdminSetUserOptions = regexp.MustCompile(`^/admin/setuseroptions$`)
2021-12-15 01:17:42 +00:00
func BuildAdminSetUserOptions() string {
2021-12-15 01:17:42 +00:00
defer CatchPanic()
return Url("/admin/setuseroptions", nil)
2021-12-15 01:17:42 +00:00
}
var RegexAdminNukeUser = regexp.MustCompile(`^/admin/nukeuser$`)
func BuildAdminNukeUser() string {
defer CatchPanic()
return Url("/admin/nukeuser", nil)
}
2021-06-22 09:50:40 +00:00
/*
* Snippets
*/
var RegexSnippet = regexp.MustCompile(`^/snippet/(?P<snippetid>\d+)$`)
func BuildSnippet(snippetId int) string {
defer CatchPanic()
return Url("/snippet/"+strconv.Itoa(snippetId), nil)
}
2022-08-05 04:03:45 +00:00
var RegexSnippetSubmit = regexp.MustCompile(`^/snippet$`)
func BuildSnippetSubmit() string {
defer CatchPanic()
return Url("/snippet", nil)
}
2021-05-11 22:53:23 +00:00
/*
* Feed
*/
var RegexFeed = regexp.MustCompile(`^/feed(/(?P<page>.+)?)?$`)
2021-05-05 20:34:32 +00:00
func BuildFeed() string {
defer CatchPanic()
2021-05-05 20:34:32 +00:00
return Url("/feed", nil)
}
func BuildFeedWithPage(page int) string {
defer CatchPanic()
2021-05-05 20:34:32 +00:00
if page < 1 {
panic(oops.New(nil, "Invalid feed page (%d), must be >= 1", page))
}
if page == 1 {
return BuildFeed()
}
return Url("/feed/"+strconv.Itoa(page), nil)
}
var RegexAtomFeed = regexp.MustCompile("^/atom(/(?P<feedtype>[^/]+))?(/new)?$") // NOTE(asaf): `/new` for backwards compatibility with old website
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)
}
2021-06-06 23:48:43 +00:00
/*
* Projects
*/
var RegexProjectIndex = regexp.MustCompile(`^/projects(/(?P<category>[a-z][a-z0-9]+))?(/(?P<page>\d+))?$`)
2021-06-06 23:48:43 +00:00
func BuildProjectIndex(page int, category string) string {
2021-06-06 23:48:43 +00:00
defer CatchPanic()
if page < 1 {
panic(oops.New(nil, "page must be >= 1"))
}
catpath := ""
if category != "" {
catpath = "/" + category
}
2021-06-06 23:48:43 +00:00
if page == 1 {
return Url(fmt.Sprintf("/projects%s", catpath), nil)
2021-06-06 23:48:43 +00:00
} else {
return Url(fmt.Sprintf("/projects%s/%d", catpath, page), nil)
2021-06-06 23:48:43 +00:00
}
}
2021-11-25 03:59:51 +00:00
var RegexProjectNew = regexp.MustCompile("^/p/new$")
2021-06-06 23:48:43 +00:00
func BuildProjectNew() string {
defer CatchPanic()
2021-11-25 03:59:51 +00:00
return Url("/p/new", nil)
2021-06-06 23:48:43 +00:00
}
2022-06-25 13:24:04 +00:00
func BuildProjectNewJam() string {
defer CatchPanic()
return Url("/p/new", []Q{{Name: "jam", Value: "1"}})
2022-06-25 13:24:04 +00:00
}
2021-11-10 17:34:48 +00:00
var RegexPersonalProject = regexp.MustCompile("^/p/(?P<projectid>[0-9]+)(/(?P<projectslug>[a-zA-Z0-9-]+))?")
2021-06-06 23:48:43 +00:00
func BuildPersonalProject(id int, slug string) string {
2021-06-06 23:48:43 +00:00
defer CatchPanic()
2021-11-08 19:16:54 +00:00
return Url(fmt.Sprintf("/p/%d/%s", id, slug), nil)
2021-06-06 23:48:43 +00:00
}
var RegexProjectEdit = regexp.MustCompile("^/edit$")
2021-07-08 07:40:30 +00:00
func (c *UrlContext) BuildProjectEdit(section string) string {
2021-07-08 07:40:30 +00:00
defer CatchPanic()
return c.UrlWithFragment("/edit", nil, section)
2021-07-08 07:40:30 +00:00
}
2021-05-11 22:53:23 +00:00
/*
* Podcast
*/
var RegexPodcast = regexp.MustCompile(`^/podcast$`)
func BuildPodcast() string {
defer CatchPanic()
return Url("/podcast", nil)
2021-05-11 22:53:23 +00:00
}
2021-07-23 03:09:46 +00:00
var RegexPodcastEdit = regexp.MustCompile(`^/podcast/edit$`)
func BuildPodcastEdit() string {
2021-07-23 03:09:46 +00:00
defer CatchPanic()
return Url("/podcast/edit", nil)
2021-07-23 03:09:46 +00:00
}
var RegexPodcastEpisode = regexp.MustCompile(`^/podcast/ep/(?P<episodeid>[^/]+)$`)
func BuildPodcastEpisode(episodeGUID string) string {
2021-07-23 03:09:46 +00:00
defer CatchPanic()
return Url(fmt.Sprintf("/podcast/ep/%s", episodeGUID), nil)
2021-07-23 03:09:46 +00:00
}
var RegexPodcastEpisodeNew = regexp.MustCompile(`^/podcast/ep/new$`)
func BuildPodcastEpisodeNew() string {
2021-07-23 03:09:46 +00:00
defer CatchPanic()
return Url("/podcast/ep/new", nil)
2021-07-23 03:09:46 +00:00
}
var RegexPodcastEpisodeEdit = regexp.MustCompile(`^/podcast/ep/(?P<episodeid>[^/]+)/edit$`)
func BuildPodcastEpisodeEdit(episodeGUID string) string {
2021-07-23 03:09:46 +00:00
defer CatchPanic()
return Url(fmt.Sprintf("/podcast/ep/%s/edit", episodeGUID), nil)
2021-07-23 03:09:46 +00:00
}
var RegexPodcastRSS = regexp.MustCompile(`^/podcast/podcast.xml$`)
func BuildPodcastRSS() string {
2021-07-23 03:09:46 +00:00
defer CatchPanic()
return Url("/podcast/podcast.xml", nil)
2021-07-23 03:09:46 +00:00
}
func BuildPodcastEpisodeFile(filename string) string {
2021-07-23 03:09:46 +00:00
defer CatchPanic()
return BuildUserFile(fmt.Sprintf("podcast/%s/%s", models.HMNProjectSlug, filename))
2021-07-23 03:09:46 +00:00
}
/*
* Fishbowls
*/
var RegexFishbowlIndex = regexp.MustCompile(`^/fishbowl$`)
func BuildFishbowlIndex() string {
defer CatchPanic()
return Url("/fishbowl", nil)
}
var RegexFishbowl = regexp.MustCompile(`^/fishbowl/(?P<slug>[^/]+)/?$`)
func BuildFishbowl(slug string) string {
defer CatchPanic()
return Url(fmt.Sprintf("/fishbowl/%s/", slug), nil)
}
var RegexFishbowlFiles = regexp.MustCompile(`^/fishbowl/(?P<slug>[^/]+)(?P<path>/.+)$`)
/*
* Education
*/
var RegexEducationIndex = regexp.MustCompile(`^/education$`)
func BuildEducationIndex() string {
defer CatchPanic()
return Url("/education", nil)
}
var RegexEducationGlossary = regexp.MustCompile(`^/education/glossary(/(?P<slug>[^/]+))?$`)
func BuildEducationGlossary(termSlug string) string {
defer CatchPanic()
if termSlug == "" {
return Url("/education/glossary", nil)
} else {
return Url(fmt.Sprintf("/education/glossary/%s", termSlug), nil)
}
}
var RegexEducationArticle = regexp.MustCompile(`^/education/(?P<slug>[^/]+)$`)
func BuildEducationArticle(slug string) string {
return Url(fmt.Sprintf("/education/%s", slug), nil)
}
var RegexEducationArticleNew = regexp.MustCompile(`^/education/new$`)
func BuildEducationArticleNew() string {
return Url("/education/new", nil)
}
var RegexEducationArticleEdit = regexp.MustCompile(`^/education/(?P<slug>[^/]+)/edit$`)
func BuildEducationArticleEdit(slug string) string {
return Url(fmt.Sprintf("/education/%s/edit", slug), nil)
}
var RegexEducationArticleDelete = regexp.MustCompile(`^/education/(?P<slug>[^/]+)/delete$`)
func BuildEducationArticleDelete(slug string) string {
return Url(fmt.Sprintf("/education/%s/delete", slug), nil)
}
var RegexEducationRerender = regexp.MustCompile(`^/education/rerender$`)
func BuildEducationRerender() string {
return Url("/education/rerender", nil)
}
2021-05-11 22:53:23 +00:00
/*
* Forums
*/
2021-08-28 17:07:45 +00:00
// NOTE(asaf): This also matches urls generated by BuildForumThread (/t/ is identified as a subforum, and the threadid as a page)
// Make sure to match Thread before Subforum in the router.
var RegexForum = regexp.MustCompile(`^/forums(/(?P<subforums>[^\d/]+(/[^\d]+)*))?(/(?P<page>\d+))?$`)
2021-05-05 20:34:32 +00:00
func (c *UrlContext) Url(path string, query []Q) string {
return c.UrlWithFragment(path, query, "")
}
func (c *UrlContext) UrlWithFragment(path string, query []Q, fragment string) string {
if c == nil {
logging.Warn().Stack().Msg("URL context was nil; defaulting to the HMN URL context")
c = &HMNProjectContext
}
if c.PersonalProject {
url := url.URL{
Scheme: baseUrlParsed.Scheme,
Host: baseUrlParsed.Host,
Path: fmt.Sprintf("p/%d/%s/%s", c.ProjectID, models.GeneratePersonalProjectSlug(c.ProjectName), trim(path)),
RawQuery: encodeQuery(query),
Fragment: fragment,
}
return url.String()
} else {
subdomain := c.ProjectSlug
if c.ProjectSlug == models.HMNProjectSlug {
subdomain = ""
}
host := baseUrlParsed.Host
if len(subdomain) > 0 {
host = c.ProjectSlug + "." + host
}
url := url.URL{
Scheme: baseUrlParsed.Scheme,
Host: host,
Path: trim(path),
RawQuery: encodeQuery(query),
Fragment: fragment,
}
return url.String()
}
}
func (c *UrlContext) BuildForum(subforums []string, page int) string {
defer CatchPanic()
2021-05-05 20:34:32 +00:00
if page < 1 {
panic(oops.New(nil, "Invalid forum thread page (%d), must be >= 1", page))
}
builder := buildSubforumPath(subforums)
2021-05-11 22:53:23 +00:00
2021-05-05 20:34:32 +00:00
if page > 1 {
builder.WriteRune('/')
builder.WriteString(strconv.Itoa(page))
}
return c.Url(builder.String(), nil)
2021-05-05 20:34:32 +00:00
}
var RegexForumNewThread = regexp.MustCompile(`^/forums(/(?P<subforums>[^\d/]+(/[^\d]+)*))?/t/new$`)
var RegexForumNewThreadSubmit = regexp.MustCompile(`^/forums(/(?P<subforums>[^\d/]+(/[^\d]+)*))?/t/new/submit$`)
2021-05-05 20:34:32 +00:00
func (c *UrlContext) BuildForumNewThread(subforums []string, submit bool) string {
defer CatchPanic()
builder := buildSubforumPath(subforums)
builder.WriteString("/t/new")
if submit {
builder.WriteString("/submit")
}
2021-05-05 20:34:32 +00:00
return c.Url(builder.String(), nil)
2021-05-11 22:53:23 +00:00
}
var RegexForumThread = regexp.MustCompile(`^/forums(/(?P<subforums>[^\d/]+(/[^\d]+)*))?/t/(?P<threadid>\d+)(-([^/]+))?(/(?P<page>\d+))?$`)
2021-05-11 22:53:23 +00:00
func (c *UrlContext) BuildForumThread(subforums []string, threadId int, title string, page int) string {
defer CatchPanic()
2021-05-11 22:53:23 +00:00
builder := buildForumThreadPath(subforums, threadId, title, page)
2021-05-05 20:34:32 +00:00
return c.Url(builder.String(), nil)
2021-05-05 20:34:32 +00:00
}
func (c *UrlContext) BuildForumThreadWithPostHash(subforums []string, threadId int, title string, page int, postId int) string {
defer CatchPanic()
2021-05-11 22:53:23 +00:00
builder := buildForumThreadPath(subforums, threadId, title, page)
2021-11-10 17:34:48 +00:00
return c.UrlWithFragment(builder.String(), nil, strconv.Itoa(postId))
2021-05-11 22:53:23 +00:00
}
var RegexForumPost = regexp.MustCompile(`^/forums(/(?P<subforums>[^\d/]+(/[^\d]+)*))?/t/(?P<threadid>\d+)/p/(?P<postid>\d+)$`)
2021-05-05 20:34:32 +00:00
func (c *UrlContext) BuildForumPost(subforums []string, threadId int, postId int) string {
defer CatchPanic()
2021-05-11 22:53:23 +00:00
builder := buildForumPostPath(subforums, threadId, postId)
2021-05-05 20:34:32 +00:00
return c.Url(builder.String(), nil)
2021-05-05 20:34:32 +00:00
}
var RegexForumPostDelete = regexp.MustCompile(`^/forums(/(?P<subforums>[^\d/]+(/[^\d]+)*))?/t/(?P<threadid>\d+)/p/(?P<postid>\d+)/delete$`)
func (c *UrlContext) BuildForumPostDelete(subforums []string, threadId int, postId int) string {
defer CatchPanic()
2021-05-11 22:53:23 +00:00
builder := buildForumPostPath(subforums, threadId, postId)
builder.WriteString("/delete")
return c.Url(builder.String(), nil)
}
var RegexForumPostEdit = regexp.MustCompile(`^/forums(/(?P<subforums>[^\d/]+(/[^\d]+)*))?/t/(?P<threadid>\d+)/p/(?P<postid>\d+)/edit$`)
func (c *UrlContext) BuildForumPostEdit(subforums []string, threadId int, postId int) string {
defer CatchPanic()
2021-05-11 22:53:23 +00:00
builder := buildForumPostPath(subforums, threadId, postId)
builder.WriteString("/edit")
return c.Url(builder.String(), nil)
}
var RegexForumPostReply = regexp.MustCompile(`^/forums(/(?P<subforums>[^\d/]+(/[^\d]+)*))?/t/(?P<threadid>\d+)/p/(?P<postid>\d+)/reply$`)
func (c *UrlContext) BuildForumPostReply(subforums []string, threadId int, postId int) string {
defer CatchPanic()
2021-05-11 22:53:23 +00:00
builder := buildForumPostPath(subforums, threadId, postId)
builder.WriteString("/reply")
return c.Url(builder.String(), nil)
}
2021-09-09 00:43:24 +00:00
var RegexWikiArticle = regexp.MustCompile(`^/wiki/(?P<threadid>\d+)(-([^/]+))?$`)
2021-05-11 22:53:23 +00:00
/*
* Blog
*/
2021-09-20 15:17:53 +00:00
var RegexBlogsRedirect = regexp.MustCompile(`^/blogs(?P<remainder>.*)`)
2021-05-11 22:53:23 +00:00
var RegexBlog = regexp.MustCompile(`^/blog(/(?P<page>\d+))?$`)
func (c *UrlContext) BuildBlog(page int) string {
defer CatchPanic()
2021-05-11 22:53:23 +00:00
if page < 1 {
panic(oops.New(nil, "Invalid blog page (%d), must be >= 1", page))
}
path := "/blog"
if page > 1 {
path += "/" + strconv.Itoa(page)
}
return c.Url(path, nil)
2021-05-11 22:53:23 +00:00
}
2021-07-30 22:32:19 +00:00
var RegexBlogThread = regexp.MustCompile(`^/blog/p/(?P<threadid>\d+)(-([^/]+))?$`)
2021-05-11 22:53:23 +00:00
func (c *UrlContext) BuildBlogThread(threadId int, title string) string {
defer CatchPanic()
2021-07-30 22:32:19 +00:00
builder := buildBlogThreadPath(threadId, title)
return c.Url(builder.String(), nil)
2021-05-11 22:53:23 +00:00
}
func (c *UrlContext) BuildBlogThreadWithPostHash(threadId int, title string, postId int) string {
defer CatchPanic()
2021-07-30 22:32:19 +00:00
builder := buildBlogThreadPath(threadId, title)
return c.UrlWithFragment(builder.String(), nil, strconv.Itoa(postId))
2021-05-11 22:53:23 +00:00
}
var RegexBlogNewThread = regexp.MustCompile(`^/blog/new$`)
func (c *UrlContext) BuildBlogNewThread() string {
defer CatchPanic()
return c.Url("/blog/new", nil)
}
2021-05-11 22:53:23 +00:00
var RegexBlogPost = regexp.MustCompile(`^/blog/p/(?P<threadid>\d+)/e/(?P<postid>\d+)$`)
func (c *UrlContext) BuildBlogPost(threadId int, postId int) string {
defer CatchPanic()
2021-05-11 22:53:23 +00:00
builder := buildBlogPostPath(threadId, postId)
return c.Url(builder.String(), nil)
2021-05-11 22:53:23 +00:00
}
var RegexBlogPostDelete = regexp.MustCompile(`^/blog/p/(?P<threadid>\d+)/e/(?P<postid>\d+)/delete$`)
func (c *UrlContext) BuildBlogPostDelete(threadId int, postId int) string {
defer CatchPanic()
2021-05-11 22:53:23 +00:00
builder := buildBlogPostPath(threadId, postId)
builder.WriteString("/delete")
return c.Url(builder.String(), nil)
2021-05-11 22:53:23 +00:00
}
var RegexBlogPostEdit = regexp.MustCompile(`^/blog/p/(?P<threadid>\d+)/e/(?P<postid>\d+)/edit$`)
func (c *UrlContext) BuildBlogPostEdit(threadId int, postId int) string {
defer CatchPanic()
2021-05-11 22:53:23 +00:00
builder := buildBlogPostPath(threadId, postId)
builder.WriteString("/edit")
return c.Url(builder.String(), nil)
2021-05-11 22:53:23 +00:00
}
var RegexBlogPostReply = regexp.MustCompile(`^/blog/p/(?P<threadid>\d+)/e/(?P<postid>\d+)/reply$`)
func (c *UrlContext) BuildBlogPostReply(threadId int, postId int) string {
defer CatchPanic()
2021-05-11 22:53:23 +00:00
builder := buildBlogPostPath(threadId, postId)
builder.WriteString("/reply")
return c.Url(builder.String(), nil)
2021-05-11 22:53:23 +00:00
}
/*
2022-11-05 21:23:12 +00:00
* Library (old)
2021-05-11 22:53:23 +00:00
*/
2021-10-23 16:14:10 +00:00
var RegexLibraryAny = regexp.MustCompile(`^/library`)
/*
* Episode Guide
*/
var RegexEpisodeList = regexp.MustCompile(`^/episode(/(?P<topic>[^/]+))?$`)
func (c *UrlContext) BuildEpisodeList(topic string) string {
defer CatchPanic()
var builder strings.Builder
builder.WriteString("/episode")
if topic != "" {
builder.WriteString("/")
builder.WriteString(topic)
}
return c.Url(builder.String(), nil)
}
var RegexEpisode = regexp.MustCompile(`^/episode/(?P<topic>[^/]+)/(?P<episode>[^/]+)$`)
func (c *UrlContext) BuildEpisode(topic string, episode string) string {
defer CatchPanic()
return c.Url(fmt.Sprintf("/episode/%s/%s", topic, episode), nil)
}
var RegexCineraIndex = regexp.MustCompile(`^/(?P<topic>[^/]+).index$`)
func (c *UrlContext) BuildCineraIndex(topic string) string {
defer CatchPanic()
return c.Url(fmt.Sprintf("/%s.index", topic), nil)
}
2021-08-16 04:40:56 +00:00
/*
* Discord OAuth
*/
var RegexDiscordOAuthCallback = regexp.MustCompile("^/_discord_callback$")
func BuildDiscordOAuthCallback() string {
return Url("/_discord_callback", nil)
}
2021-08-16 05:07:17 +00:00
var RegexDiscordUnlink = regexp.MustCompile("^/_discord_unlink$")
func BuildDiscordUnlink() string {
return Url("/_discord_unlink", nil)
}
var RegexDiscordShowcaseBacklog = regexp.MustCompile("^/discord_showcase_backlog$")
func BuildDiscordShowcaseBacklog() string {
return Url("/discord_showcase_backlog", nil)
}
2021-11-25 03:59:51 +00:00
/*
* API
*/
var RegexAPICheckUsername = regexp.MustCompile("^/api/check_username$")
func BuildAPICheckUsername() string {
return Url("/api/check_username", nil)
}
2022-03-22 18:07:43 +00:00
/*
* Twitch stuff
*/
var RegexTwitchEventSubCallback = regexp.MustCompile("^/twitch_eventsub$")
func BuildTwitchEventSubCallback() string {
return Url("/twitch_eventsub", nil)
}
var RegexTwitchDebugPage = regexp.MustCompile("^/twitch_debug$")
/*
* User assets
*/
var RegexAssetUpload = regexp.MustCompile("^/upload_asset$")
// NOTE(asaf): Providing the projectSlug avoids any CORS problems.
func (c *UrlContext) BuildAssetUpload() string {
return c.Url("/upload_asset", nil)
}
2021-05-11 22:53:23 +00:00
/*
* Assets
*/
var RegexProjectCSS = regexp.MustCompile("^/assets/project.css$")
2021-05-05 20:34:32 +00:00
func BuildProjectCSS(color string) string {
defer CatchPanic()
2021-05-06 04:04:58 +00:00
return Url("/assets/project.css", []Q{{"color", color}})
2021-05-05 20:34:32 +00:00
}
var RegexMarkdownWorkerJS = regexp.MustCompile("^/assets/markdown_worker.js$")
2021-07-30 22:32:19 +00:00
func BuildMarkdownWorkerJS() string {
2021-07-30 22:32:19 +00:00
defer CatchPanic()
return Url("/assets/markdown_worker.js", nil)
2021-07-30 22:32:19 +00:00
}
2021-09-22 19:18:39 +00:00
var RegexS3Asset *regexp.Regexp
2021-06-22 09:50:40 +00:00
func BuildS3Asset(s3key string) string {
defer CatchPanic()
2021-09-22 19:18:39 +00:00
res := fmt.Sprintf("%s%s", S3BaseUrl, s3key)
return res
2021-06-22 09:50:40 +00:00
}
var RegexPublic = regexp.MustCompile("^/public/.+$")
2021-05-05 20:34:32 +00:00
func BuildPublic(filepath string, cachebust bool) string {
defer CatchPanic()
2021-05-05 20:34:32 +00:00
filepath = strings.Trim(filepath, "/")
if len(strings.TrimSpace(filepath)) == 0 {
panic(oops.New(nil, "Attempted to build a /public url with no path"))
}
if strings.Contains(filepath, "?") {
panic(oops.New(nil, "Public url failpath must not contain query params"))
}
2021-05-05 20:34:32 +00:00
var builder strings.Builder
builder.WriteString("/public")
pathParts := strings.Split(filepath, "/")
for _, part := range pathParts {
part = strings.TrimSpace(part)
if len(part) == 0 {
panic(oops.New(nil, "Attempted to build a /public url with blank path segments: %s", filepath))
}
builder.WriteRune('/')
builder.WriteString(part)
}
var query []Q
if cachebust {
query = []Q{{"v", cacheBustVersion}}
}
return Url(builder.String(), query)
}
func BuildTheme(filepath string, theme string, cachebust bool) string {
defer CatchPanic()
filepath = strings.Trim(filepath, "/")
if len(theme) == 0 {
panic(oops.New(nil, "Theme can't be blank"))
}
return BuildPublic(fmt.Sprintf("themes/%s/%s", theme, filepath), cachebust)
}
func BuildUserFile(filepath string) string {
if filepath == "" {
return ""
}
filepath = strings.Trim(filepath, "/")
return BuildPublic(fmt.Sprintf("media/%s", filepath), false)
2021-05-05 20:34:32 +00:00
}
2021-05-11 22:53:23 +00:00
/*
* Other
*/
var RegexForumMarkRead = regexp.MustCompile(`^/markread/(?P<sfid>\d+)$`)
2021-05-11 22:53:23 +00:00
// NOTE(asaf): subforumId == 0 means ALL SUBFORUMS
func (c *UrlContext) BuildForumMarkRead(subforumId int) string {
defer CatchPanic()
if subforumId < 0 {
panic(oops.New(nil, "Invalid subforum ID (%d), must be >= 0", subforumId))
2021-05-11 22:53:23 +00:00
}
var builder strings.Builder
2021-07-23 19:00:37 +00:00
builder.WriteString("/markread/")
builder.WriteString(strconv.Itoa(subforumId))
2021-05-11 22:53:23 +00:00
return c.Url(builder.String(), nil)
2021-05-11 22:53:23 +00:00
}
var RegexCatchAll = regexp.MustCompile("^")
2021-05-11 22:53:23 +00:00
/*
* Helper functions
*/
func buildSubforumPath(subforums []string) *strings.Builder {
2021-05-11 22:53:23 +00:00
for _, subforum := range subforums {
if strings.Contains(subforum, "/") {
panic(oops.New(nil, "Tried building forum url with / in subforum name"))
}
subforum = strings.TrimSpace(subforum)
if len(subforum) == 0 {
panic(oops.New(nil, "Tried building forum url with blank subforum"))
}
}
var builder strings.Builder
builder.WriteString("/forums")
for _, subforum := range subforums {
builder.WriteRune('/')
builder.WriteString(subforum)
}
return &builder
}
func buildForumThreadPath(subforums []string, threadId int, title string, page int) *strings.Builder {
if page < 1 {
panic(oops.New(nil, "Invalid forum thread page (%d), must be >= 1", page))
}
if threadId < 1 {
panic(oops.New(nil, "Invalid forum thread ID (%d), must be >= 1", threadId))
}
builder := buildSubforumPath(subforums)
2021-05-11 22:53:23 +00:00
builder.WriteString("/t/")
builder.WriteString(strconv.Itoa(threadId))
if len(title) > 0 {
builder.WriteRune('-')
builder.WriteString(PathSafeTitle(title))
}
if page > 1 {
builder.WriteRune('/')
builder.WriteString(strconv.Itoa(page))
}
return builder
}
func buildForumPostPath(subforums []string, threadId int, postId int) *strings.Builder {
if threadId < 1 {
panic(oops.New(nil, "Invalid forum thread ID (%d), must be >= 1", threadId))
}
if postId < 1 {
panic(oops.New(nil, "Invalid forum post ID (%d), must be >= 1", postId))
}
builder := buildSubforumPath(subforums)
2021-05-11 22:53:23 +00:00
builder.WriteString("/t/")
builder.WriteString(strconv.Itoa(threadId))
builder.WriteString("/p/")
builder.WriteString(strconv.Itoa(postId))
return builder
}
2021-07-30 22:32:19 +00:00
func buildBlogThreadPath(threadId int, title string) *strings.Builder {
2021-05-11 22:53:23 +00:00
if threadId < 1 {
panic(oops.New(nil, "Invalid blog thread ID (%d), must be >= 1", threadId))
}
var builder strings.Builder
builder.WriteString("/blog/p/")
builder.WriteString(strconv.Itoa(threadId))
if len(title) > 0 {
builder.WriteRune('-')
builder.WriteString(PathSafeTitle(title))
}
return &builder
}
func buildBlogPostPath(threadId int, postId int) *strings.Builder {
if threadId < 1 {
panic(oops.New(nil, "Invalid blog thread ID (%d), must be >= 1", threadId))
}
if postId < 1 {
panic(oops.New(nil, "Invalid blog post ID (%d), must be >= 1", postId))
}
var builder strings.Builder
builder.WriteString("/blog/p/")
builder.WriteString(strconv.Itoa(threadId))
builder.WriteString("/e/")
builder.WriteString(strconv.Itoa(postId))
return &builder
}
func buildLibraryResourcePath(resourceId int) *strings.Builder {
if resourceId < 1 {
panic(oops.New(nil, "Invalid library resource ID (%d), must be >= 1", resourceId))
}
var builder strings.Builder
builder.WriteString("/library/resource/")
builder.WriteString(strconv.Itoa(resourceId))
return &builder
}
func buildLibraryDiscussionPath(resourceId int, threadId int, page int) *strings.Builder {
if page < 1 {
panic(oops.New(nil, "Invalid page number (%d), must be >= 1", page))
}
if threadId < 1 {
panic(oops.New(nil, "Invalid library thread ID (%d), must be >= 1", threadId))
}
builder := buildLibraryResourcePath(resourceId)
builder.WriteString("/d/")
builder.WriteString(strconv.Itoa(threadId))
if page > 1 {
builder.WriteRune('/')
builder.WriteString(strconv.Itoa(page))
}
return builder
}
func buildLibraryPostPath(resourceId int, threadId int, postId int) *strings.Builder {
if threadId < 1 {
panic(oops.New(nil, "Invalid library thread ID (%d), must be >= 1", threadId))
}
if postId < 1 {
panic(oops.New(nil, "Invalid library post ID (%d), must be >= 1", postId))
}
builder := buildLibraryResourcePath(resourceId)
builder.WriteString("/d/")
builder.WriteString(strconv.Itoa(threadId))
builder.WriteString("/p/")
builder.WriteString(strconv.Itoa(postId))
return builder
}
var PathCharsToClear = regexp.MustCompile("[$&`<>{}()\\[\\]\"+#%@;=?\\\\^|~]")
var PathCharsToReplace = regexp.MustCompile("[ :/\\\\]")
func PathSafeTitle(title string) string {
title = strings.ToLower(title)
title = PathCharsToReplace.ReplaceAllLiteralString(title, "_")
title = PathCharsToClear.ReplaceAllLiteralString(title, "")
title = url.PathEscape(title)
return title
}
2021-05-25 13:51:49 +00:00
// TODO(asaf): Find a nicer solution that doesn't require adding a defer to every construction function while also not printing errors in tests.
func CatchPanic() {
if !isTest {
if recovered := recover(); recovered != nil {
logging.LogPanicValue(nil, recovered, "Url construction failed")
}
}
}