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} 411 412func (p *Pages) Knots(w io.Writer, params KnotsParams) error { 413 return p.execute("knots/index", w, params) 414} 415 416type KnotParams struct { 417 LoggedInUser *oauth.User 418 Registration *models.Registration 419 Members []string 420 Repos map[string][]models.Repo 421 IsOwner bool 422} 423 424func (p *Pages) Knot(w io.Writer, params KnotParams) error { 425 return p.execute("knots/dashboard", w, params) 426} 427 428type KnotListingParams struct { 429 *models.Registration 430} 431 432func (p *Pages) KnotListing(w io.Writer, params KnotListingParams) error { 433 return p.executePlain("knots/fragments/knotListing", w, params) 434} 435 436type SpindlesParams struct { 437 LoggedInUser *oauth.User 438 Spindles []models.Spindle 439} 440 441func (p *Pages) Spindles(w io.Writer, params SpindlesParams) error { 442 return p.execute("spindles/index", w, params) 443} 444 445type SpindleListingParams struct { 446 models.Spindle 447} 448 449func (p *Pages) SpindleListing(w io.Writer, params SpindleListingParams) error { 450 return p.executePlain("spindles/fragments/spindleListing", w, params) 451} 452 453type SpindleDashboardParams struct { 454 LoggedInUser *oauth.User 455 Spindle models.Spindle 456 Members []string 457 Repos map[string][]models.Repo 458} 459 460func (p *Pages) SpindleDashboard(w io.Writer, params SpindleDashboardParams) error { 461 return p.execute("spindles/dashboard", w, params) 462} 463 464type NewRepoParams struct { 465 LoggedInUser *oauth.User 466 Knots []string 467} 468 469func (p *Pages) NewRepo(w io.Writer, params NewRepoParams) error { 470 return p.execute("repo/new", w, params) 471} 472 473type ForkRepoParams struct { 474 LoggedInUser *oauth.User 475 Knots []string 476 RepoInfo repoinfo.RepoInfo 477} 478 479func (p *Pages) ForkRepo(w io.Writer, params ForkRepoParams) error { 480 return p.execute("repo/fork", w, params) 481} 482 483type ProfileCard struct { 484 UserDid string 485 UserHandle string 486 FollowStatus models.FollowStatus 487 Punchcard *models.Punchcard 488 Profile *models.Profile 489 Stats ProfileStats 490 Active string 491} 492 493type ProfileStats struct { 494 RepoCount int64 495 StarredCount int64 496 StringCount int64 497 FollowersCount int64 498 FollowingCount int64 499} 500 501func (p *ProfileCard) GetTabs() [][]any { 502 tabs := [][]any{ 503 {"overview", "overview", "square-chart-gantt", nil}, 504 {"repos", "repos", "book-marked", p.Stats.RepoCount}, 505 {"starred", "starred", "star", p.Stats.StarredCount}, 506 {"strings", "strings", "line-squiggle", p.Stats.StringCount}, 507 } 508 509 return tabs 510} 511 512type ProfileOverviewParams struct { 513 LoggedInUser *oauth.User 514 Repos []models.Repo 515 CollaboratingRepos []models.Repo 516 ProfileTimeline *models.ProfileTimeline 517 Card *ProfileCard 518 Active string 519} 520 521func (p *Pages) ProfileOverview(w io.Writer, params ProfileOverviewParams) error { 522 params.Active = "overview" 523 return p.executeProfile("user/overview", w, params) 524} 525 526type ProfileReposParams struct { 527 LoggedInUser *oauth.User 528 Repos []models.Repo 529 Card *ProfileCard 530 Active string 531} 532 533func (p *Pages) ProfileRepos(w io.Writer, params ProfileReposParams) error { 534 params.Active = "repos" 535 return p.executeProfile("user/repos", w, params) 536} 537 538type ProfileStarredParams struct { 539 LoggedInUser *oauth.User 540 Repos []models.Repo 541 Card *ProfileCard 542 Active string 543} 544 545func (p *Pages) ProfileStarred(w io.Writer, params ProfileStarredParams) error { 546 params.Active = "starred" 547 return p.executeProfile("user/starred", w, params) 548} 549 550type ProfileStringsParams struct { 551 LoggedInUser *oauth.User 552 Strings []models.String 553 Card *ProfileCard 554 Active string 555} 556 557func (p *Pages) ProfileStrings(w io.Writer, params ProfileStringsParams) error { 558 params.Active = "strings" 559 return p.executeProfile("user/strings", w, params) 560} 561 562type FollowCard struct { 563 UserDid string 564 LoggedInUser *oauth.User 565 FollowStatus models.FollowStatus 566 FollowersCount int64 567 FollowingCount int64 568 Profile *models.Profile 569} 570 571type ProfileFollowersParams struct { 572 LoggedInUser *oauth.User 573 Followers []FollowCard 574 Card *ProfileCard 575 Active string 576} 577 578func (p *Pages) ProfileFollowers(w io.Writer, params ProfileFollowersParams) error { 579 params.Active = "overview" 580 return p.executeProfile("user/followers", w, params) 581} 582 583type ProfileFollowingParams struct { 584 LoggedInUser *oauth.User 585 Following []FollowCard 586 Card *ProfileCard 587 Active string 588} 589 590func (p *Pages) ProfileFollowing(w io.Writer, params ProfileFollowingParams) error { 591 params.Active = "overview" 592 return p.executeProfile("user/following", w, params) 593} 594 595type FollowFragmentParams struct { 596 UserDid string 597 FollowStatus models.FollowStatus 598} 599 600func (p *Pages) FollowFragment(w io.Writer, params FollowFragmentParams) error { 601 return p.executePlain("user/fragments/follow", w, params) 602} 603 604type EditBioParams struct { 605 LoggedInUser *oauth.User 606 Profile *models.Profile 607} 608 609func (p *Pages) EditBioFragment(w io.Writer, params EditBioParams) error { 610 return p.executePlain("user/fragments/editBio", w, params) 611} 612 613type EditPinsParams struct { 614 LoggedInUser *oauth.User 615 Profile *models.Profile 616 AllRepos []PinnedRepo 617} 618 619type PinnedRepo struct { 620 IsPinned bool 621 models.Repo 622} 623 624func (p *Pages) EditPinsFragment(w io.Writer, params EditPinsParams) error { 625 return p.executePlain("user/fragments/editPins", w, params) 626} 627 628type StarBtnFragmentParams struct { 629 IsStarred bool 630 SubjectAt syntax.ATURI 631 StarCount int 632} 633 634func (p *Pages) StarBtnFragment(w io.Writer, params StarBtnFragmentParams) error { 635 return p.executePlain("fragments/starBtn", w, params) 636} 637 638type RepoIndexParams struct { 639 LoggedInUser *oauth.User 640 RepoInfo repoinfo.RepoInfo 641 Active string 642 TagMap map[string][]string 643 CommitsTrunc []*object.Commit 644 TagsTrunc []*types.TagReference 645 BranchesTrunc []types.Branch 646 // ForkInfo *types.ForkInfo 647 HTMLReadme template.HTML 648 Raw bool 649 EmailToDid map[string]string 650 VerifiedCommits commitverify.VerifiedCommits 651 Languages []types.RepoLanguageDetails 652 Pipelines map[string]models.Pipeline 653 NeedsKnotUpgrade bool 654 types.RepoIndexResponse 655} 656 657func (p *Pages) RepoIndexPage(w io.Writer, params RepoIndexParams) error { 658 params.Active = "overview" 659 if params.IsEmpty { 660 return p.executeRepo("repo/empty", w, params) 661 } 662 663 if params.NeedsKnotUpgrade { 664 return p.executeRepo("repo/needsUpgrade", w, params) 665 } 666 667 p.rctx.RepoInfo = params.RepoInfo 668 p.rctx.RepoInfo.Ref = params.Ref 669 p.rctx.RendererType = markup.RendererTypeRepoMarkdown 670 671 if params.ReadmeFileName != "" { 672 ext := filepath.Ext(params.ReadmeFileName) 673 switch ext { 674 case ".md", ".markdown", ".mdown", ".mkdn", ".mkd": 675 params.Raw = false 676 htmlString := p.rctx.RenderMarkdown(params.Readme) 677 sanitized := p.rctx.SanitizeDefault(htmlString) 678 params.HTMLReadme = template.HTML(sanitized) 679 default: 680 params.Raw = true 681 } 682 } 683 684 return p.executeRepo("repo/index", w, params) 685} 686 687type RepoLogParams struct { 688 LoggedInUser *oauth.User 689 RepoInfo repoinfo.RepoInfo 690 TagMap map[string][]string 691 Active string 692 EmailToDid map[string]string 693 VerifiedCommits commitverify.VerifiedCommits 694 Pipelines map[string]models.Pipeline 695 696 types.RepoLogResponse 697} 698 699func (p *Pages) RepoLog(w io.Writer, params RepoLogParams) error { 700 params.Active = "overview" 701 return p.executeRepo("repo/log", w, params) 702} 703 704type RepoCommitParams struct { 705 LoggedInUser *oauth.User 706 RepoInfo repoinfo.RepoInfo 707 Active string 708 EmailToDid map[string]string 709 Pipeline *models.Pipeline 710 DiffOpts types.DiffOpts 711 712 // singular because it's always going to be just one 713 VerifiedCommit commitverify.VerifiedCommits 714 715 types.RepoCommitResponse 716} 717 718func (p *Pages) RepoCommit(w io.Writer, params RepoCommitParams) error { 719 params.Active = "overview" 720 return p.executeRepo("repo/commit", w, params) 721} 722 723type RepoTreeParams struct { 724 LoggedInUser *oauth.User 725 RepoInfo repoinfo.RepoInfo 726 Active string 727 BreadCrumbs [][]string 728 TreePath string 729 Raw bool 730 HTMLReadme template.HTML 731 types.RepoTreeResponse 732} 733 734type RepoTreeStats struct { 735 NumFolders uint64 736 NumFiles uint64 737} 738 739func (r RepoTreeParams) TreeStats() RepoTreeStats { 740 numFolders, numFiles := 0, 0 741 for _, f := range r.Files { 742 if !f.IsFile() { 743 numFolders += 1 744 } else if f.IsFile() { 745 numFiles += 1 746 } 747 } 748 749 return RepoTreeStats{ 750 NumFolders: uint64(numFolders), 751 NumFiles: uint64(numFiles), 752 } 753} 754 755func (p *Pages) RepoTree(w io.Writer, params RepoTreeParams) error { 756 params.Active = "overview" 757 758 p.rctx.RepoInfo = params.RepoInfo 759 p.rctx.RepoInfo.Ref = params.Ref 760 p.rctx.RendererType = markup.RendererTypeRepoMarkdown 761 762 if params.ReadmeFileName != "" { 763 ext := filepath.Ext(params.ReadmeFileName) 764 switch ext { 765 case ".md", ".markdown", ".mdown", ".mkdn", ".mkd": 766 params.Raw = false 767 htmlString := p.rctx.RenderMarkdown(params.Readme) 768 sanitized := p.rctx.SanitizeDefault(htmlString) 769 params.HTMLReadme = template.HTML(sanitized) 770 default: 771 params.Raw = true 772 } 773 } 774 775 return p.executeRepo("repo/tree", w, params) 776} 777 778type RepoBranchesParams struct { 779 LoggedInUser *oauth.User 780 RepoInfo repoinfo.RepoInfo 781 Active string 782 types.RepoBranchesResponse 783} 784 785func (p *Pages) RepoBranches(w io.Writer, params RepoBranchesParams) error { 786 params.Active = "overview" 787 return p.executeRepo("repo/branches", w, params) 788} 789 790type RepoTagsParams struct { 791 LoggedInUser *oauth.User 792 RepoInfo repoinfo.RepoInfo 793 Active string 794 types.RepoTagsResponse 795 ArtifactMap map[plumbing.Hash][]models.Artifact 796 DanglingArtifacts []models.Artifact 797} 798 799func (p *Pages) RepoTags(w io.Writer, params RepoTagsParams) error { 800 params.Active = "overview" 801 return p.executeRepo("repo/tags", w, params) 802} 803 804type RepoArtifactParams struct { 805 LoggedInUser *oauth.User 806 RepoInfo repoinfo.RepoInfo 807 Artifact models.Artifact 808} 809 810func (p *Pages) RepoArtifactFragment(w io.Writer, params RepoArtifactParams) error { 811 return p.executePlain("repo/fragments/artifact", w, params) 812} 813 814type RepoBlobParams struct { 815 LoggedInUser *oauth.User 816 RepoInfo repoinfo.RepoInfo 817 Active string 818 BreadCrumbs [][]string 819 BlobView models.BlobView 820 *tangled.RepoBlob_Output 821} 822 823func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error { 824 switch params.BlobView.ContentType { 825 case models.BlobContentTypeMarkup: 826 p.rctx.RepoInfo = params.RepoInfo 827 } 828 829 params.Active = "overview" 830 return p.executeRepo("repo/blob", w, params) 831} 832 833type Collaborator struct { 834 Did string 835 Handle string 836 Role string 837} 838 839type RepoSettingsParams struct { 840 LoggedInUser *oauth.User 841 RepoInfo repoinfo.RepoInfo 842 Collaborators []Collaborator 843 Active string 844 Branches []types.Branch 845 Spindles []string 846 CurrentSpindle string 847 Secrets []*tangled.RepoListSecrets_Secret 848 849 // TODO: use repoinfo.roles 850 IsCollaboratorInviteAllowed bool 851} 852 853func (p *Pages) RepoSettings(w io.Writer, params RepoSettingsParams) error { 854 params.Active = "settings" 855 return p.executeRepo("repo/settings", w, params) 856} 857 858type RepoGeneralSettingsParams struct { 859 LoggedInUser *oauth.User 860 RepoInfo repoinfo.RepoInfo 861 Labels []models.LabelDefinition 862 DefaultLabels []models.LabelDefinition 863 SubscribedLabels map[string]struct{} 864 ShouldSubscribeAll bool 865 Active string 866 Tabs []map[string]any 867 Tab string 868 Branches []types.Branch 869} 870 871func (p *Pages) RepoGeneralSettings(w io.Writer, params RepoGeneralSettingsParams) error { 872 params.Active = "settings" 873 return p.executeRepo("repo/settings/general", w, params) 874} 875 876type RepoAccessSettingsParams struct { 877 LoggedInUser *oauth.User 878 RepoInfo repoinfo.RepoInfo 879 Active string 880 Tabs []map[string]any 881 Tab string 882 Collaborators []Collaborator 883} 884 885func (p *Pages) RepoAccessSettings(w io.Writer, params RepoAccessSettingsParams) error { 886 params.Active = "settings" 887 return p.executeRepo("repo/settings/access", w, params) 888} 889 890type RepoPipelineSettingsParams struct { 891 LoggedInUser *oauth.User 892 RepoInfo repoinfo.RepoInfo 893 Active string 894 Tabs []map[string]any 895 Tab string 896 Spindles []string 897 CurrentSpindle string 898 Secrets []map[string]any 899} 900 901func (p *Pages) RepoPipelineSettings(w io.Writer, params RepoPipelineSettingsParams) error { 902 params.Active = "settings" 903 return p.executeRepo("repo/settings/pipelines", w, params) 904} 905 906type RepoIssuesParams struct { 907 LoggedInUser *oauth.User 908 RepoInfo repoinfo.RepoInfo 909 Active string 910 Issues []models.Issue 911 IssueCount int 912 LabelDefs map[string]*models.LabelDefinition 913 Page pagination.Page 914 FilteringByOpen bool 915 FilterQuery string 916} 917 918func (p *Pages) RepoIssues(w io.Writer, params RepoIssuesParams) error { 919 params.Active = "issues" 920 return p.executeRepo("repo/issues/issues", w, params) 921} 922 923type RepoSingleIssueParams struct { 924 LoggedInUser *oauth.User 925 RepoInfo repoinfo.RepoInfo 926 Active string 927 Issue *models.Issue 928 CommentList []models.CommentListItem 929 LabelDefs map[string]*models.LabelDefinition 930 931 OrderedReactionKinds []models.ReactionKind 932 Reactions map[models.ReactionKind]models.ReactionDisplayData 933 UserReacted map[models.ReactionKind]bool 934} 935 936func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error { 937 params.Active = "issues" 938 return p.executeRepo("repo/issues/issue", w, params) 939} 940 941type EditIssueParams struct { 942 LoggedInUser *oauth.User 943 RepoInfo repoinfo.RepoInfo 944 Issue *models.Issue 945 Action string 946} 947 948func (p *Pages) EditIssueFragment(w io.Writer, params EditIssueParams) error { 949 params.Action = "edit" 950 return p.executePlain("repo/issues/fragments/putIssue", w, params) 951} 952 953type ThreadReactionFragmentParams struct { 954 ThreadAt syntax.ATURI 955 Kind models.ReactionKind 956 Count int 957 Users []string 958 IsReacted bool 959} 960 961func (p *Pages) ThreadReactionFragment(w io.Writer, params ThreadReactionFragmentParams) error { 962 return p.executePlain("repo/fragments/reaction", w, params) 963} 964 965type RepoNewIssueParams struct { 966 LoggedInUser *oauth.User 967 RepoInfo repoinfo.RepoInfo 968 Issue *models.Issue // existing issue if any -- passed when editing 969 Active string 970 Action string 971} 972 973func (p *Pages) RepoNewIssue(w io.Writer, params RepoNewIssueParams) error { 974 params.Active = "issues" 975 params.Action = "create" 976 return p.executeRepo("repo/issues/new", w, params) 977} 978 979type EditIssueCommentParams struct { 980 LoggedInUser *oauth.User 981 RepoInfo repoinfo.RepoInfo 982 Issue *models.Issue 983 Comment *models.IssueComment 984} 985 986func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error { 987 return p.executePlain("repo/issues/fragments/editIssueComment", w, params) 988} 989 990type ReplyIssueCommentPlaceholderParams struct { 991 LoggedInUser *oauth.User 992 RepoInfo repoinfo.RepoInfo 993 Issue *models.Issue 994 Comment *models.IssueComment 995} 996 997func (p *Pages) ReplyIssueCommentPlaceholderFragment(w io.Writer, params ReplyIssueCommentPlaceholderParams) error { 998 return p.executePlain("repo/issues/fragments/replyIssueCommentPlaceholder", w, params) 999} 1000 1001type ReplyIssueCommentParams struct { 1002 LoggedInUser *oauth.User 1003 RepoInfo repoinfo.RepoInfo 1004 Issue *models.Issue 1005 Comment *models.IssueComment 1006} 1007 1008func (p *Pages) ReplyIssueCommentFragment(w io.Writer, params ReplyIssueCommentParams) error { 1009 return p.executePlain("repo/issues/fragments/replyComment", w, params) 1010} 1011 1012type IssueCommentBodyParams struct { 1013 LoggedInUser *oauth.User 1014 RepoInfo repoinfo.RepoInfo 1015 Issue *models.Issue 1016 Comment *models.IssueComment 1017} 1018 1019func (p *Pages) IssueCommentBodyFragment(w io.Writer, params IssueCommentBodyParams) error { 1020 return p.executePlain("repo/issues/fragments/issueCommentBody", w, params) 1021} 1022 1023type RepoNewPullParams struct { 1024 LoggedInUser *oauth.User 1025 RepoInfo repoinfo.RepoInfo 1026 Branches []types.Branch 1027 Strategy string 1028 SourceBranch string 1029 TargetBranch string 1030 Title string 1031 Body string 1032 Active string 1033} 1034 1035func (p *Pages) RepoNewPull(w io.Writer, params RepoNewPullParams) error { 1036 params.Active = "pulls" 1037 return p.executeRepo("repo/pulls/new", w, params) 1038} 1039 1040type RepoPullsParams struct { 1041 LoggedInUser *oauth.User 1042 RepoInfo repoinfo.RepoInfo 1043 Pulls []*models.Pull 1044 Active string 1045 FilteringBy models.PullState 1046 FilterQuery string 1047 Stacks map[string]models.Stack 1048 Pipelines map[string]models.Pipeline 1049 LabelDefs map[string]*models.LabelDefinition 1050} 1051 1052func (p *Pages) RepoPulls(w io.Writer, params RepoPullsParams) error { 1053 params.Active = "pulls" 1054 return p.executeRepo("repo/pulls/pulls", w, params) 1055} 1056 1057type ResubmitResult uint64 1058 1059const ( 1060 ShouldResubmit ResubmitResult = iota 1061 ShouldNotResubmit 1062 Unknown 1063) 1064 1065func (r ResubmitResult) Yes() bool { 1066 return r == ShouldResubmit 1067} 1068func (r ResubmitResult) No() bool { 1069 return r == ShouldNotResubmit 1070} 1071func (r ResubmitResult) Unknown() bool { 1072 return r == Unknown 1073} 1074 1075type RepoSinglePullParams struct { 1076 LoggedInUser *oauth.User 1077 RepoInfo repoinfo.RepoInfo 1078 Active string 1079 Pull *models.Pull 1080 Stack models.Stack 1081 AbandonedPulls []*models.Pull 1082 BranchDeleteStatus *models.BranchDeleteStatus 1083 MergeCheck types.MergeCheckResponse 1084 ResubmitCheck ResubmitResult 1085 Pipelines map[string]models.Pipeline 1086 1087 OrderedReactionKinds []models.ReactionKind 1088 Reactions map[models.ReactionKind]models.ReactionDisplayData 1089 UserReacted map[models.ReactionKind]bool 1090 1091 LabelDefs map[string]*models.LabelDefinition 1092} 1093 1094func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error { 1095 params.Active = "pulls" 1096 return p.executeRepo("repo/pulls/pull", w, params) 1097} 1098 1099type RepoPullPatchParams struct { 1100 LoggedInUser *oauth.User 1101 RepoInfo repoinfo.RepoInfo 1102 Pull *models.Pull 1103 Stack models.Stack 1104 Diff *types.NiceDiff 1105 Round int 1106 Submission *models.PullSubmission 1107 OrderedReactionKinds []models.ReactionKind 1108 DiffOpts types.DiffOpts 1109} 1110 1111// this name is a mouthful 1112func (p *Pages) RepoPullPatchPage(w io.Writer, params RepoPullPatchParams) error { 1113 return p.execute("repo/pulls/patch", w, params) 1114} 1115 1116type RepoPullInterdiffParams struct { 1117 LoggedInUser *oauth.User 1118 RepoInfo repoinfo.RepoInfo 1119 Pull *models.Pull 1120 Round int 1121 Interdiff *patchutil.InterdiffResult 1122 OrderedReactionKinds []models.ReactionKind 1123 DiffOpts types.DiffOpts 1124} 1125 1126// this name is a mouthful 1127func (p *Pages) RepoPullInterdiffPage(w io.Writer, params RepoPullInterdiffParams) error { 1128 return p.execute("repo/pulls/interdiff", w, params) 1129} 1130 1131type PullPatchUploadParams struct { 1132 RepoInfo repoinfo.RepoInfo 1133} 1134 1135func (p *Pages) PullPatchUploadFragment(w io.Writer, params PullPatchUploadParams) error { 1136 return p.executePlain("repo/pulls/fragments/pullPatchUpload", w, params) 1137} 1138 1139type PullCompareBranchesParams struct { 1140 RepoInfo repoinfo.RepoInfo 1141 Branches []types.Branch 1142 SourceBranch string 1143} 1144 1145func (p *Pages) PullCompareBranchesFragment(w io.Writer, params PullCompareBranchesParams) error { 1146 return p.executePlain("repo/pulls/fragments/pullCompareBranches", w, params) 1147} 1148 1149type PullCompareForkParams struct { 1150 RepoInfo repoinfo.RepoInfo 1151 Forks []models.Repo 1152 Selected string 1153} 1154 1155func (p *Pages) PullCompareForkFragment(w io.Writer, params PullCompareForkParams) error { 1156 return p.executePlain("repo/pulls/fragments/pullCompareForks", w, params) 1157} 1158 1159type PullCompareForkBranchesParams struct { 1160 RepoInfo repoinfo.RepoInfo 1161 SourceBranches []types.Branch 1162 TargetBranches []types.Branch 1163} 1164 1165func (p *Pages) PullCompareForkBranchesFragment(w io.Writer, params PullCompareForkBranchesParams) error { 1166 return p.executePlain("repo/pulls/fragments/pullCompareForksBranches", w, params) 1167} 1168 1169type PullResubmitParams struct { 1170 LoggedInUser *oauth.User 1171 RepoInfo repoinfo.RepoInfo 1172 Pull *models.Pull 1173 SubmissionId int 1174} 1175 1176func (p *Pages) PullResubmitFragment(w io.Writer, params PullResubmitParams) error { 1177 return p.executePlain("repo/pulls/fragments/pullResubmit", w, params) 1178} 1179 1180type PullActionsParams struct { 1181 LoggedInUser *oauth.User 1182 RepoInfo repoinfo.RepoInfo 1183 Pull *models.Pull 1184 RoundNumber int 1185 MergeCheck types.MergeCheckResponse 1186 ResubmitCheck ResubmitResult 1187 BranchDeleteStatus *models.BranchDeleteStatus 1188 Stack models.Stack 1189} 1190 1191func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error { 1192 return p.executePlain("repo/pulls/fragments/pullActions", w, params) 1193} 1194 1195type PullNewCommentParams struct { 1196 LoggedInUser *oauth.User 1197 RepoInfo repoinfo.RepoInfo 1198 Pull *models.Pull 1199 RoundNumber int 1200} 1201 1202func (p *Pages) PullNewCommentFragment(w io.Writer, params PullNewCommentParams) error { 1203 return p.executePlain("repo/pulls/fragments/pullNewComment", w, params) 1204} 1205 1206type RepoCompareParams struct { 1207 LoggedInUser *oauth.User 1208 RepoInfo repoinfo.RepoInfo 1209 Forks []models.Repo 1210 Branches []types.Branch 1211 Tags []*types.TagReference 1212 Base string 1213 Head string 1214 Diff *types.NiceDiff 1215 DiffOpts types.DiffOpts 1216 1217 Active string 1218} 1219 1220func (p *Pages) RepoCompare(w io.Writer, params RepoCompareParams) error { 1221 params.Active = "overview" 1222 return p.executeRepo("repo/compare/compare", w, params) 1223} 1224 1225type RepoCompareNewParams struct { 1226 LoggedInUser *oauth.User 1227 RepoInfo repoinfo.RepoInfo 1228 Forks []models.Repo 1229 Branches []types.Branch 1230 Tags []*types.TagReference 1231 Base string 1232 Head string 1233 1234 Active string 1235} 1236 1237func (p *Pages) RepoCompareNew(w io.Writer, params RepoCompareNewParams) error { 1238 params.Active = "overview" 1239 return p.executeRepo("repo/compare/new", w, params) 1240} 1241 1242type RepoCompareAllowPullParams struct { 1243 LoggedInUser *oauth.User 1244 RepoInfo repoinfo.RepoInfo 1245 Base string 1246 Head string 1247} 1248 1249func (p *Pages) RepoCompareAllowPullFragment(w io.Writer, params RepoCompareAllowPullParams) error { 1250 return p.executePlain("repo/fragments/compareAllowPull", w, params) 1251} 1252 1253type RepoCompareDiffParams struct { 1254 LoggedInUser *oauth.User 1255 RepoInfo repoinfo.RepoInfo 1256 Diff types.NiceDiff 1257} 1258 1259func (p *Pages) RepoCompareDiff(w io.Writer, params RepoCompareDiffParams) error { 1260 return p.executePlain("repo/fragments/diff", w, []any{params.RepoInfo.FullName, &params.Diff}) 1261} 1262 1263type LabelPanelParams struct { 1264 LoggedInUser *oauth.User 1265 RepoInfo repoinfo.RepoInfo 1266 Defs map[string]*models.LabelDefinition 1267 Subject string 1268 State models.LabelState 1269} 1270 1271func (p *Pages) LabelPanel(w io.Writer, params LabelPanelParams) error { 1272 return p.executePlain("repo/fragments/labelPanel", w, params) 1273} 1274 1275type EditLabelPanelParams struct { 1276 LoggedInUser *oauth.User 1277 RepoInfo repoinfo.RepoInfo 1278 Defs map[string]*models.LabelDefinition 1279 Subject string 1280 State models.LabelState 1281} 1282 1283func (p *Pages) EditLabelPanel(w io.Writer, params EditLabelPanelParams) error { 1284 return p.executePlain("repo/fragments/editLabelPanel", w, params) 1285} 1286 1287type PipelinesParams struct { 1288 LoggedInUser *oauth.User 1289 RepoInfo repoinfo.RepoInfo 1290 Pipelines []models.Pipeline 1291 Active string 1292} 1293 1294func (p *Pages) Pipelines(w io.Writer, params PipelinesParams) error { 1295 params.Active = "pipelines" 1296 return p.executeRepo("repo/pipelines/pipelines", w, params) 1297} 1298 1299type LogBlockParams struct { 1300 Id int 1301 Name string 1302 Command string 1303 Collapsed bool 1304 StartTime time.Time 1305} 1306 1307func (p *Pages) LogBlock(w io.Writer, params LogBlockParams) error { 1308 return p.executePlain("repo/pipelines/fragments/logBlock", w, params) 1309} 1310 1311type LogBlockEndParams struct { 1312 Id int 1313 StartTime time.Time 1314 EndTime time.Time 1315} 1316 1317func (p *Pages) LogBlockEnd(w io.Writer, params LogBlockEndParams) error { 1318 return p.executePlain("repo/pipelines/fragments/logBlockEnd", w, params) 1319} 1320 1321type LogLineParams struct { 1322 Id int 1323 Content string 1324} 1325 1326func (p *Pages) LogLine(w io.Writer, params LogLineParams) error { 1327 return p.executePlain("repo/pipelines/fragments/logLine", w, params) 1328} 1329 1330type WorkflowParams struct { 1331 LoggedInUser *oauth.User 1332 RepoInfo repoinfo.RepoInfo 1333 Pipeline models.Pipeline 1334 Workflow string 1335 LogUrl string 1336 Active string 1337} 1338 1339func (p *Pages) Workflow(w io.Writer, params WorkflowParams) error { 1340 params.Active = "pipelines" 1341 return p.executeRepo("repo/pipelines/workflow", w, params) 1342} 1343 1344type PutStringParams struct { 1345 LoggedInUser *oauth.User 1346 Action string 1347 1348 // this is supplied in the case of editing an existing string 1349 String models.String 1350} 1351 1352func (p *Pages) PutString(w io.Writer, params PutStringParams) error { 1353 return p.execute("strings/put", w, params) 1354} 1355 1356type StringsDashboardParams struct { 1357 LoggedInUser *oauth.User 1358 Card ProfileCard 1359 Strings []models.String 1360} 1361 1362func (p *Pages) StringsDashboard(w io.Writer, params StringsDashboardParams) error { 1363 return p.execute("strings/dashboard", w, params) 1364} 1365 1366type StringTimelineParams struct { 1367 LoggedInUser *oauth.User 1368 Strings []models.String 1369} 1370 1371func (p *Pages) StringsTimeline(w io.Writer, params StringTimelineParams) error { 1372 return p.execute("strings/timeline", w, params) 1373} 1374 1375type SingleStringParams struct { 1376 LoggedInUser *oauth.User 1377 ShowRendered bool 1378 RenderToggle bool 1379 RenderedContents template.HTML 1380 String *models.String 1381 Stats models.StringStats 1382 IsStarred bool 1383 StarCount int 1384 Owner identity.Identity 1385} 1386 1387func (p *Pages) SingleString(w io.Writer, params SingleStringParams) error { 1388 return p.execute("strings/string", w, params) 1389} 1390 1391func (p *Pages) Home(w io.Writer, params TimelineParams) error { 1392 return p.execute("timeline/home", w, params) 1393} 1394 1395func (p *Pages) Static() http.Handler { 1396 if p.dev { 1397 return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static"))) 1398 } 1399 1400 sub, err := fs.Sub(p.embedFS, "static") 1401 if err != nil { 1402 p.logger.Error("no static dir found? that's crazy", "err", err) 1403 panic(err) 1404 } 1405 // Custom handler to apply Cache-Control headers for font files 1406 return Cache(http.StripPrefix("/static/", http.FileServer(http.FS(sub)))) 1407} 1408 1409func Cache(h http.Handler) http.Handler { 1410 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1411 path := strings.Split(r.URL.Path, "?")[0] 1412 1413 if strings.HasSuffix(path, ".css") { 1414 // on day for css files 1415 w.Header().Set("Cache-Control", "public, max-age=86400") 1416 } else { 1417 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") 1418 } 1419 h.ServeHTTP(w, r) 1420 }) 1421} 1422 1423func (p *Pages) CssContentHash() string { 1424 cssFile, err := p.embedFS.Open("static/tw.css") 1425 if err != nil { 1426 slog.Debug("Error opening CSS file", "err", err) 1427 return "" 1428 } 1429 defer cssFile.Close() 1430 1431 hasher := sha256.New() 1432 if _, err := io.Copy(hasher, cssFile); err != nil { 1433 slog.Debug("Error hashing CSS file", "err", err) 1434 return "" 1435 } 1436 1437 return hex.EncodeToString(hasher.Sum(nil))[:8] // Use first 8 chars of hash 1438} 1439 1440func (p *Pages) Error500(w io.Writer) error { 1441 return p.execute("errors/500", w, nil) 1442} 1443 1444func (p *Pages) Error404(w io.Writer) error { 1445 return p.execute("errors/404", w, nil) 1446} 1447 1448func (p *Pages) ErrorKnot404(w io.Writer) error { 1449 return p.execute("errors/knot404", w, nil) 1450} 1451 1452func (p *Pages) Error503(w io.Writer) error { 1453 return p.execute("errors/503", w, nil) 1454}