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