this repo has no description
1package pages 2 3import ( 4 "crypto/sha256" 5 "embed" 6 "encoding/hex" 7 "fmt" 8 "html/template" 9 "io" 10 "io/fs" 11 "log/slog" 12 "net/http" 13 "os" 14 "path/filepath" 15 "strings" 16 "sync" 17 "time" 18 19 "tangled.org/core/api/tangled" 20 "tangled.org/core/appview/commitverify" 21 "tangled.org/core/appview/config" 22 "tangled.org/core/appview/models" 23 "tangled.org/core/appview/oauth" 24 "tangled.org/core/appview/pages/markup" 25 "tangled.org/core/appview/pages/repoinfo" 26 "tangled.org/core/appview/pagination" 27 "tangled.org/core/idresolver" 28 "tangled.org/core/patchutil" 29 "tangled.org/core/types" 30 31 "github.com/bluesky-social/indigo/atproto/identity" 32 "github.com/bluesky-social/indigo/atproto/syntax" 33 "github.com/go-git/go-git/v5/plumbing" 34 "github.com/go-git/go-git/v5/plumbing/object" 35) 36 37//go:embed templates/* static legal 38var Files embed.FS 39 40type Pages struct { 41 mu sync.RWMutex 42 cache *TmplCache[string, *template.Template] 43 44 avatar config.AvatarConfig 45 resolver *idresolver.Resolver 46 dev bool 47 embedFS fs.FS 48 templateDir string // Path to templates on disk for dev mode 49 rctx *markup.RenderContext 50 logger *slog.Logger 51} 52 53func NewPages(config *config.Config, res *idresolver.Resolver, logger *slog.Logger) *Pages { 54 // initialized with safe defaults, can be overriden per use 55 rctx := &markup.RenderContext{ 56 IsDev: config.Core.Dev, 57 CamoUrl: config.Camo.Host, 58 CamoSecret: config.Camo.SharedSecret, 59 Sanitizer: markup.NewSanitizer(), 60 Files: Files, 61 } 62 63 p := &Pages{ 64 mu: sync.RWMutex{}, 65 cache: NewTmplCache[string, *template.Template](), 66 dev: config.Core.Dev, 67 avatar: config.Avatar, 68 rctx: rctx, 69 resolver: res, 70 templateDir: "appview/pages", 71 logger: logger, 72 } 73 74 if p.dev { 75 p.embedFS = os.DirFS(p.templateDir) 76 } else { 77 p.embedFS = Files 78 } 79 80 return p 81} 82 83// reverse of pathToName 84func (p *Pages) nameToPath(s string) string { 85 return "templates/" + s + ".html" 86} 87 88func (p *Pages) fragmentPaths() ([]string, error) { 89 var fragmentPaths []string 90 err := fs.WalkDir(p.embedFS, "templates", func(path string, d fs.DirEntry, err error) error { 91 if err != nil { 92 return err 93 } 94 if d.IsDir() { 95 return nil 96 } 97 if !strings.HasSuffix(path, ".html") { 98 return nil 99 } 100 if !strings.Contains(path, "fragments/") { 101 return nil 102 } 103 fragmentPaths = append(fragmentPaths, path) 104 return nil 105 }) 106 if err != nil { 107 return nil, err 108 } 109 110 return fragmentPaths, nil 111} 112 113// parse without memoization 114func (p *Pages) rawParse(stack ...string) (*template.Template, error) { 115 paths, err := p.fragmentPaths() 116 if err != nil { 117 return nil, err 118 } 119 for _, s := range stack { 120 paths = append(paths, p.nameToPath(s)) 121 } 122 123 funcs := p.funcMap() 124 top := stack[len(stack)-1] 125 parsed, err := template.New(top). 126 Funcs(funcs). 127 ParseFS(p.embedFS, paths...) 128 if err != nil { 129 return nil, err 130 } 131 132 return parsed, nil 133} 134 135func (p *Pages) parse(stack ...string) (*template.Template, error) { 136 key := strings.Join(stack, "|") 137 138 // never cache in dev mode 139 if cached, exists := p.cache.Get(key); !p.dev && exists { 140 return cached, nil 141 } 142 143 result, err := p.rawParse(stack...) 144 if err != nil { 145 return nil, err 146 } 147 148 p.cache.Set(key, result) 149 return result, nil 150} 151 152func (p *Pages) parseBase(top string) (*template.Template, error) { 153 stack := []string{ 154 "layouts/base", 155 top, 156 } 157 return p.parse(stack...) 158} 159 160func (p *Pages) parseRepoBase(top string) (*template.Template, error) { 161 stack := []string{ 162 "layouts/base", 163 "layouts/repobase", 164 top, 165 } 166 return p.parse(stack...) 167} 168 169func (p *Pages) parseProfileBase(top string) (*template.Template, error) { 170 stack := []string{ 171 "layouts/base", 172 "layouts/profilebase", 173 top, 174 } 175 return p.parse(stack...) 176} 177 178func (p *Pages) executePlain(name string, w io.Writer, params any) error { 179 tpl, err := p.parse(name) 180 if err != nil { 181 return err 182 } 183 184 return tpl.Execute(w, params) 185} 186 187func (p *Pages) execute(name string, w io.Writer, params any) error { 188 tpl, err := p.parseBase(name) 189 if err != nil { 190 return err 191 } 192 193 return tpl.ExecuteTemplate(w, "layouts/base", params) 194} 195 196func (p *Pages) executeRepo(name string, w io.Writer, params any) error { 197 tpl, err := p.parseRepoBase(name) 198 if err != nil { 199 return err 200 } 201 202 return tpl.ExecuteTemplate(w, "layouts/base", params) 203} 204 205func (p *Pages) executeProfile(name string, w io.Writer, params any) error { 206 tpl, err := p.parseProfileBase(name) 207 if err != nil { 208 return err 209 } 210 211 return tpl.ExecuteTemplate(w, "layouts/base", params) 212} 213 214func (p *Pages) Favicon(w io.Writer) error { 215 return p.executePlain("fragments/dolly/silhouette", w, nil) 216} 217 218type LoginParams struct { 219 ReturnUrl string 220 ErrorCode string 221} 222 223func (p *Pages) Login(w io.Writer, params LoginParams) error { 224 return p.executePlain("user/login", w, params) 225} 226 227type SignupParams struct { 228 CloudflareSiteKey string 229} 230 231func (p *Pages) Signup(w io.Writer, params SignupParams) error { 232 return p.executePlain("user/signup", w, params) 233} 234 235func (p *Pages) CompleteSignup(w io.Writer) error { 236 return p.executePlain("user/completeSignup", w, nil) 237} 238 239type TermsOfServiceParams struct { 240 LoggedInUser *oauth.User 241 Content template.HTML 242} 243 244func (p *Pages) TermsOfService(w io.Writer, params TermsOfServiceParams) error { 245 filename := "terms.md" 246 filePath := filepath.Join("legal", filename) 247 248 file, err := p.embedFS.Open(filePath) 249 if err != nil { 250 return fmt.Errorf("failed to read %s: %w", filename, err) 251 } 252 defer file.Close() 253 254 markdownBytes, err := io.ReadAll(file) 255 if err != nil { 256 return fmt.Errorf("failed to read %s: %w", filename, err) 257 } 258 259 p.rctx.RendererType = markup.RendererTypeDefault 260 htmlString := p.rctx.RenderMarkdown(string(markdownBytes)) 261 sanitized := p.rctx.SanitizeDefault(htmlString) 262 params.Content = template.HTML(sanitized) 263 264 return p.execute("legal/terms", w, params) 265} 266 267type PrivacyPolicyParams struct { 268 LoggedInUser *oauth.User 269 Content template.HTML 270} 271 272func (p *Pages) PrivacyPolicy(w io.Writer, params PrivacyPolicyParams) error { 273 filename := "privacy.md" 274 filePath := filepath.Join("legal", filename) 275 276 file, err := p.embedFS.Open(filePath) 277 if err != nil { 278 return fmt.Errorf("failed to read %s: %w", filename, err) 279 } 280 defer file.Close() 281 282 markdownBytes, err := io.ReadAll(file) 283 if err != nil { 284 return fmt.Errorf("failed to read %s: %w", filename, err) 285 } 286 287 p.rctx.RendererType = markup.RendererTypeDefault 288 htmlString := p.rctx.RenderMarkdown(string(markdownBytes)) 289 sanitized := p.rctx.SanitizeDefault(htmlString) 290 params.Content = template.HTML(sanitized) 291 292 return p.execute("legal/privacy", w, params) 293} 294 295type BrandParams struct { 296 LoggedInUser *oauth.User 297} 298 299func (p *Pages) Brand(w io.Writer, params BrandParams) error { 300 return p.execute("brand/brand", w, params) 301} 302 303type TimelineParams struct { 304 LoggedInUser *oauth.User 305 Timeline []models.TimelineEvent 306 Repos []models.Repo 307 GfiLabel *models.LabelDefinition 308} 309 310func (p *Pages) Timeline(w io.Writer, params TimelineParams) error { 311 return p.execute("timeline/timeline", w, params) 312} 313 314type GoodFirstIssuesParams struct { 315 LoggedInUser *oauth.User 316 Issues []models.Issue 317 RepoGroups []*models.RepoGroup 318 LabelDefs map[string]*models.LabelDefinition 319 GfiLabel *models.LabelDefinition 320 Page pagination.Page 321} 322 323func (p *Pages) GoodFirstIssues(w io.Writer, params GoodFirstIssuesParams) error { 324 return p.execute("goodfirstissues/index", w, params) 325} 326 327type UserProfileSettingsParams struct { 328 LoggedInUser *oauth.User 329 Tabs []map[string]any 330 Tab string 331} 332 333func (p *Pages) UserProfileSettings(w io.Writer, params UserProfileSettingsParams) error { 334 return p.execute("user/settings/profile", w, params) 335} 336 337type NotificationsParams struct { 338 LoggedInUser *oauth.User 339 Notifications []*models.NotificationWithEntity 340 UnreadCount int 341 Page pagination.Page 342 Total int64 343} 344 345func (p *Pages) Notifications(w io.Writer, params NotificationsParams) error { 346 return p.execute("notifications/list", w, params) 347} 348 349type NotificationItemParams struct { 350 Notification *models.Notification 351} 352 353func (p *Pages) NotificationItem(w io.Writer, params NotificationItemParams) error { 354 return p.executePlain("notifications/fragments/item", w, params) 355} 356 357type NotificationCountParams struct { 358 Count int64 359} 360 361func (p *Pages) NotificationCount(w io.Writer, params NotificationCountParams) error { 362 return p.executePlain("notifications/fragments/count", w, params) 363} 364 365type UserKeysSettingsParams struct { 366 LoggedInUser *oauth.User 367 PubKeys []models.PublicKey 368 Tabs []map[string]any 369 Tab string 370} 371 372func (p *Pages) UserKeysSettings(w io.Writer, params UserKeysSettingsParams) error { 373 return p.execute("user/settings/keys", w, params) 374} 375 376type UserEmailsSettingsParams struct { 377 LoggedInUser *oauth.User 378 Emails []models.Email 379 Tabs []map[string]any 380 Tab string 381} 382 383func (p *Pages) UserEmailsSettings(w io.Writer, params UserEmailsSettingsParams) error { 384 return p.execute("user/settings/emails", w, params) 385} 386 387type UserNotificationSettingsParams struct { 388 LoggedInUser *oauth.User 389 Preferences *models.NotificationPreferences 390 Tabs []map[string]any 391 Tab string 392} 393 394func (p *Pages) UserNotificationSettings(w io.Writer, params UserNotificationSettingsParams) error { 395 return p.execute("user/settings/notifications", w, params) 396} 397 398type UpgradeBannerParams struct { 399 Registrations []models.Registration 400 Spindles []models.Spindle 401} 402 403func (p *Pages) UpgradeBanner(w io.Writer, params UpgradeBannerParams) error { 404 return p.executePlain("banner", w, params) 405} 406 407type KnotsParams struct { 408 LoggedInUser *oauth.User 409 Registrations []models.Registration 410 Tabs []map[string]any 411 Tab string 412} 413 414func (p *Pages) Knots(w io.Writer, params KnotsParams) error { 415 return p.execute("knots/index", w, params) 416} 417 418type KnotParams struct { 419 LoggedInUser *oauth.User 420 Registration *models.Registration 421 Members []string 422 Repos map[string][]models.Repo 423 IsOwner bool 424 Tabs []map[string]any 425 Tab string 426} 427 428func (p *Pages) Knot(w io.Writer, params KnotParams) error { 429 return p.execute("knots/dashboard", w, params) 430} 431 432type KnotListingParams struct { 433 *models.Registration 434} 435 436func (p *Pages) KnotListing(w io.Writer, params KnotListingParams) error { 437 return p.executePlain("knots/fragments/knotListing", w, params) 438} 439 440type SpindlesParams struct { 441 LoggedInUser *oauth.User 442 Spindles []models.Spindle 443 Tabs []map[string]any 444 Tab string 445} 446 447func (p *Pages) Spindles(w io.Writer, params SpindlesParams) error { 448 return p.execute("spindles/index", w, params) 449} 450 451type SpindleListingParams struct { 452 models.Spindle 453 Tabs []map[string]any 454 Tab string 455} 456 457func (p *Pages) SpindleListing(w io.Writer, params SpindleListingParams) error { 458 return p.executePlain("spindles/fragments/spindleListing", w, params) 459} 460 461type SpindleDashboardParams struct { 462 LoggedInUser *oauth.User 463 Spindle models.Spindle 464 Members []string 465 Repos map[string][]models.Repo 466 Tabs []map[string]any 467 Tab string 468} 469 470func (p *Pages) SpindleDashboard(w io.Writer, params SpindleDashboardParams) error { 471 return p.execute("spindles/dashboard", w, params) 472} 473 474type NewRepoParams struct { 475 LoggedInUser *oauth.User 476 Knots []string 477} 478 479func (p *Pages) NewRepo(w io.Writer, params NewRepoParams) error { 480 return p.execute("repo/new", w, params) 481} 482 483type ForkRepoParams struct { 484 LoggedInUser *oauth.User 485 Knots []string 486 RepoInfo repoinfo.RepoInfo 487} 488 489func (p *Pages) ForkRepo(w io.Writer, params ForkRepoParams) error { 490 return p.execute("repo/fork", w, params) 491} 492 493type ProfileCard struct { 494 UserDid string 495 FollowStatus models.FollowStatus 496 Punchcard *models.Punchcard 497 Profile *models.Profile 498 Stats ProfileStats 499 Active string 500} 501 502type ProfileStats struct { 503 RepoCount int64 504 StarredCount int64 505 StringCount int64 506 FollowersCount int64 507 FollowingCount int64 508} 509 510func (p *ProfileCard) GetTabs() [][]any { 511 tabs := [][]any{ 512 {"overview", "overview", "square-chart-gantt", nil}, 513 {"repos", "repos", "book-marked", p.Stats.RepoCount}, 514 {"starred", "starred", "star", p.Stats.StarredCount}, 515 {"strings", "strings", "line-squiggle", p.Stats.StringCount}, 516 } 517 518 return tabs 519} 520 521type ProfileOverviewParams struct { 522 LoggedInUser *oauth.User 523 Repos []models.Repo 524 CollaboratingRepos []models.Repo 525 ProfileTimeline *models.ProfileTimeline 526 Card *ProfileCard 527 Active string 528} 529 530func (p *Pages) ProfileOverview(w io.Writer, params ProfileOverviewParams) error { 531 params.Active = "overview" 532 return p.executeProfile("user/overview", w, params) 533} 534 535type ProfileReposParams struct { 536 LoggedInUser *oauth.User 537 Repos []models.Repo 538 Card *ProfileCard 539 Active string 540} 541 542func (p *Pages) ProfileRepos(w io.Writer, params ProfileReposParams) error { 543 params.Active = "repos" 544 return p.executeProfile("user/repos", w, params) 545} 546 547type ProfileStarredParams struct { 548 LoggedInUser *oauth.User 549 Repos []models.Repo 550 Card *ProfileCard 551 Active string 552} 553 554func (p *Pages) ProfileStarred(w io.Writer, params ProfileStarredParams) error { 555 params.Active = "starred" 556 return p.executeProfile("user/starred", w, params) 557} 558 559type ProfileStringsParams struct { 560 LoggedInUser *oauth.User 561 Strings []models.String 562 Card *ProfileCard 563 Active string 564} 565 566func (p *Pages) ProfileStrings(w io.Writer, params ProfileStringsParams) error { 567 params.Active = "strings" 568 return p.executeProfile("user/strings", w, params) 569} 570 571type FollowCard struct { 572 UserDid string 573 LoggedInUser *oauth.User 574 FollowStatus models.FollowStatus 575 FollowersCount int64 576 FollowingCount int64 577 Profile *models.Profile 578} 579 580type ProfileFollowersParams struct { 581 LoggedInUser *oauth.User 582 Followers []FollowCard 583 Card *ProfileCard 584 Active string 585} 586 587func (p *Pages) ProfileFollowers(w io.Writer, params ProfileFollowersParams) error { 588 params.Active = "overview" 589 return p.executeProfile("user/followers", w, params) 590} 591 592type ProfileFollowingParams struct { 593 LoggedInUser *oauth.User 594 Following []FollowCard 595 Card *ProfileCard 596 Active string 597} 598 599func (p *Pages) ProfileFollowing(w io.Writer, params ProfileFollowingParams) error { 600 params.Active = "overview" 601 return p.executeProfile("user/following", w, params) 602} 603 604type FollowFragmentParams struct { 605 UserDid string 606 FollowStatus models.FollowStatus 607} 608 609func (p *Pages) FollowFragment(w io.Writer, params FollowFragmentParams) error { 610 return p.executePlain("user/fragments/follow", w, params) 611} 612 613type EditBioParams struct { 614 LoggedInUser *oauth.User 615 Profile *models.Profile 616} 617 618func (p *Pages) EditBioFragment(w io.Writer, params EditBioParams) error { 619 return p.executePlain("user/fragments/editBio", w, params) 620} 621 622type EditPinsParams struct { 623 LoggedInUser *oauth.User 624 Profile *models.Profile 625 AllRepos []PinnedRepo 626} 627 628type PinnedRepo struct { 629 IsPinned bool 630 models.Repo 631} 632 633func (p *Pages) EditPinsFragment(w io.Writer, params EditPinsParams) error { 634 return p.executePlain("user/fragments/editPins", w, params) 635} 636 637type StarBtnFragmentParams struct { 638 IsStarred bool 639 SubjectAt syntax.ATURI 640 StarCount int 641} 642 643func (p *Pages) StarBtnFragment(w io.Writer, params StarBtnFragmentParams) error { 644 return p.executePlain("fragments/starBtn", w, params) 645} 646 647type RepoIndexParams struct { 648 LoggedInUser *oauth.User 649 RepoInfo repoinfo.RepoInfo 650 Active string 651 TagMap map[string][]string 652 CommitsTrunc []*object.Commit 653 TagsTrunc []*types.TagReference 654 BranchesTrunc []types.Branch 655 // ForkInfo *types.ForkInfo 656 HTMLReadme template.HTML 657 Raw bool 658 EmailToDid map[string]string 659 VerifiedCommits commitverify.VerifiedCommits 660 Languages []types.RepoLanguageDetails 661 Pipelines map[string]models.Pipeline 662 NeedsKnotUpgrade bool 663 types.RepoIndexResponse 664} 665 666func (p *Pages) RepoIndexPage(w io.Writer, params RepoIndexParams) error { 667 params.Active = "overview" 668 if params.IsEmpty { 669 return p.executeRepo("repo/empty", w, params) 670 } 671 672 if params.NeedsKnotUpgrade { 673 return p.executeRepo("repo/needsUpgrade", w, params) 674 } 675 676 p.rctx.RepoInfo = params.RepoInfo 677 p.rctx.RepoInfo.Ref = params.Ref 678 p.rctx.RendererType = markup.RendererTypeRepoMarkdown 679 680 if params.ReadmeFileName != "" { 681 ext := filepath.Ext(params.ReadmeFileName) 682 switch ext { 683 case ".md", ".markdown", ".mdown", ".mkdn", ".mkd": 684 params.Raw = false 685 htmlString := p.rctx.RenderMarkdown(params.Readme) 686 sanitized := p.rctx.SanitizeDefault(htmlString) 687 params.HTMLReadme = template.HTML(sanitized) 688 default: 689 params.Raw = true 690 } 691 } 692 693 return p.executeRepo("repo/index", w, params) 694} 695 696type RepoLogParams struct { 697 LoggedInUser *oauth.User 698 RepoInfo repoinfo.RepoInfo 699 TagMap map[string][]string 700 Active string 701 EmailToDid map[string]string 702 VerifiedCommits commitverify.VerifiedCommits 703 Pipelines map[string]models.Pipeline 704 705 types.RepoLogResponse 706} 707 708func (p *Pages) RepoLog(w io.Writer, params RepoLogParams) error { 709 params.Active = "overview" 710 return p.executeRepo("repo/log", w, params) 711} 712 713type RepoCommitParams struct { 714 LoggedInUser *oauth.User 715 RepoInfo repoinfo.RepoInfo 716 Active string 717 EmailToDid map[string]string 718 Pipeline *models.Pipeline 719 DiffOpts types.DiffOpts 720 721 // singular because it's always going to be just one 722 VerifiedCommit commitverify.VerifiedCommits 723 724 types.RepoCommitResponse 725} 726 727func (p *Pages) RepoCommit(w io.Writer, params RepoCommitParams) error { 728 params.Active = "overview" 729 return p.executeRepo("repo/commit", w, params) 730} 731 732type RepoTreeParams struct { 733 LoggedInUser *oauth.User 734 RepoInfo repoinfo.RepoInfo 735 Active string 736 BreadCrumbs [][]string 737 TreePath string 738 Raw bool 739 HTMLReadme template.HTML 740 types.RepoTreeResponse 741} 742 743type RepoTreeStats struct { 744 NumFolders uint64 745 NumFiles uint64 746} 747 748func (r RepoTreeParams) TreeStats() RepoTreeStats { 749 numFolders, numFiles := 0, 0 750 for _, f := range r.Files { 751 if !f.IsFile() { 752 numFolders += 1 753 } else if f.IsFile() { 754 numFiles += 1 755 } 756 } 757 758 return RepoTreeStats{ 759 NumFolders: uint64(numFolders), 760 NumFiles: uint64(numFiles), 761 } 762} 763 764func (p *Pages) RepoTree(w io.Writer, params RepoTreeParams) error { 765 params.Active = "overview" 766 767 p.rctx.RepoInfo = params.RepoInfo 768 p.rctx.RepoInfo.Ref = params.Ref 769 p.rctx.RendererType = markup.RendererTypeRepoMarkdown 770 771 if params.ReadmeFileName != "" { 772 ext := filepath.Ext(params.ReadmeFileName) 773 switch ext { 774 case ".md", ".markdown", ".mdown", ".mkdn", ".mkd": 775 params.Raw = false 776 htmlString := p.rctx.RenderMarkdown(params.Readme) 777 sanitized := p.rctx.SanitizeDefault(htmlString) 778 params.HTMLReadme = template.HTML(sanitized) 779 default: 780 params.Raw = true 781 } 782 } 783 784 return p.executeRepo("repo/tree", w, params) 785} 786 787type RepoBranchesParams struct { 788 LoggedInUser *oauth.User 789 RepoInfo repoinfo.RepoInfo 790 Active string 791 types.RepoBranchesResponse 792} 793 794func (p *Pages) RepoBranches(w io.Writer, params RepoBranchesParams) error { 795 params.Active = "overview" 796 return p.executeRepo("repo/branches", w, params) 797} 798 799type RepoTagsParams struct { 800 LoggedInUser *oauth.User 801 RepoInfo repoinfo.RepoInfo 802 Active string 803 types.RepoTagsResponse 804 ArtifactMap map[plumbing.Hash][]models.Artifact 805 DanglingArtifacts []models.Artifact 806} 807 808func (p *Pages) RepoTags(w io.Writer, params RepoTagsParams) error { 809 params.Active = "overview" 810 return p.executeRepo("repo/tags", w, params) 811} 812 813type RepoArtifactParams struct { 814 LoggedInUser *oauth.User 815 RepoInfo repoinfo.RepoInfo 816 Artifact models.Artifact 817} 818 819func (p *Pages) RepoArtifactFragment(w io.Writer, params RepoArtifactParams) error { 820 return p.executePlain("repo/fragments/artifact", w, params) 821} 822 823type RepoBlobParams struct { 824 LoggedInUser *oauth.User 825 RepoInfo repoinfo.RepoInfo 826 Active string 827 BreadCrumbs [][]string 828 BlobView models.BlobView 829 *tangled.RepoBlob_Output 830} 831 832func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error { 833 switch params.BlobView.ContentType { 834 case models.BlobContentTypeMarkup: 835 p.rctx.RepoInfo = params.RepoInfo 836 } 837 838 params.Active = "overview" 839 return p.executeRepo("repo/blob", w, params) 840} 841 842type Collaborator struct { 843 Did string 844 Role string 845} 846 847type RepoSettingsParams struct { 848 LoggedInUser *oauth.User 849 RepoInfo repoinfo.RepoInfo 850 Collaborators []Collaborator 851 Active string 852 Branches []types.Branch 853 Spindles []string 854 CurrentSpindle string 855 Secrets []*tangled.RepoListSecrets_Secret 856 857 // TODO: use repoinfo.roles 858 IsCollaboratorInviteAllowed bool 859} 860 861func (p *Pages) RepoSettings(w io.Writer, params RepoSettingsParams) error { 862 params.Active = "settings" 863 return p.executeRepo("repo/settings", w, params) 864} 865 866type RepoGeneralSettingsParams struct { 867 LoggedInUser *oauth.User 868 RepoInfo repoinfo.RepoInfo 869 Labels []models.LabelDefinition 870 DefaultLabels []models.LabelDefinition 871 SubscribedLabels map[string]struct{} 872 ShouldSubscribeAll bool 873 Active string 874 Tabs []map[string]any 875 Tab string 876 Branches []types.Branch 877} 878 879func (p *Pages) RepoGeneralSettings(w io.Writer, params RepoGeneralSettingsParams) error { 880 params.Active = "settings" 881 return p.executeRepo("repo/settings/general", w, params) 882} 883 884type RepoAccessSettingsParams struct { 885 LoggedInUser *oauth.User 886 RepoInfo repoinfo.RepoInfo 887 Active string 888 Tabs []map[string]any 889 Tab string 890 Collaborators []Collaborator 891} 892 893func (p *Pages) RepoAccessSettings(w io.Writer, params RepoAccessSettingsParams) error { 894 params.Active = "settings" 895 return p.executeRepo("repo/settings/access", w, params) 896} 897 898type RepoPipelineSettingsParams struct { 899 LoggedInUser *oauth.User 900 RepoInfo repoinfo.RepoInfo 901 Active string 902 Tabs []map[string]any 903 Tab string 904 Spindles []string 905 CurrentSpindle string 906 Secrets []map[string]any 907} 908 909func (p *Pages) RepoPipelineSettings(w io.Writer, params RepoPipelineSettingsParams) error { 910 params.Active = "settings" 911 return p.executeRepo("repo/settings/pipelines", w, params) 912} 913 914type RepoIssuesParams struct { 915 LoggedInUser *oauth.User 916 RepoInfo repoinfo.RepoInfo 917 Active string 918 Issues []models.Issue 919 IssueCount int 920 LabelDefs map[string]*models.LabelDefinition 921 Page pagination.Page 922 FilteringByOpen bool 923 FilterQuery string 924} 925 926func (p *Pages) RepoIssues(w io.Writer, params RepoIssuesParams) error { 927 params.Active = "issues" 928 return p.executeRepo("repo/issues/issues", w, params) 929} 930 931type RepoSingleIssueParams struct { 932 LoggedInUser *oauth.User 933 RepoInfo repoinfo.RepoInfo 934 Active string 935 Issue *models.Issue 936 CommentList []models.CommentListItem 937 LabelDefs map[string]*models.LabelDefinition 938 939 OrderedReactionKinds []models.ReactionKind 940 Reactions map[models.ReactionKind]models.ReactionDisplayData 941 UserReacted map[models.ReactionKind]bool 942} 943 944func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error { 945 params.Active = "issues" 946 return p.executeRepo("repo/issues/issue", w, params) 947} 948 949type EditIssueParams struct { 950 LoggedInUser *oauth.User 951 RepoInfo repoinfo.RepoInfo 952 Issue *models.Issue 953 Action string 954} 955 956func (p *Pages) EditIssueFragment(w io.Writer, params EditIssueParams) error { 957 params.Action = "edit" 958 return p.executePlain("repo/issues/fragments/putIssue", w, params) 959} 960 961type ThreadReactionFragmentParams struct { 962 ThreadAt syntax.ATURI 963 Kind models.ReactionKind 964 Count int 965 Users []string 966 IsReacted bool 967} 968 969func (p *Pages) ThreadReactionFragment(w io.Writer, params ThreadReactionFragmentParams) error { 970 return p.executePlain("repo/fragments/reaction", w, params) 971} 972 973type RepoNewIssueParams struct { 974 LoggedInUser *oauth.User 975 RepoInfo repoinfo.RepoInfo 976 Issue *models.Issue // existing issue if any -- passed when editing 977 Active string 978 Action string 979} 980 981func (p *Pages) RepoNewIssue(w io.Writer, params RepoNewIssueParams) error { 982 params.Active = "issues" 983 params.Action = "create" 984 return p.executeRepo("repo/issues/new", w, params) 985} 986 987type EditIssueCommentParams struct { 988 LoggedInUser *oauth.User 989 RepoInfo repoinfo.RepoInfo 990 Issue *models.Issue 991 Comment *models.IssueComment 992} 993 994func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error { 995 return p.executePlain("repo/issues/fragments/editIssueComment", w, params) 996} 997 998type ReplyIssueCommentPlaceholderParams struct { 999 LoggedInUser *oauth.User 1000 RepoInfo repoinfo.RepoInfo 1001 Issue *models.Issue 1002 Comment *models.IssueComment 1003} 1004 1005func (p *Pages) ReplyIssueCommentPlaceholderFragment(w io.Writer, params ReplyIssueCommentPlaceholderParams) error { 1006 return p.executePlain("repo/issues/fragments/replyIssueCommentPlaceholder", w, params) 1007} 1008 1009type ReplyIssueCommentParams struct { 1010 LoggedInUser *oauth.User 1011 RepoInfo repoinfo.RepoInfo 1012 Issue *models.Issue 1013 Comment *models.IssueComment 1014} 1015 1016func (p *Pages) ReplyIssueCommentFragment(w io.Writer, params ReplyIssueCommentParams) error { 1017 return p.executePlain("repo/issues/fragments/replyComment", w, params) 1018} 1019 1020type IssueCommentBodyParams struct { 1021 LoggedInUser *oauth.User 1022 RepoInfo repoinfo.RepoInfo 1023 Issue *models.Issue 1024 Comment *models.IssueComment 1025} 1026 1027func (p *Pages) IssueCommentBodyFragment(w io.Writer, params IssueCommentBodyParams) error { 1028 return p.executePlain("repo/issues/fragments/issueCommentBody", w, params) 1029} 1030 1031type RepoNewPullParams struct { 1032 LoggedInUser *oauth.User 1033 RepoInfo repoinfo.RepoInfo 1034 Branches []types.Branch 1035 Strategy string 1036 SourceBranch string 1037 TargetBranch string 1038 Title string 1039 Body string 1040 Active string 1041} 1042 1043func (p *Pages) RepoNewPull(w io.Writer, params RepoNewPullParams) error { 1044 params.Active = "pulls" 1045 return p.executeRepo("repo/pulls/new", w, params) 1046} 1047 1048type RepoPullsParams struct { 1049 LoggedInUser *oauth.User 1050 RepoInfo repoinfo.RepoInfo 1051 Pulls []*models.Pull 1052 Active string 1053 FilteringBy models.PullState 1054 FilterQuery string 1055 Stacks map[string]models.Stack 1056 Pipelines map[string]models.Pipeline 1057 LabelDefs map[string]*models.LabelDefinition 1058} 1059 1060func (p *Pages) RepoPulls(w io.Writer, params RepoPullsParams) error { 1061 params.Active = "pulls" 1062 return p.executeRepo("repo/pulls/pulls", w, params) 1063} 1064 1065type ResubmitResult uint64 1066 1067const ( 1068 ShouldResubmit ResubmitResult = iota 1069 ShouldNotResubmit 1070 Unknown 1071) 1072 1073func (r ResubmitResult) Yes() bool { 1074 return r == ShouldResubmit 1075} 1076func (r ResubmitResult) No() bool { 1077 return r == ShouldNotResubmit 1078} 1079func (r ResubmitResult) Unknown() bool { 1080 return r == Unknown 1081} 1082 1083type RepoSinglePullParams struct { 1084 LoggedInUser *oauth.User 1085 RepoInfo repoinfo.RepoInfo 1086 Active string 1087 Pull *models.Pull 1088 Stack models.Stack 1089 AbandonedPulls []*models.Pull 1090 BranchDeleteStatus *models.BranchDeleteStatus 1091 MergeCheck types.MergeCheckResponse 1092 ResubmitCheck ResubmitResult 1093 Pipelines map[string]models.Pipeline 1094 1095 OrderedReactionKinds []models.ReactionKind 1096 Reactions map[models.ReactionKind]models.ReactionDisplayData 1097 UserReacted map[models.ReactionKind]bool 1098 1099 LabelDefs map[string]*models.LabelDefinition 1100} 1101 1102func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error { 1103 params.Active = "pulls" 1104 return p.executeRepo("repo/pulls/pull", w, params) 1105} 1106 1107type RepoPullPatchParams struct { 1108 LoggedInUser *oauth.User 1109 RepoInfo repoinfo.RepoInfo 1110 Pull *models.Pull 1111 Stack models.Stack 1112 Diff *types.NiceDiff 1113 Round int 1114 Submission *models.PullSubmission 1115 OrderedReactionKinds []models.ReactionKind 1116 DiffOpts types.DiffOpts 1117} 1118 1119// this name is a mouthful 1120func (p *Pages) RepoPullPatchPage(w io.Writer, params RepoPullPatchParams) error { 1121 return p.execute("repo/pulls/patch", w, params) 1122} 1123 1124type RepoPullInterdiffParams struct { 1125 LoggedInUser *oauth.User 1126 RepoInfo repoinfo.RepoInfo 1127 Pull *models.Pull 1128 Round int 1129 Interdiff *patchutil.InterdiffResult 1130 OrderedReactionKinds []models.ReactionKind 1131 DiffOpts types.DiffOpts 1132} 1133 1134// this name is a mouthful 1135func (p *Pages) RepoPullInterdiffPage(w io.Writer, params RepoPullInterdiffParams) error { 1136 return p.execute("repo/pulls/interdiff", w, params) 1137} 1138 1139type PullPatchUploadParams struct { 1140 RepoInfo repoinfo.RepoInfo 1141} 1142 1143func (p *Pages) PullPatchUploadFragment(w io.Writer, params PullPatchUploadParams) error { 1144 return p.executePlain("repo/pulls/fragments/pullPatchUpload", w, params) 1145} 1146 1147type PullCompareBranchesParams struct { 1148 RepoInfo repoinfo.RepoInfo 1149 Branches []types.Branch 1150 SourceBranch string 1151} 1152 1153func (p *Pages) PullCompareBranchesFragment(w io.Writer, params PullCompareBranchesParams) error { 1154 return p.executePlain("repo/pulls/fragments/pullCompareBranches", w, params) 1155} 1156 1157type PullCompareForkParams struct { 1158 RepoInfo repoinfo.RepoInfo 1159 Forks []models.Repo 1160 Selected string 1161} 1162 1163func (p *Pages) PullCompareForkFragment(w io.Writer, params PullCompareForkParams) error { 1164 return p.executePlain("repo/pulls/fragments/pullCompareForks", w, params) 1165} 1166 1167type PullCompareForkBranchesParams struct { 1168 RepoInfo repoinfo.RepoInfo 1169 SourceBranches []types.Branch 1170 TargetBranches []types.Branch 1171} 1172 1173func (p *Pages) PullCompareForkBranchesFragment(w io.Writer, params PullCompareForkBranchesParams) error { 1174 return p.executePlain("repo/pulls/fragments/pullCompareForksBranches", w, params) 1175} 1176 1177type PullResubmitParams struct { 1178 LoggedInUser *oauth.User 1179 RepoInfo repoinfo.RepoInfo 1180 Pull *models.Pull 1181 SubmissionId int 1182} 1183 1184func (p *Pages) PullResubmitFragment(w io.Writer, params PullResubmitParams) error { 1185 return p.executePlain("repo/pulls/fragments/pullResubmit", w, params) 1186} 1187 1188type PullActionsParams struct { 1189 LoggedInUser *oauth.User 1190 RepoInfo repoinfo.RepoInfo 1191 Pull *models.Pull 1192 RoundNumber int 1193 MergeCheck types.MergeCheckResponse 1194 ResubmitCheck ResubmitResult 1195 BranchDeleteStatus *models.BranchDeleteStatus 1196 Stack models.Stack 1197} 1198 1199func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error { 1200 return p.executePlain("repo/pulls/fragments/pullActions", w, params) 1201} 1202 1203type PullNewCommentParams struct { 1204 LoggedInUser *oauth.User 1205 RepoInfo repoinfo.RepoInfo 1206 Pull *models.Pull 1207 RoundNumber int 1208} 1209 1210func (p *Pages) PullNewCommentFragment(w io.Writer, params PullNewCommentParams) error { 1211 return p.executePlain("repo/pulls/fragments/pullNewComment", w, params) 1212} 1213 1214type RepoCompareParams struct { 1215 LoggedInUser *oauth.User 1216 RepoInfo repoinfo.RepoInfo 1217 Forks []models.Repo 1218 Branches []types.Branch 1219 Tags []*types.TagReference 1220 Base string 1221 Head string 1222 Diff *types.NiceDiff 1223 DiffOpts types.DiffOpts 1224 1225 Active string 1226} 1227 1228func (p *Pages) RepoCompare(w io.Writer, params RepoCompareParams) error { 1229 params.Active = "overview" 1230 return p.executeRepo("repo/compare/compare", w, params) 1231} 1232 1233type RepoCompareNewParams struct { 1234 LoggedInUser *oauth.User 1235 RepoInfo repoinfo.RepoInfo 1236 Forks []models.Repo 1237 Branches []types.Branch 1238 Tags []*types.TagReference 1239 Base string 1240 Head string 1241 1242 Active string 1243} 1244 1245func (p *Pages) RepoCompareNew(w io.Writer, params RepoCompareNewParams) error { 1246 params.Active = "overview" 1247 return p.executeRepo("repo/compare/new", w, params) 1248} 1249 1250type RepoCompareAllowPullParams struct { 1251 LoggedInUser *oauth.User 1252 RepoInfo repoinfo.RepoInfo 1253 Base string 1254 Head string 1255} 1256 1257func (p *Pages) RepoCompareAllowPullFragment(w io.Writer, params RepoCompareAllowPullParams) error { 1258 return p.executePlain("repo/fragments/compareAllowPull", w, params) 1259} 1260 1261type RepoCompareDiffFragmentParams struct { 1262 Diff types.NiceDiff 1263 DiffOpts types.DiffOpts 1264} 1265 1266func (p *Pages) RepoCompareDiffFragment(w io.Writer, params RepoCompareDiffFragmentParams) error { 1267 return p.executePlain("repo/fragments/diff", w, []any{&params.Diff, &params.DiffOpts}) 1268} 1269 1270type LabelPanelParams struct { 1271 LoggedInUser *oauth.User 1272 RepoInfo repoinfo.RepoInfo 1273 Defs map[string]*models.LabelDefinition 1274 Subject string 1275 State models.LabelState 1276} 1277 1278func (p *Pages) LabelPanel(w io.Writer, params LabelPanelParams) error { 1279 return p.executePlain("repo/fragments/labelPanel", w, params) 1280} 1281 1282type EditLabelPanelParams struct { 1283 LoggedInUser *oauth.User 1284 RepoInfo repoinfo.RepoInfo 1285 Defs map[string]*models.LabelDefinition 1286 Subject string 1287 State models.LabelState 1288} 1289 1290func (p *Pages) EditLabelPanel(w io.Writer, params EditLabelPanelParams) error { 1291 return p.executePlain("repo/fragments/editLabelPanel", w, params) 1292} 1293 1294type PipelinesParams struct { 1295 LoggedInUser *oauth.User 1296 RepoInfo repoinfo.RepoInfo 1297 Pipelines []models.Pipeline 1298 Active string 1299} 1300 1301func (p *Pages) Pipelines(w io.Writer, params PipelinesParams) error { 1302 params.Active = "pipelines" 1303 return p.executeRepo("repo/pipelines/pipelines", w, params) 1304} 1305 1306type LogBlockParams struct { 1307 Id int 1308 Name string 1309 Command string 1310 Collapsed bool 1311 StartTime time.Time 1312} 1313 1314func (p *Pages) LogBlock(w io.Writer, params LogBlockParams) error { 1315 return p.executePlain("repo/pipelines/fragments/logBlock", w, params) 1316} 1317 1318type LogBlockEndParams struct { 1319 Id int 1320 StartTime time.Time 1321 EndTime time.Time 1322} 1323 1324func (p *Pages) LogBlockEnd(w io.Writer, params LogBlockEndParams) error { 1325 return p.executePlain("repo/pipelines/fragments/logBlockEnd", w, params) 1326} 1327 1328type LogLineParams struct { 1329 Id int 1330 Content string 1331} 1332 1333func (p *Pages) LogLine(w io.Writer, params LogLineParams) error { 1334 return p.executePlain("repo/pipelines/fragments/logLine", w, params) 1335} 1336 1337type WorkflowParams struct { 1338 LoggedInUser *oauth.User 1339 RepoInfo repoinfo.RepoInfo 1340 Pipeline models.Pipeline 1341 Workflow string 1342 LogUrl string 1343 Active string 1344} 1345 1346func (p *Pages) Workflow(w io.Writer, params WorkflowParams) error { 1347 params.Active = "pipelines" 1348 return p.executeRepo("repo/pipelines/workflow", w, params) 1349} 1350 1351type PutStringParams struct { 1352 LoggedInUser *oauth.User 1353 Action string 1354 1355 // this is supplied in the case of editing an existing string 1356 String models.String 1357} 1358 1359func (p *Pages) PutString(w io.Writer, params PutStringParams) error { 1360 return p.execute("strings/put", w, params) 1361} 1362 1363type StringsDashboardParams struct { 1364 LoggedInUser *oauth.User 1365 Card ProfileCard 1366 Strings []models.String 1367} 1368 1369func (p *Pages) StringsDashboard(w io.Writer, params StringsDashboardParams) error { 1370 return p.execute("strings/dashboard", w, params) 1371} 1372 1373type StringTimelineParams struct { 1374 LoggedInUser *oauth.User 1375 Strings []models.String 1376} 1377 1378func (p *Pages) StringsTimeline(w io.Writer, params StringTimelineParams) error { 1379 return p.execute("strings/timeline", w, params) 1380} 1381 1382type SingleStringParams struct { 1383 LoggedInUser *oauth.User 1384 ShowRendered bool 1385 RenderToggle bool 1386 RenderedContents template.HTML 1387 String *models.String 1388 Stats models.StringStats 1389 IsStarred bool 1390 StarCount int 1391 Owner identity.Identity 1392} 1393 1394func (p *Pages) SingleString(w io.Writer, params SingleStringParams) error { 1395 return p.execute("strings/string", w, params) 1396} 1397 1398func (p *Pages) Home(w io.Writer, params TimelineParams) error { 1399 return p.execute("timeline/home", w, params) 1400} 1401 1402func (p *Pages) Static() http.Handler { 1403 if p.dev { 1404 return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static"))) 1405 } 1406 1407 sub, err := fs.Sub(p.embedFS, "static") 1408 if err != nil { 1409 p.logger.Error("no static dir found? that's crazy", "err", err) 1410 panic(err) 1411 } 1412 // Custom handler to apply Cache-Control headers for font files 1413 return Cache(http.StripPrefix("/static/", http.FileServer(http.FS(sub)))) 1414} 1415 1416func Cache(h http.Handler) http.Handler { 1417 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1418 path := strings.Split(r.URL.Path, "?")[0] 1419 1420 if strings.HasSuffix(path, ".css") { 1421 // on day for css files 1422 w.Header().Set("Cache-Control", "public, max-age=86400") 1423 } else { 1424 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") 1425 } 1426 h.ServeHTTP(w, r) 1427 }) 1428} 1429 1430func (p *Pages) CssContentHash() string { 1431 cssFile, err := p.embedFS.Open("static/tw.css") 1432 if err != nil { 1433 slog.Debug("Error opening CSS file", "err", err) 1434 return "" 1435 } 1436 defer cssFile.Close() 1437 1438 hasher := sha256.New() 1439 if _, err := io.Copy(hasher, cssFile); err != nil { 1440 slog.Debug("Error hashing CSS file", "err", err) 1441 return "" 1442 } 1443 1444 return hex.EncodeToString(hasher.Sum(nil))[:8] // Use first 8 chars of hash 1445} 1446 1447func (p *Pages) Error500(w io.Writer) error { 1448 return p.execute("errors/500", w, nil) 1449} 1450 1451func (p *Pages) Error404(w io.Writer) error { 1452 return p.execute("errors/404", w, nil) 1453} 1454 1455func (p *Pages) ErrorKnot404(w io.Writer) error { 1456 return p.execute("errors/knot404", w, nil) 1457} 1458 1459func (p *Pages) Error503(w io.Writer) error { 1460 return p.execute("errors/503", w, nil) 1461}