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