Monorepo for Tangled tangled.org
6

Configure Feed

Select the types of activity you want to include in your feed.

at icy/ytnwlw 14 kB View raw
1package state 2 3import ( 4 "context" 5 "database/sql" 6 "errors" 7 "log/slog" 8 "net/http" 9 "strings" 10 11 "github.com/go-chi/chi/v5" 12 "tangled.org/core/appview/config" 13 "tangled.org/core/appview/db" 14 "tangled.org/core/appview/focus" 15 "tangled.org/core/appview/issues" 16 "tangled.org/core/appview/knotacl" 17 "tangled.org/core/appview/knots" 18 "tangled.org/core/appview/labels" 19 "tangled.org/core/appview/metrics" 20 "tangled.org/core/appview/middleware" 21 "tangled.org/core/appview/migration" 22 "tangled.org/core/appview/notifications" 23 "tangled.org/core/appview/pipelines" 24 "tangled.org/core/appview/pulls" 25 "tangled.org/core/appview/repo" 26 "tangled.org/core/appview/settings" 27 "tangled.org/core/appview/signup" 28 "tangled.org/core/appview/spindles" 29 "tangled.org/core/appview/state/userutil" 30 avstrings "tangled.org/core/appview/strings" 31 avtimeline "tangled.org/core/appview/timeline" 32 "tangled.org/core/blog" 33 "tangled.org/core/log" 34) 35 36// newDispatchHandler builds the /* catch-all handler. It is a standalone 37// function so that it can be tested without a full State. 38func newDispatchHandler( 39 cfg *config.Config, 40 execer db.Execer, 41 logger *slog.Logger, 42 userRouter, standardRouter http.Handler, 43) http.HandlerFunc { 44 return func(w http.ResponseWriter, r *http.Request) { 45 pat := chi.URLParam(r, "*") 46 pathParts := strings.SplitN(pat, "/", 2) 47 48 if len(pathParts) > 0 { 49 firstPart := pathParts[0] 50 51 if userutil.IsDid(firstPart) { 52 repo, err := db.GetRepoByDid(execer, firstPart) 53 switch { 54 case err == nil: 55 remaining := "" 56 if len(pathParts) > 1 { 57 remaining = "/" + pathParts[1] 58 } 59 rewritten := "/" + repo.Did + "/" + repo.Rkey + remaining 60 r2 := r.Clone(r.Context()) 61 r2.URL.Path = rewritten 62 r2.URL.RawPath = rewritten 63 userRouter.ServeHTTP(w, r2) 64 case errors.Is(err, sql.ErrNoRows): 65 userRouter.ServeHTTP(w, r) 66 default: 67 logger.Error("db error looking up repo DID", "repoDid", firstPart, "err", err) 68 http.Error(w, "internal server error", http.StatusInternalServerError) 69 } 70 return 71 } 72 73 if userutil.IsHandle(firstPart) { 74 userRouter.ServeHTTP(w, r) 75 return 76 } 77 78 // if using a flattened DID (like you would in go modules), unflatten 79 if userutil.IsFlattenedDid(firstPart) { 80 unflattenedDid := userutil.UnflattenDid(firstPart) 81 redirectPath := strings.Join(append([]string{unflattenedDid}, pathParts[1:]...), "/") 82 83 redirectURL := *r.URL 84 redirectURL.Path = "/" + redirectPath 85 86 http.Redirect(w, r, redirectURL.String(), http.StatusFound) 87 return 88 } 89 90 // if using a handle with @, rewrite to work without @ 91 if normalized := strings.TrimPrefix(firstPart, "@"); userutil.IsHandle(normalized) { 92 redirectPath := strings.Join(append([]string{normalized}, pathParts[1:]...), "/") 93 94 redirectURL := *r.URL 95 redirectURL.Path = "/" + redirectPath 96 97 http.Redirect(w, r, redirectURL.String(), http.StatusFound) 98 return 99 } 100 101 // project mode: rewrite /{repo}/... → /{projectUser}/{repo}/... 102 // unless the first segment is a reserved standard-route prefix. 103 if cfg.Project.Enabled && cfg.Project.User != "" { 104 if firstPart == "" { 105 r2 := r.Clone(r.Context()) 106 r2.URL.Path = "/" + cfg.Project.User 107 r2.URL.RawPath = "/" + cfg.Project.User 108 userRouter.ServeHTTP(w, r2) 109 return 110 } 111 if _, isStd := standardPrefixes[firstPart]; !isStd { 112 rewritten := "/" + cfg.Project.User + "/" + pat 113 r2 := r.Clone(r.Context()) 114 r2.URL.Path = rewritten 115 r2.URL.RawPath = rewritten 116 userRouter.ServeHTTP(w, r2) 117 return 118 } 119 } 120 } 121 122 standardRouter.ServeHTTP(w, r) 123 } 124} 125 126// standardPrefixes is the set of first path segments that belong to the 127// standard (non-user) router. In project mode, any segment not in this set 128// is treated as a repo name and rewritten to /{ProjectUser}/{segment}. 129var standardPrefixes = map[string]struct{}{ 130 "static": {}, "home": {}, "timeline": {}, "upgradeBanner": {}, 131 "newsletter": {}, "core": {}, "login": {}, "logout": {}, 132 "search": {}, "account": {}, "repo": {}, "goodfirstissues": {}, 133 "follow": {}, "vouch": {}, "star": {}, "react": {}, 134 "profile": {}, "settings": {}, "strings": {}, "notifications": {}, 135 "signup": {}, "keys": {}, "terms": {}, "privacy": {}, "brand": {}, 136 "oauth": {}, 137} 138 139func (s *State) Router() http.Handler { 140 router := chi.NewRouter() 141 middleware := middleware.New( 142 s.oauth, 143 s.db, 144 s.enforcer, 145 s.aclService, 146 s.repoResolver, 147 s.idResolver, 148 s.pages, 149 s.rdb, 150 s.logger, 151 ) 152 153 router.Use(metrics.Middleware) 154 router.Use(knotacl.MemoMiddleware) 155 156 if err := db.ReapStaleRunningMigrations(context.Background(), s.db); err != nil { 157 s.logger.Warn("failed to reap stale running migrations", "err", err) 158 } 159 m := migration.NewMigration(s.db, s.oauth, s.idResolver.Directory(), s.logger) 160 router.Use(m.BackgroundMigrationMiddleware) 161 162 router.Get("/pwa-manifest.json", s.WebAppManifest) 163 router.Get("/robots.txt", s.RobotsTxt) 164 router.Get("/.well-known/security.txt", s.SecurityTxt) 165 166 userRouter := s.UserRouter(&middleware) 167 standardRouter := s.StandardRouter(&middleware) 168 169 router.HandleFunc("/*", newDispatchHandler(s.config, s.db, s.logger, userRouter, standardRouter)) 170 171 return router 172} 173 174func (s *State) UserRouter(mw *middleware.Middleware) http.Handler { 175 r := chi.NewRouter() 176 r.Use(mw.InjectBaseParams) 177 178 r.With(mw.ResolveIdent()).Route("/{user}", func(r chi.Router) { 179 r.Get("/", s.Profile) 180 r.Get("/feed.atom", s.AtomFeedPage) 181 182 r.With(mw.ResolveRepo()).Route("/{repo}", func(r chi.Router) { 183 r.Use(mw.GoImport()) 184 185 // These routes get proxied to the knot 186 r.Get("/info/refs", s.InfoRefs) 187 r.Post("/git-upload-archive", s.UploadArchive) 188 r.Post("/git-upload-pack", s.UploadPack) 189 r.Post("/git-receive-pack", s.ReceivePack) 190 191 r.Group(func(r chi.Router) { 192 r.Use(mw.CanonicalizeRepoURL()) 193 r.Mount("/issues", s.IssuesRouter(mw)) 194 r.Mount("/pulls", s.PullsRouter(mw)) 195 r.Mount("/pipelines", s.PipelinesRouter(mw)) 196 r.Mount("/labels", s.LabelsRouter()) 197 r.Mount("/", s.RepoRouter(mw)) 198 }) 199 }) 200 }) 201 202 r.NotFound(func(w http.ResponseWriter, r *http.Request) { 203 w.WriteHeader(http.StatusNotFound) 204 s.pages.Error404(w) 205 }) 206 207 return r 208} 209 210func (s *State) StandardRouter(mw *middleware.Middleware) http.Handler { 211 r := chi.NewRouter() 212 r.Use(mw.InjectBaseParams) 213 214 r.Handle("/static/*", s.pages.Static()) 215 216 tl := avtimeline.New(s.oauth, s.db, s.config, s.pages, s.logger, blog.PostsFS) 217 r.Get("/", tl.HomeOrTimeline) 218 if s.config.Project.Enabled { 219 r.Get("/home", http.RedirectHandler("/", http.StatusFound).ServeHTTP) 220 r.Get("/timeline", http.RedirectHandler("/", http.StatusFound).ServeHTTP) 221 } else { 222 r.Get("/home", tl.Home) 223 r.Get("/timeline", tl.Timeline) 224 } 225 r.Get("/upgradeBanner", s.UpgradeBanner) 226 r.Post("/newsletter/signup", s.NewsletterSignup) 227 r.Post("/newsletter/dismiss", s.NewsletterDismiss) 228 229 // special-case handler for serving tangled.org/core 230 r.Get("/core", s.Core()) 231 232 r.Get("/login", s.Login) 233 r.Post("/login", s.Login) 234 r.Post("/logout", s.Logout) 235 236 r.With(middleware.Paginate).Get("/search", s.Search) 237 r.With(middleware.AuthMiddleware(s.oauth)).Get("/search/quick", s.SearchQuick) 238 r.With(middleware.AuthMiddleware(s.oauth)).Get("/search/quick/mobile", s.SearchQuickMobile) 239 240 r.Post("/account/switch", s.SwitchAccount) 241 r.With(middleware.AuthMiddleware(s.oauth)).Delete("/account/{did}", s.RemoveAccount) 242 243 r.Route("/repo", func(r chi.Router) { 244 r.Route("/new", func(r chi.Router) { 245 r.Use(middleware.AuthMiddleware(s.oauth)) 246 r.Get("/", s.NewRepo) 247 r.Post("/", s.NewRepo) 248 }) 249 // r.Post("/import", s.ImportRepo) 250 }) 251 252 r.With(middleware.Paginate).Get("/goodfirstissues", s.GoodFirstIssues) 253 254 r.With(middleware.AuthMiddleware(s.oauth)).Route("/follow", func(r chi.Router) { 255 r.Post("/", s.Follow) 256 r.Delete("/", s.Follow) 257 }) 258 259 r.With(middleware.AuthMiddleware(s.oauth)).Route("/vouch", func(r chi.Router) { 260 r.Post("/", s.Vouch) 261 r.Post("/skip", s.SkipVouchSuggestion) 262 }) 263 264 r.With(middleware.AuthMiddleware(s.oauth)).Route("/star", func(r chi.Router) { 265 r.Post("/", s.Star) 266 r.Delete("/", s.Star) 267 }) 268 269 r.With(middleware.AuthMiddleware(s.oauth)).Route("/react", func(r chi.Router) { 270 r.Post("/", s.React) 271 r.Delete("/", s.React) 272 }) 273 274 r.With(middleware.AuthMiddleware(s.oauth)).Route("/comment", func(r chi.Router) { 275 r.Get("/", s.CommentBodyFragment) 276 r.Get("/edit", s.EditCommentFragment) 277 r.Get("/reply", s.NewReplyCommentFragment) 278 r.Get("/reply/placeholder", s.ReplyPlaceholderFragment) 279 r.Post("/", s.NewComment) 280 r.Patch("/", s.EditComment) 281 r.Delete("/", s.DeleteComment) 282 }) 283 284 r.Get("/profile/popover", s.ProfilePopover) 285 286 r.Route("/profile", func(r chi.Router) { 287 r.Use(middleware.AuthMiddleware(s.oauth)) 288 r.Get("/edit-bio", s.EditBioFragment) 289 r.Get("/edit-pins", s.EditPinsFragment) 290 r.Post("/bio", s.UpdateProfileBio) 291 r.Post("/pins", s.UpdateProfilePins) 292 r.Post("/avatar", s.UploadProfileAvatar) 293 r.Delete("/avatar", s.RemoveProfileAvatar) 294 r.Post("/punchcard", s.UpdateProfilePunchcardSetting) 295 }) 296 297 r.Mount("/settings", s.SettingsRouter()) 298 r.Mount("/strings", s.StringsRouter(mw)) 299 300 r.Mount("/settings/knots", s.KnotsRouter()) 301 r.Mount("/settings/spindles", s.SpindlesRouter()) 302 303 r.Mount("/notifications", s.NotificationsRouter(mw)) 304 r.Mount("/focus", s.FocusRouter(mw)) 305 306 if s.config.Project.Enabled { 307 r.Mount("/signup", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 308 http.Redirect(w, r, "/", http.StatusFound) 309 })) 310 } else { 311 r.Mount("/signup", s.SignupRouter()) 312 } 313 r.Mount("/", s.oauth.Router()) 314 315 r.Get("/keys/{user}", s.Keys) 316 r.Get("/terms", s.TermsOfService) 317 r.Get("/privacy", s.PrivacyPolicy) 318 r.Get("/brand", s.Brand) 319 320 r.NotFound(func(w http.ResponseWriter, r *http.Request) { 321 w.WriteHeader(http.StatusNotFound) 322 s.pages.Error404(w) 323 }) 324 return r 325} 326 327// Core serves tangled.org/core go-import meta tags, and redirects 328// to the core repository if accessed normally. 329func (s *State) Core() http.HandlerFunc { 330 return func(w http.ResponseWriter, r *http.Request) { 331 if r.URL.Query().Get("go-get") == "1" { 332 w.Header().Set("Content-Type", "text/html") 333 w.Write([]byte(`<meta name="go-import" content="tangled.org/core git https://tangled.org/@tangled.org/core">`)) 334 return 335 } 336 337 http.Redirect(w, r, "/@tangled.org/core", http.StatusFound) 338 } 339} 340 341func (s *State) SettingsRouter() http.Handler { 342 settings := &settings.Settings{ 343 Db: s.db, 344 OAuth: s.oauth, 345 Pages: s.pages, 346 Config: s.config, 347 CfClient: s.cfClient, 348 Logger: log.SubLogger(s.logger, "settings"), 349 IdResolver: s.idResolver, 350 } 351 352 return settings.Router() 353} 354 355func (s *State) SpindlesRouter() http.Handler { 356 logger := log.SubLogger(s.logger, "spindles") 357 358 spindles := &spindles.Spindles{ 359 Db: s.db, 360 OAuth: s.oauth, 361 Pages: s.pages, 362 Config: s.config, 363 Enforcer: s.enforcer, 364 IdResolver: s.idResolver, 365 Logger: logger, 366 } 367 368 return spindles.Router() 369} 370 371func (s *State) KnotsRouter() http.Handler { 372 logger := log.SubLogger(s.logger, "knots") 373 374 knots := &knots.Knots{ 375 Db: s.db, 376 OAuth: s.oauth, 377 Pages: s.pages, 378 Config: s.config, 379 Enforcer: s.enforcer, 380 Acl: s.aclService, 381 IdResolver: s.idResolver, 382 Knotstream: s.knotstream, 383 Logger: logger, 384 } 385 386 return knots.Router() 387} 388 389func (s *State) StringsRouter(mw *middleware.Middleware) http.Handler { 390 logger := log.SubLogger(s.logger, "strings") 391 392 strs := &avstrings.Strings{ 393 Db: s.db, 394 OAuth: s.oauth, 395 Pages: s.pages, 396 IdResolver: s.idResolver, 397 Notifier: s.notifier, 398 Logger: logger, 399 } 400 401 return strs.Router(mw) 402} 403 404func (s *State) IssuesRouter(mw *middleware.Middleware) http.Handler { 405 issues := issues.New( 406 s.oauth, 407 s.repoResolver, 408 s.aclService, 409 s.pages, 410 s.idResolver, 411 s.mentionsResolver, 412 s.db, 413 s.config, 414 s.notifier, 415 s.validator, 416 s.indexer.Issues, 417 log.SubLogger(s.logger, "issues"), 418 ) 419 return issues.Router(mw) 420} 421 422func (s *State) PullsRouter(mw *middleware.Middleware) http.Handler { 423 pulls := pulls.New( 424 s.oauth, 425 s.repoResolver, 426 s.pages, 427 s.idResolver, 428 s.mentionsResolver, 429 s.db, 430 s.config, 431 s.notifier, 432 s.aclService, 433 s.validator, 434 s.indexer.Pulls, 435 log.SubLogger(s.logger, "pulls"), 436 ) 437 return pulls.Router(mw) 438} 439 440func (s *State) RepoRouter(mw *middleware.Middleware) http.Handler { 441 repo := repo.New( 442 s.oauth, 443 s.repoResolver, 444 s.pages, 445 s.spindlestream, 446 s.idResolver, 447 s.db, 448 s.config, 449 s.notifier, 450 s.enforcer, 451 s.aclService, 452 log.SubLogger(s.logger, "repo"), 453 s.validator, 454 s.cfClient, 455 ) 456 return repo.Router(mw) 457} 458 459func (s *State) PipelinesRouter(mw *middleware.Middleware) http.Handler { 460 pipes := pipelines.New( 461 s.oauth, 462 s.repoResolver, 463 s.pages, 464 s.spindlestream, 465 s.pipelineNotifier, 466 s.idResolver, 467 s.db, 468 s.config, 469 s.enforcer, 470 log.SubLogger(s.logger, "pipelines"), 471 ) 472 return pipes.Router(mw) 473} 474 475func (s *State) LabelsRouter() http.Handler { 476 ls := labels.New( 477 s.oauth, 478 s.pages, 479 s.db, 480 s.validator, 481 s.enforcer, 482 s.notifier, 483 log.SubLogger(s.logger, "labels"), 484 ) 485 return ls.Router() 486} 487 488func (s *State) NotificationsRouter(mw *middleware.Middleware) http.Handler { 489 notifs := notifications.New(s.db, s.oauth, s.pages, log.SubLogger(s.logger, "notifications")) 490 return notifs.Router(mw) 491} 492 493func (s *State) FocusRouter(mw *middleware.Middleware) http.Handler { 494 f := focus.New(s.db, s.oauth, s.idResolver, s.pages, log.SubLogger(s.logger, "focus")) 495 return f.Router(mw) 496} 497 498func (s *State) SignupRouter() http.Handler { 499 sig := signup.New(s.config, s.db, s.posthog, s.idResolver, s.pages, log.SubLogger(s.logger, "signup")) 500 return sig.Router() 501}