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