Monorepo for Tangled
at disable-punchcard 787 lines 22 kB view raw
1package state 2 3import ( 4 "context" 5 "fmt" 6 "log" 7 "net/http" 8 "slices" 9 "strings" 10 "time" 11 12 comatproto "github.com/bluesky-social/indigo/api/atproto" 13 "github.com/bluesky-social/indigo/atproto/identity" 14 "github.com/bluesky-social/indigo/atproto/syntax" 15 lexutil "github.com/bluesky-social/indigo/lex/util" 16 "github.com/go-chi/chi/v5" 17 "github.com/gorilla/feeds" 18 "tangled.org/core/api/tangled" 19 "tangled.org/core/appview/db" 20 "tangled.org/core/appview/models" 21 "tangled.org/core/appview/pages" 22 "tangled.org/core/orm" 23) 24 25func (s *State) Profile(w http.ResponseWriter, r *http.Request) { 26 tabVal := r.URL.Query().Get("tab") 27 switch tabVal { 28 case "repos": 29 s.reposPage(w, r) 30 case "followers": 31 s.followersPage(w, r) 32 case "following": 33 s.followingPage(w, r) 34 case "starred": 35 s.starredPage(w, r) 36 case "strings": 37 s.stringsPage(w, r) 38 default: 39 s.profileOverview(w, r) 40 } 41} 42 43func (s *State) profile(r *http.Request) (*pages.ProfileCard, error) { 44 didOrHandle := chi.URLParam(r, "user") 45 if didOrHandle == "" { 46 return nil, fmt.Errorf("empty DID or handle") 47 } 48 49 ident, ok := r.Context().Value("resolvedId").(identity.Identity) 50 if !ok { 51 return nil, fmt.Errorf("failed to resolve ID") 52 } 53 did := ident.DID.String() 54 55 profile, err := db.GetProfile(s.db, did) 56 if err != nil { 57 return nil, fmt.Errorf("failed to get profile: %w", err) 58 } 59 60 repoCount, err := db.CountRepos(s.db, orm.FilterEq("did", did)) 61 if err != nil { 62 return nil, fmt.Errorf("failed to get repo count: %w", err) 63 } 64 65 stringCount, err := db.CountStrings(s.db, orm.FilterEq("did", did)) 66 if err != nil { 67 return nil, fmt.Errorf("failed to get string count: %w", err) 68 } 69 70 starredCount, err := db.CountStars(s.db, orm.FilterEq("did", did)) 71 if err != nil { 72 return nil, fmt.Errorf("failed to get starred repo count: %w", err) 73 } 74 75 followStats, err := db.GetFollowerFollowingCount(s.db, did) 76 if err != nil { 77 return nil, fmt.Errorf("failed to get follower stats: %w", err) 78 } 79 80 loggedInUser := s.oauth.GetMultiAccountUser(r) 81 followStatus := models.IsNotFollowing 82 if loggedInUser != nil { 83 followStatus = db.GetFollowStatus(s.db, loggedInUser.Active.Did, did) 84 } 85 86 now := time.Now() 87 startOfYear := time.Date(now.Year(), 1, 1, 0, 0, 0, 0, time.UTC) 88 punchcard, err := db.MakePunchcard( 89 s.db, 90 orm.FilterEq("did", did), 91 orm.FilterGte("date", startOfYear.Format(time.DateOnly)), 92 orm.FilterLte("date", now.Format(time.DateOnly)), 93 ) 94 if err != nil { 95 return nil, fmt.Errorf("failed to get punchcard for %s: %w", did, err) 96 } 97 98 return &pages.ProfileCard{ 99 UserDid: did, 100 Profile: profile, 101 FollowStatus: followStatus, 102 Stats: pages.ProfileStats{ 103 RepoCount: repoCount, 104 StringCount: stringCount, 105 StarredCount: starredCount, 106 FollowersCount: followStats.Followers, 107 FollowingCount: followStats.Following, 108 }, 109 Punchcard: punchcard, 110 }, nil 111} 112 113func (s *State) profileOverview(w http.ResponseWriter, r *http.Request) { 114 l := s.logger.With("handler", "profileHomePage") 115 116 profile, err := s.profile(r) 117 if err != nil { 118 l.Error("failed to build profile card", "err", err) 119 s.pages.Error500(w) 120 return 121 } 122 l = l.With("profileDid", profile.UserDid) 123 124 repos, err := db.GetRepos( 125 s.db, 126 0, 127 orm.FilterEq("did", profile.UserDid), 128 ) 129 if err != nil { 130 l.Error("failed to fetch repos", "err", err) 131 } 132 133 // filter out ones that are pinned 134 pinnedRepos := []models.Repo{} 135 for i, r := range repos { 136 // if this is a pinned repo, add it 137 if slices.Contains(profile.Profile.PinnedRepos[:], r.RepoAt()) { 138 pinnedRepos = append(pinnedRepos, r) 139 } 140 141 // if there are no saved pins, add the first 4 repos 142 if profile.Profile.IsPinnedReposEmpty() && i < 4 { 143 pinnedRepos = append(pinnedRepos, r) 144 } 145 } 146 147 collaboratingRepos, err := db.CollaboratingIn(s.db, profile.UserDid) 148 if err != nil { 149 l.Error("failed to fetch collaborating repos", "err", err) 150 } 151 152 pinnedCollaboratingRepos := []models.Repo{} 153 for _, r := range collaboratingRepos { 154 // if this is a pinned repo, add it 155 if slices.Contains(profile.Profile.PinnedRepos[:], r.RepoAt()) { 156 pinnedCollaboratingRepos = append(pinnedCollaboratingRepos, r) 157 } 158 } 159 160 loggedInUser := s.oauth.GetMultiAccountUser(r) 161 162 forProfile, err := db.GetProfile(s.db, profile.UserDid) 163 if err != nil { 164 l.Error("failed to get for profile to check punchcard settings", "err", err) 165 } 166 requesterProfile, err := db.GetProfile(s.db, loggedInUser.Did()) 167 if err != nil { 168 l.Error("failed to get requester profile to check punchcard settings", "err", err) 169 } 170 171 showPunchcard := true 172 if forProfile != nil && forProfile.PunchardSetting == models.ProfilePunchcardOptionHideMine { 173 showPunchcard = false 174 } 175 if requesterProfile != nil && requesterProfile.PunchardSetting == models.ProfilePunchcardOptionHideAll { 176 showPunchcard = false 177 } 178 179 timeline, err := db.MakeProfileTimeline(s.db, profile.UserDid, showPunchcard) 180 if err != nil { 181 l.Error("failed to create timeline", "err", err) 182 } 183 184 loggedInUserProfile, err := db.GetProfile(s.db, loggedInUser.Did()) 185 if err != nil { 186 l.Error("failed to get logged in user profile to check punchcard settings", "err", err) 187 } 188 189 if loggedInUserProfile != nil && loggedInUserProfile.PunchardSetting == models.ProfilePunchcardOptionHideAll { 190 191 } 192 193 s.pages.ProfileOverview(w, pages.ProfileOverviewParams{ 194 LoggedInUser: loggedInUser, 195 Card: profile, 196 Repos: pinnedRepos, 197 CollaboratingRepos: pinnedCollaboratingRepos, 198 ProfileTimeline: timeline, 199 ShowPunchcard: showPunchcard, 200 }) 201} 202 203func (s *State) reposPage(w http.ResponseWriter, r *http.Request) { 204 l := s.logger.With("handler", "reposPage") 205 206 profile, err := s.profile(r) 207 if err != nil { 208 l.Error("failed to build profile card", "err", err) 209 s.pages.Error500(w) 210 return 211 } 212 l = l.With("profileDid", profile.UserDid) 213 214 repos, err := db.GetRepos( 215 s.db, 216 0, 217 orm.FilterEq("did", profile.UserDid), 218 ) 219 if err != nil { 220 l.Error("failed to get repos", "err", err) 221 s.pages.Error500(w) 222 return 223 } 224 225 err = s.pages.ProfileRepos(w, pages.ProfileReposParams{ 226 LoggedInUser: s.oauth.GetMultiAccountUser(r), 227 Repos: repos, 228 Card: profile, 229 }) 230} 231 232func (s *State) starredPage(w http.ResponseWriter, r *http.Request) { 233 l := s.logger.With("handler", "starredPage") 234 235 profile, err := s.profile(r) 236 if err != nil { 237 l.Error("failed to build profile card", "err", err) 238 s.pages.Error500(w) 239 return 240 } 241 l = l.With("profileDid", profile.UserDid) 242 243 stars, err := db.GetRepoStars(s.db, 0, orm.FilterEq("did", profile.UserDid)) 244 if err != nil { 245 l.Error("failed to get stars", "err", err) 246 s.pages.Error500(w) 247 return 248 } 249 var repos []models.Repo 250 for _, s := range stars { 251 repos = append(repos, *s.Repo) 252 } 253 254 err = s.pages.ProfileStarred(w, pages.ProfileStarredParams{ 255 LoggedInUser: s.oauth.GetMultiAccountUser(r), 256 Repos: repos, 257 Card: profile, 258 }) 259} 260 261func (s *State) stringsPage(w http.ResponseWriter, r *http.Request) { 262 l := s.logger.With("handler", "stringsPage") 263 264 profile, err := s.profile(r) 265 if err != nil { 266 l.Error("failed to build profile card", "err", err) 267 s.pages.Error500(w) 268 return 269 } 270 l = l.With("profileDid", profile.UserDid) 271 272 strings, err := db.GetStrings(s.db, 0, orm.FilterEq("did", profile.UserDid)) 273 if err != nil { 274 l.Error("failed to get strings", "err", err) 275 s.pages.Error500(w) 276 return 277 } 278 279 err = s.pages.ProfileStrings(w, pages.ProfileStringsParams{ 280 LoggedInUser: s.oauth.GetMultiAccountUser(r), 281 Strings: strings, 282 Card: profile, 283 }) 284} 285 286type FollowsPageParams struct { 287 Follows []pages.FollowCard 288 Card *pages.ProfileCard 289} 290 291func (s *State) followPage( 292 r *http.Request, 293 fetchFollows func(db.Execer, string) ([]models.Follow, error), 294 extractDid func(models.Follow) string, 295) (*FollowsPageParams, error) { 296 l := s.logger.With("handler", "reposPage") 297 298 profile, err := s.profile(r) 299 if err != nil { 300 return nil, err 301 } 302 l = l.With("profileDid", profile.UserDid) 303 304 loggedInUser := s.oauth.GetMultiAccountUser(r) 305 params := FollowsPageParams{ 306 Card: profile, 307 } 308 309 follows, err := fetchFollows(s.db, profile.UserDid) 310 if err != nil { 311 l.Error("failed to fetch follows", "err", err) 312 return &params, err 313 } 314 315 if len(follows) == 0 { 316 return &params, nil 317 } 318 319 followDids := make([]string, 0, len(follows)) 320 for _, follow := range follows { 321 followDids = append(followDids, extractDid(follow)) 322 } 323 324 profiles, err := db.GetProfiles(s.db, orm.FilterIn("did", followDids)) 325 if err != nil { 326 l.Error("failed to get profiles", "followDids", followDids, "err", err) 327 return &params, err 328 } 329 330 followStatsMap, err := db.GetFollowerFollowingCounts(s.db, followDids) 331 if err != nil { 332 log.Printf("getting follow counts for %s: %s", followDids, err) 333 } 334 335 loggedInUserFollowing := make(map[string]struct{}) 336 if loggedInUser != nil { 337 following, err := db.GetFollowing(s.db, loggedInUser.Active.Did) 338 if err != nil { 339 l.Error("failed to get follow list", "err", err, "loggedInUser", loggedInUser.Active.Did) 340 return &params, err 341 } 342 loggedInUserFollowing = make(map[string]struct{}, len(following)) 343 for _, follow := range following { 344 loggedInUserFollowing[follow.SubjectDid] = struct{}{} 345 } 346 } 347 348 followCards := make([]pages.FollowCard, len(follows)) 349 for i, did := range followDids { 350 followStats := followStatsMap[did] 351 followStatus := models.IsNotFollowing 352 if _, exists := loggedInUserFollowing[did]; exists { 353 followStatus = models.IsFollowing 354 } else if loggedInUser != nil && loggedInUser.Active.Did == did { 355 followStatus = models.IsSelf 356 } 357 358 var profile *models.Profile 359 if p, exists := profiles[did]; exists { 360 profile = p 361 } else { 362 profile = &models.Profile{} 363 profile.Did = did 364 } 365 followCards[i] = pages.FollowCard{ 366 LoggedInUser: loggedInUser, 367 UserDid: did, 368 FollowStatus: followStatus, 369 FollowersCount: followStats.Followers, 370 FollowingCount: followStats.Following, 371 Profile: profile, 372 } 373 } 374 375 params.Follows = followCards 376 377 return &params, nil 378} 379 380func (s *State) followersPage(w http.ResponseWriter, r *http.Request) { 381 followPage, err := s.followPage(r, db.GetFollowers, func(f models.Follow) string { return f.UserDid }) 382 if err != nil { 383 s.pages.Notice(w, "all-followers", "Failed to load followers") 384 return 385 } 386 387 s.pages.ProfileFollowers(w, pages.ProfileFollowersParams{ 388 LoggedInUser: s.oauth.GetMultiAccountUser(r), 389 Followers: followPage.Follows, 390 Card: followPage.Card, 391 }) 392} 393 394func (s *State) followingPage(w http.ResponseWriter, r *http.Request) { 395 followPage, err := s.followPage(r, db.GetFollowing, func(f models.Follow) string { return f.SubjectDid }) 396 if err != nil { 397 s.pages.Notice(w, "all-following", "Failed to load following") 398 return 399 } 400 401 s.pages.ProfileFollowing(w, pages.ProfileFollowingParams{ 402 LoggedInUser: s.oauth.GetMultiAccountUser(r), 403 Following: followPage.Follows, 404 Card: followPage.Card, 405 }) 406} 407 408func (s *State) AtomFeedPage(w http.ResponseWriter, r *http.Request) { 409 ident, ok := r.Context().Value("resolvedId").(identity.Identity) 410 if !ok { 411 s.pages.Error404(w) 412 return 413 } 414 415 feed, err := s.getProfileFeed(r.Context(), &ident) 416 if err != nil { 417 s.pages.Error500(w) 418 return 419 } 420 421 if feed == nil { 422 return 423 } 424 425 atom, err := feed.ToAtom() 426 if err != nil { 427 s.pages.Error500(w) 428 return 429 } 430 431 w.Header().Set("content-type", "application/atom+xml") 432 w.Write([]byte(atom)) 433} 434 435func (s *State) getProfileFeed(ctx context.Context, id *identity.Identity) (*feeds.Feed, error) { 436 timeline, err := db.MakeProfileTimeline(s.db, id.DID.String(), false) 437 if err != nil { 438 return nil, err 439 } 440 441 author := &feeds.Author{ 442 Name: fmt.Sprintf("@%s", id.Handle), 443 } 444 445 feed := feeds.Feed{ 446 Title: fmt.Sprintf("%s's timeline", author.Name), 447 Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s", s.config.Core.AppviewHost, id.Handle), Type: "text/html", Rel: "alternate"}, 448 Items: make([]*feeds.Item, 0), 449 Updated: time.UnixMilli(0), 450 Author: author, 451 } 452 453 for _, byMonth := range timeline.ByMonth { 454 if err := s.addPullRequestItems(ctx, &feed, byMonth.PullEvents.Items, author); err != nil { 455 return nil, err 456 } 457 if err := s.addIssueItems(ctx, &feed, byMonth.IssueEvents.Items, author); err != nil { 458 return nil, err 459 } 460 if err := s.addRepoItems(ctx, &feed, byMonth.RepoEvents, author); err != nil { 461 return nil, err 462 } 463 } 464 465 slices.SortFunc(feed.Items, func(a *feeds.Item, b *feeds.Item) int { 466 return int(b.Created.UnixMilli()) - int(a.Created.UnixMilli()) 467 }) 468 469 if len(feed.Items) > 0 { 470 feed.Updated = feed.Items[0].Created 471 } 472 473 return &feed, nil 474} 475 476func (s *State) addPullRequestItems(ctx context.Context, feed *feeds.Feed, pulls []*models.Pull, author *feeds.Author) error { 477 for _, pull := range pulls { 478 owner, err := s.idResolver.ResolveIdent(ctx, pull.Repo.Did) 479 if err != nil { 480 return err 481 } 482 483 // Add pull request creation item 484 feed.Items = append(feed.Items, s.createPullRequestItem(pull, owner, author)) 485 } 486 return nil 487} 488 489func (s *State) addIssueItems(ctx context.Context, feed *feeds.Feed, issues []*models.Issue, author *feeds.Author) error { 490 for _, issue := range issues { 491 owner, err := s.idResolver.ResolveIdent(ctx, issue.Repo.Did) 492 if err != nil { 493 return err 494 } 495 496 feed.Items = append(feed.Items, s.createIssueItem(issue, owner, author)) 497 } 498 return nil 499} 500 501func (s *State) addRepoItems(ctx context.Context, feed *feeds.Feed, repos []models.RepoEvent, author *feeds.Author) error { 502 for _, repo := range repos { 503 item, err := s.createRepoItem(ctx, repo, author) 504 if err != nil { 505 return err 506 } 507 feed.Items = append(feed.Items, item) 508 } 509 return nil 510} 511 512func (s *State) createPullRequestItem(pull *models.Pull, owner *identity.Identity, author *feeds.Author) *feeds.Item { 513 return &feeds.Item{ 514 Title: fmt.Sprintf("%s created pull request '%s' in @%s/%s", author.Name, pull.Title, owner.Handle, pull.Repo.Name), 515 Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s/%s/pulls/%d", s.config.Core.AppviewHost, owner.Handle, pull.Repo.Name, pull.PullId), Type: "text/html", Rel: "alternate"}, 516 Created: pull.Created, 517 Author: author, 518 } 519} 520 521func (s *State) createIssueItem(issue *models.Issue, owner *identity.Identity, author *feeds.Author) *feeds.Item { 522 return &feeds.Item{ 523 Title: fmt.Sprintf("%s created issue '%s' in @%s/%s", author.Name, issue.Title, owner.Handle, issue.Repo.Name), 524 Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s/%s/issues/%d", s.config.Core.AppviewHost, owner.Handle, issue.Repo.Name, issue.IssueId), Type: "text/html", Rel: "alternate"}, 525 Created: issue.Created, 526 Author: author, 527 } 528} 529 530func (s *State) createRepoItem(ctx context.Context, repo models.RepoEvent, author *feeds.Author) (*feeds.Item, error) { 531 var title string 532 if repo.Source != nil { 533 sourceOwner, err := s.idResolver.ResolveIdent(ctx, repo.Source.Did) 534 if err != nil { 535 return nil, err 536 } 537 title = fmt.Sprintf("%s forked repository @%s/%s to '%s'", author.Name, sourceOwner.Handle, repo.Source.Name, repo.Repo.Name) 538 } else { 539 title = fmt.Sprintf("%s created repository '%s'", author.Name, repo.Repo.Name) 540 } 541 542 return &feeds.Item{ 543 Title: title, 544 Link: &feeds.Link{Href: fmt.Sprintf("%s/@%s/%s", s.config.Core.AppviewHost, author.Name[1:], repo.Repo.Name), Type: "text/html", Rel: "alternate"}, // Remove @ prefix 545 Created: repo.Repo.Created, 546 Author: author, 547 }, nil 548} 549 550func (s *State) UpdateProfileBio(w http.ResponseWriter, r *http.Request) { 551 user := s.oauth.GetMultiAccountUser(r) 552 553 err := r.ParseForm() 554 if err != nil { 555 log.Println("invalid profile update form", err) 556 s.pages.Notice(w, "update-profile", "Invalid form.") 557 return 558 } 559 560 profile, err := db.GetProfile(s.db, user.Active.Did) 561 if err != nil { 562 log.Printf("getting profile data for %s: %s", user.Active.Did, err) 563 } 564 565 profile.Description = r.FormValue("description") 566 profile.IncludeBluesky = r.FormValue("includeBluesky") == "on" 567 profile.Location = r.FormValue("location") 568 profile.Pronouns = r.FormValue("pronouns") 569 570 var links [5]string 571 for i := range 5 { 572 iLink := r.FormValue(fmt.Sprintf("link%d", i)) 573 links[i] = iLink 574 } 575 profile.Links = links 576 577 // Parse stats (exactly 2) 578 stat0 := r.FormValue("stat0") 579 stat1 := r.FormValue("stat1") 580 581 if stat0 != "" { 582 profile.Stats[0].Kind = models.VanityStatKind(stat0) 583 } 584 585 if stat1 != "" { 586 profile.Stats[1].Kind = models.VanityStatKind(stat1) 587 } 588 589 if err := db.ValidateProfile(s.db, profile); err != nil { 590 log.Println("invalid profile", err) 591 s.pages.Notice(w, "update-profile", err.Error()) 592 return 593 } 594 595 s.updateProfile(profile, w, r) 596} 597 598func (s *State) UpdateProfilePins(w http.ResponseWriter, r *http.Request) { 599 user := s.oauth.GetMultiAccountUser(r) 600 601 err := r.ParseForm() 602 if err != nil { 603 log.Println("invalid profile update form", err) 604 s.pages.Notice(w, "update-profile", "Invalid form.") 605 return 606 } 607 608 profile, err := db.GetProfile(s.db, user.Active.Did) 609 if err != nil { 610 log.Printf("getting profile data for %s: %s", user.Active.Did, err) 611 } 612 613 i := 0 614 var pinnedRepos [6]syntax.ATURI 615 for key, values := range r.Form { 616 if i >= 6 { 617 log.Println("invalid pin update form", err) 618 s.pages.Notice(w, "update-profile", "Only 6 repositories can be pinned at a time.") 619 return 620 } 621 if strings.HasPrefix(key, "pinnedRepo") && len(values) > 0 && values[0] != "" && i < 6 { 622 aturi, err := syntax.ParseATURI(values[0]) 623 if err != nil { 624 log.Println("invalid profile update form", err) 625 s.pages.Notice(w, "update-profile", "Invalid form.") 626 return 627 } 628 pinnedRepos[i] = aturi 629 i++ 630 } 631 } 632 profile.PinnedRepos = pinnedRepos 633 634 s.updateProfile(profile, w, r) 635} 636 637func (s *State) updateProfile(profile *models.Profile, w http.ResponseWriter, r *http.Request) { 638 user := s.oauth.GetMultiAccountUser(r) 639 tx, err := s.db.BeginTx(r.Context(), nil) 640 if err != nil { 641 log.Println("failed to start transaction", err) 642 s.pages.Notice(w, "update-profile", "Failed to update profile, try again later.") 643 return 644 } 645 646 client, err := s.oauth.AuthorizedClient(r) 647 if err != nil { 648 log.Println("failed to get authorized client", err) 649 s.pages.Notice(w, "update-profile", "Failed to update profile, try again later.") 650 return 651 } 652 653 // yeah... lexgen dose not support syntax.ATURI in the record for some reason, 654 // nor does it support exact size arrays 655 var pinnedRepoStrings []string 656 for _, r := range profile.PinnedRepos { 657 pinnedRepoStrings = append(pinnedRepoStrings, r.String()) 658 } 659 660 var vanityStats []string 661 for _, v := range profile.Stats { 662 vanityStats = append(vanityStats, string(v.Kind)) 663 } 664 665 ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.ActorProfileNSID, user.Active.Did, "self") 666 var cid *string 667 if ex != nil { 668 cid = ex.Cid 669 } 670 671 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 672 Collection: tangled.ActorProfileNSID, 673 Repo: user.Active.Did, 674 Rkey: "self", 675 Record: &lexutil.LexiconTypeDecoder{ 676 Val: &tangled.ActorProfile{ 677 Bluesky: profile.IncludeBluesky, 678 Description: &profile.Description, 679 Links: profile.Links[:], 680 Location: &profile.Location, 681 PinnedRepositories: pinnedRepoStrings, 682 Stats: vanityStats[:], 683 Pronouns: &profile.Pronouns, 684 }}, 685 SwapRecord: cid, 686 }) 687 if err != nil { 688 log.Println("failed to update profile", err) 689 s.pages.Notice(w, "update-profile", "Failed to update PDS, try again later.") 690 return 691 } 692 693 err = db.UpsertProfile(tx, profile) 694 if err != nil { 695 log.Println("failed to update profile", err) 696 s.pages.Notice(w, "update-profile", "Failed to update profile, try again later.") 697 return 698 } 699 700 s.notifier.UpdateProfile(r.Context(), profile) 701 702 s.pages.HxRedirect(w, "/"+user.Active.Did) 703} 704 705func (s *State) EditBioFragment(w http.ResponseWriter, r *http.Request) { 706 user := s.oauth.GetMultiAccountUser(r) 707 708 profile, err := db.GetProfile(s.db, user.Active.Did) 709 if err != nil { 710 log.Printf("getting profile data for %s: %s", user.Active.Did, err) 711 } 712 713 s.pages.EditBioFragment(w, pages.EditBioParams{ 714 LoggedInUser: user, 715 Profile: profile, 716 }) 717} 718 719func (s *State) EditPinsFragment(w http.ResponseWriter, r *http.Request) { 720 user := s.oauth.GetMultiAccountUser(r) 721 722 profile, err := db.GetProfile(s.db, user.Active.Did) 723 if err != nil { 724 log.Printf("getting profile data for %s: %s", user.Active.Did, err) 725 } 726 727 repos, err := db.GetRepos(s.db, 0, orm.FilterEq("did", user.Active.Did)) 728 if err != nil { 729 log.Printf("getting repos for %s: %s", user.Active.Did, err) 730 } 731 732 collaboratingRepos, err := db.CollaboratingIn(s.db, user.Active.Did) 733 if err != nil { 734 log.Printf("getting collaborating repos for %s: %s", user.Active.Did, err) 735 } 736 737 allRepos := []pages.PinnedRepo{} 738 739 for _, r := range repos { 740 isPinned := slices.Contains(profile.PinnedRepos[:], r.RepoAt()) 741 allRepos = append(allRepos, pages.PinnedRepo{ 742 IsPinned: isPinned, 743 Repo: r, 744 }) 745 } 746 for _, r := range collaboratingRepos { 747 isPinned := slices.Contains(profile.PinnedRepos[:], r.RepoAt()) 748 allRepos = append(allRepos, pages.PinnedRepo{ 749 IsPinned: isPinned, 750 Repo: r, 751 }) 752 } 753 754 s.pages.EditPinsFragment(w, pages.EditPinsParams{ 755 LoggedInUser: user, 756 Profile: profile, 757 AllRepos: allRepos, 758 }) 759} 760 761func (s *State) UpdateProfilePunchcardSetting(w http.ResponseWriter, r *http.Request) { 762 err := r.ParseForm() 763 if err != nil { 764 log.Println("invalid profile update form", err) 765 return 766 } 767 user := s.oauth.GetUser(r) 768 769 profile, err := db.GetProfile(s.db, user.Did) 770 if err != nil { 771 log.Printf("getting profile data for %s: %s", user.Did, err) 772 } 773 774 if profile == nil { 775 return 776 } 777 778 punchcard := r.Form.Get("punchcard-setting") 779 780 err = db.SetProfilePunchcardStatus(s.db, profile.Did, models.ProfilePunchcardFromString(punchcard)) 781 if err != nil { 782 log.Println("failed to update profile", err) 783 return 784 } 785 786 s.pages.HxRefresh(w) 787}