forked from tangled.org/core
this repo has no description
1package state 2 3import ( 4 "context" 5 "crypto/hmac" 6 "crypto/sha256" 7 "encoding/hex" 8 "fmt" 9 "log" 10 "log/slog" 11 "net/http" 12 "strings" 13 "time" 14 15 comatproto "github.com/bluesky-social/indigo/api/atproto" 16 "github.com/bluesky-social/indigo/atproto/syntax" 17 lexutil "github.com/bluesky-social/indigo/lex/util" 18 securejoin "github.com/cyphar/filepath-securejoin" 19 "github.com/go-chi/chi/v5" 20 tangled "tangled.sh/tangled.sh/core/api/tangled" 21 "tangled.sh/tangled.sh/core/appview" 22 "tangled.sh/tangled.sh/core/appview/auth" 23 "tangled.sh/tangled.sh/core/appview/db" 24 "tangled.sh/tangled.sh/core/appview/pages" 25 "tangled.sh/tangled.sh/core/jetstream" 26 "tangled.sh/tangled.sh/core/rbac" 27) 28 29type State struct { 30 db *db.DB 31 auth *auth.Auth 32 enforcer *rbac.Enforcer 33 tidClock *syntax.TIDClock 34 pages *pages.Pages 35 resolver *appview.Resolver 36 jc *jetstream.JetstreamClient 37 config *appview.Config 38} 39 40func Make(config *appview.Config) (*State, error) { 41 d, err := db.Make(config.DbPath) 42 if err != nil { 43 return nil, err 44 } 45 46 auth, err := auth.Make(config.CookieSecret) 47 if err != nil { 48 return nil, err 49 } 50 51 enforcer, err := rbac.NewEnforcer(config.DbPath) 52 if err != nil { 53 return nil, err 54 } 55 56 clock := syntax.NewTIDClock(0) 57 58 pgs := pages.NewPages() 59 60 resolver := appview.NewResolver() 61 62 wrapper := db.DbWrapper{d} 63 jc, err := jetstream.NewJetstreamClient(config.JetstreamEndpoint, "appview", []string{tangled.GraphFollowNSID}, nil, slog.Default(), wrapper, false) 64 if err != nil { 65 return nil, fmt.Errorf("failed to create jetstream client: %w", err) 66 } 67 err = jc.StartJetstream(context.Background(), jetstreamIngester(wrapper)) 68 if err != nil { 69 return nil, fmt.Errorf("failed to start jetstream watcher: %w", err) 70 } 71 72 state := &State{ 73 d, 74 auth, 75 enforcer, 76 clock, 77 pgs, 78 resolver, 79 jc, 80 config, 81 } 82 83 return state, nil 84} 85 86func (s *State) TID() string { 87 return s.tidClock.Next().String() 88} 89 90func (s *State) Login(w http.ResponseWriter, r *http.Request) { 91 ctx := r.Context() 92 93 switch r.Method { 94 case http.MethodGet: 95 err := s.pages.Login(w, pages.LoginParams{}) 96 if err != nil { 97 log.Printf("rendering login page: %s", err) 98 } 99 100 return 101 case http.MethodPost: 102 handle := strings.TrimPrefix(r.FormValue("handle"), "@") 103 appPassword := r.FormValue("app_password") 104 105 resolved, err := s.resolver.ResolveIdent(ctx, handle) 106 if err != nil { 107 log.Println("failed to resolve handle:", err) 108 s.pages.Notice(w, "login-msg", fmt.Sprintf("\"%s\" is an invalid handle.", handle)) 109 return 110 } 111 112 atSession, err := s.auth.CreateInitialSession(ctx, resolved, appPassword) 113 if err != nil { 114 s.pages.Notice(w, "login-msg", "Invalid handle or password.") 115 return 116 } 117 sessionish := auth.CreateSessionWrapper{ServerCreateSession_Output: atSession} 118 119 err = s.auth.StoreSession(r, w, &sessionish, resolved.PDSEndpoint()) 120 if err != nil { 121 s.pages.Notice(w, "login-msg", "Failed to login, try again later.") 122 return 123 } 124 125 log.Printf("successfully saved session for %s (%s)", atSession.Handle, atSession.Did) 126 127 did := resolved.DID.String() 128 defaultKnot := "knot1.tangled.sh" 129 130 go func() { 131 log.Printf("adding %s to default knot", did) 132 err = s.enforcer.AddMember(defaultKnot, did) 133 if err != nil { 134 log.Println("failed to add user to knot1.tangled.sh: ", err) 135 return 136 } 137 err = s.enforcer.E.SavePolicy() 138 if err != nil { 139 log.Println("failed to add user to knot1.tangled.sh: ", err) 140 return 141 } 142 143 secret, err := db.GetRegistrationKey(s.db, defaultKnot) 144 if err != nil { 145 log.Println("failed to get registration key for knot1.tangled.sh") 146 return 147 } 148 signedClient, err := NewSignedClient(defaultKnot, secret, s.config.Dev) 149 resp, err := signedClient.AddMember(did) 150 if err != nil { 151 log.Println("failed to add user to knot1.tangled.sh: ", err) 152 return 153 } 154 155 if resp.StatusCode != http.StatusNoContent { 156 log.Println("failed to add user to knot1.tangled.sh: ", resp.StatusCode) 157 return 158 } 159 }() 160 161 s.pages.HxRedirect(w, "/") 162 return 163 } 164} 165 166func (s *State) Logout(w http.ResponseWriter, r *http.Request) { 167 s.auth.ClearSession(r, w) 168 w.Header().Set("HX-Redirect", "/login") 169 w.WriteHeader(http.StatusSeeOther) 170} 171 172func (s *State) Timeline(w http.ResponseWriter, r *http.Request) { 173 user := s.auth.GetUser(r) 174 175 timeline, err := db.MakeTimeline(s.db) 176 if err != nil { 177 log.Println(err) 178 s.pages.Notice(w, "timeline", "Uh oh! Failed to load timeline.") 179 } 180 181 var didsToResolve []string 182 for _, ev := range timeline { 183 if ev.Repo != nil { 184 didsToResolve = append(didsToResolve, ev.Repo.Did) 185 } 186 if ev.Follow != nil { 187 didsToResolve = append(didsToResolve, ev.Follow.UserDid, ev.Follow.SubjectDid) 188 } 189 if ev.Star != nil { 190 didsToResolve = append(didsToResolve, ev.Star.StarredByDid, ev.Star.Repo.Did) 191 } 192 } 193 194 resolvedIds := s.resolver.ResolveIdents(r.Context(), didsToResolve) 195 didHandleMap := make(map[string]string) 196 for _, identity := range resolvedIds { 197 if !identity.Handle.IsInvalidHandle() { 198 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 199 } else { 200 didHandleMap[identity.DID.String()] = identity.DID.String() 201 } 202 } 203 204 s.pages.Timeline(w, pages.TimelineParams{ 205 LoggedInUser: user, 206 Timeline: timeline, 207 DidHandleMap: didHandleMap, 208 }) 209 210 return 211} 212 213// requires auth 214func (s *State) RegistrationKey(w http.ResponseWriter, r *http.Request) { 215 switch r.Method { 216 case http.MethodGet: 217 // list open registrations under this did 218 219 return 220 case http.MethodPost: 221 session, err := s.auth.Store.Get(r, appview.SessionName) 222 if err != nil || session.IsNew { 223 log.Println("unauthorized attempt to generate registration key") 224 http.Error(w, "Forbidden", http.StatusUnauthorized) 225 return 226 } 227 228 did := session.Values[appview.SessionDid].(string) 229 230 // check if domain is valid url, and strip extra bits down to just host 231 domain := r.FormValue("domain") 232 if domain == "" { 233 http.Error(w, "Invalid form", http.StatusBadRequest) 234 return 235 } 236 237 key, err := db.GenerateRegistrationKey(s.db, domain, did) 238 239 if err != nil { 240 log.Println(err) 241 http.Error(w, "unable to register this domain", http.StatusNotAcceptable) 242 return 243 } 244 245 w.Write([]byte(key)) 246 } 247} 248 249func (s *State) Keys(w http.ResponseWriter, r *http.Request) { 250 user := chi.URLParam(r, "user") 251 user = strings.TrimPrefix(user, "@") 252 253 if user == "" { 254 w.WriteHeader(http.StatusBadRequest) 255 return 256 } 257 258 id, err := s.resolver.ResolveIdent(r.Context(), user) 259 if err != nil { 260 w.WriteHeader(http.StatusInternalServerError) 261 return 262 } 263 264 pubKeys, err := db.GetPublicKeys(s.db, id.DID.String()) 265 if err != nil { 266 w.WriteHeader(http.StatusNotFound) 267 return 268 } 269 270 if len(pubKeys) == 0 { 271 w.WriteHeader(http.StatusNotFound) 272 return 273 } 274 275 for _, k := range pubKeys { 276 key := strings.TrimRight(k.Key, "\n") 277 w.Write([]byte(fmt.Sprintln(key))) 278 } 279} 280 281// create a signed request and check if a node responds to that 282func (s *State) InitKnotServer(w http.ResponseWriter, r *http.Request) { 283 user := s.auth.GetUser(r) 284 285 domain := chi.URLParam(r, "domain") 286 if domain == "" { 287 http.Error(w, "malformed url", http.StatusBadRequest) 288 return 289 } 290 log.Println("checking ", domain) 291 292 secret, err := db.GetRegistrationKey(s.db, domain) 293 if err != nil { 294 log.Printf("no key found for domain %s: %s\n", domain, err) 295 return 296 } 297 298 client, err := NewSignedClient(domain, secret, s.config.Dev) 299 if err != nil { 300 log.Println("failed to create client to ", domain) 301 } 302 303 resp, err := client.Init(user.Did) 304 if err != nil { 305 w.Write([]byte("no dice")) 306 log.Println("domain was unreachable after 5 seconds") 307 return 308 } 309 310 if resp.StatusCode == http.StatusConflict { 311 log.Println("status conflict", resp.StatusCode) 312 w.Write([]byte("already registered, sorry!")) 313 return 314 } 315 316 if resp.StatusCode != http.StatusNoContent { 317 log.Println("status nok", resp.StatusCode) 318 w.Write([]byte("no dice")) 319 return 320 } 321 322 // verify response mac 323 signature := resp.Header.Get("X-Signature") 324 signatureBytes, err := hex.DecodeString(signature) 325 if err != nil { 326 return 327 } 328 329 expectedMac := hmac.New(sha256.New, []byte(secret)) 330 expectedMac.Write([]byte("ok")) 331 332 if !hmac.Equal(expectedMac.Sum(nil), signatureBytes) { 333 log.Printf("response body signature mismatch: %x\n", signatureBytes) 334 return 335 } 336 337 tx, err := s.db.BeginTx(r.Context(), nil) 338 if err != nil { 339 log.Println("failed to start tx", err) 340 http.Error(w, err.Error(), http.StatusInternalServerError) 341 return 342 } 343 defer func() { 344 tx.Rollback() 345 err = s.enforcer.E.LoadPolicy() 346 if err != nil { 347 log.Println("failed to rollback policies") 348 } 349 }() 350 351 // mark as registered 352 err = db.Register(tx, domain) 353 if err != nil { 354 log.Println("failed to register domain", err) 355 http.Error(w, err.Error(), http.StatusInternalServerError) 356 return 357 } 358 359 // set permissions for this did as owner 360 reg, err := db.RegistrationByDomain(tx, domain) 361 if err != nil { 362 log.Println("failed to register domain", err) 363 http.Error(w, err.Error(), http.StatusInternalServerError) 364 return 365 } 366 367 // add basic acls for this domain 368 err = s.enforcer.AddDomain(domain) 369 if err != nil { 370 log.Println("failed to setup owner of domain", err) 371 http.Error(w, err.Error(), http.StatusInternalServerError) 372 return 373 } 374 375 // add this did as owner of this domain 376 err = s.enforcer.AddOwner(domain, reg.ByDid) 377 if err != nil { 378 log.Println("failed to setup owner of domain", err) 379 http.Error(w, err.Error(), http.StatusInternalServerError) 380 return 381 } 382 383 err = tx.Commit() 384 if err != nil { 385 log.Println("failed to commit changes", err) 386 http.Error(w, err.Error(), http.StatusInternalServerError) 387 return 388 } 389 390 err = s.enforcer.E.SavePolicy() 391 if err != nil { 392 log.Println("failed to update ACLs", err) 393 http.Error(w, err.Error(), http.StatusInternalServerError) 394 return 395 } 396 397 w.Write([]byte("check success")) 398} 399 400func (s *State) KnotServerInfo(w http.ResponseWriter, r *http.Request) { 401 domain := chi.URLParam(r, "domain") 402 if domain == "" { 403 http.Error(w, "malformed url", http.StatusBadRequest) 404 return 405 } 406 407 user := s.auth.GetUser(r) 408 reg, err := db.RegistrationByDomain(s.db, domain) 409 if err != nil { 410 w.Write([]byte("failed to pull up registration info")) 411 return 412 } 413 414 var members []string 415 if reg.Registered != nil { 416 members, err = s.enforcer.GetUserByRole("server:member", domain) 417 if err != nil { 418 w.Write([]byte("failed to fetch member list")) 419 return 420 } 421 } 422 423 var didsToResolve []string 424 for _, m := range members { 425 didsToResolve = append(didsToResolve, m) 426 } 427 didsToResolve = append(didsToResolve, reg.ByDid) 428 resolvedIds := s.resolver.ResolveIdents(r.Context(), didsToResolve) 429 didHandleMap := make(map[string]string) 430 for _, identity := range resolvedIds { 431 if !identity.Handle.IsInvalidHandle() { 432 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 433 } else { 434 didHandleMap[identity.DID.String()] = identity.DID.String() 435 } 436 } 437 438 ok, err := s.enforcer.IsServerOwner(user.Did, domain) 439 isOwner := err == nil && ok 440 441 p := pages.KnotParams{ 442 LoggedInUser: user, 443 DidHandleMap: didHandleMap, 444 Registration: reg, 445 Members: members, 446 IsOwner: isOwner, 447 } 448 449 s.pages.Knot(w, p) 450} 451 452// get knots registered by this user 453func (s *State) Knots(w http.ResponseWriter, r *http.Request) { 454 // for now, this is just pubkeys 455 user := s.auth.GetUser(r) 456 registrations, err := db.RegistrationsByDid(s.db, user.Did) 457 if err != nil { 458 log.Println(err) 459 } 460 461 s.pages.Knots(w, pages.KnotsParams{ 462 LoggedInUser: user, 463 Registrations: registrations, 464 }) 465} 466 467// list members of domain, requires auth and requires owner status 468func (s *State) ListMembers(w http.ResponseWriter, r *http.Request) { 469 domain := chi.URLParam(r, "domain") 470 if domain == "" { 471 http.Error(w, "malformed url", http.StatusBadRequest) 472 return 473 } 474 475 // list all members for this domain 476 memberDids, err := s.enforcer.GetUserByRole("server:member", domain) 477 if err != nil { 478 w.Write([]byte("failed to fetch member list")) 479 return 480 } 481 482 w.Write([]byte(strings.Join(memberDids, "\n"))) 483 return 484} 485 486// add member to domain, requires auth and requires invite access 487func (s *State) AddMember(w http.ResponseWriter, r *http.Request) { 488 domain := chi.URLParam(r, "domain") 489 if domain == "" { 490 http.Error(w, "malformed url", http.StatusBadRequest) 491 return 492 } 493 494 memberDid := r.FormValue("member") 495 if memberDid == "" { 496 http.Error(w, "malformed form", http.StatusBadRequest) 497 return 498 } 499 500 memberIdent, err := s.resolver.ResolveIdent(r.Context(), memberDid) 501 if err != nil { 502 w.Write([]byte("failed to resolve member did to a handle")) 503 return 504 } 505 log.Printf("adding %s to %s\n", memberIdent.Handle.String(), domain) 506 507 // announce this relation into the firehose, store into owners' pds 508 client, _ := s.auth.AuthorizedClient(r) 509 currentUser := s.auth.GetUser(r) 510 addedAt := time.Now().Format(time.RFC3339) 511 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 512 Collection: tangled.KnotMemberNSID, 513 Repo: currentUser.Did, 514 Rkey: s.TID(), 515 Record: &lexutil.LexiconTypeDecoder{ 516 Val: &tangled.KnotMember{ 517 Member: memberIdent.DID.String(), 518 Domain: domain, 519 AddedAt: &addedAt, 520 }}, 521 }) 522 523 // invalid record 524 if err != nil { 525 log.Printf("failed to create record: %s", err) 526 return 527 } 528 log.Println("created atproto record: ", resp.Uri) 529 530 secret, err := db.GetRegistrationKey(s.db, domain) 531 if err != nil { 532 log.Printf("no key found for domain %s: %s\n", domain, err) 533 return 534 } 535 536 ksClient, err := NewSignedClient(domain, secret, s.config.Dev) 537 if err != nil { 538 log.Println("failed to create client to ", domain) 539 return 540 } 541 542 ksResp, err := ksClient.AddMember(memberIdent.DID.String()) 543 if err != nil { 544 log.Printf("failed to make request to %s: %s", domain, err) 545 return 546 } 547 548 if ksResp.StatusCode != http.StatusNoContent { 549 w.Write([]byte(fmt.Sprint("knotserver failed to add member: ", err))) 550 return 551 } 552 553 err = s.enforcer.AddMember(domain, memberIdent.DID.String()) 554 if err != nil { 555 w.Write([]byte(fmt.Sprint("failed to add member: ", err))) 556 return 557 } 558 559 w.Write([]byte(fmt.Sprint("added member: ", memberIdent.Handle.String()))) 560} 561 562func (s *State) RemoveMember(w http.ResponseWriter, r *http.Request) { 563} 564 565func (s *State) NewRepo(w http.ResponseWriter, r *http.Request) { 566 switch r.Method { 567 case http.MethodGet: 568 user := s.auth.GetUser(r) 569 knots, err := s.enforcer.GetDomainsForUser(user.Did) 570 if err != nil { 571 s.pages.Notice(w, "repo", "Invalid user account.") 572 return 573 } 574 575 s.pages.NewRepo(w, pages.NewRepoParams{ 576 LoggedInUser: user, 577 Knots: knots, 578 }) 579 580 case http.MethodPost: 581 user := s.auth.GetUser(r) 582 583 domain := r.FormValue("domain") 584 if domain == "" { 585 s.pages.Notice(w, "repo", "Invalid form submission&mdash;missing knot domain.") 586 return 587 } 588 589 repoName := r.FormValue("name") 590 if repoName == "" { 591 s.pages.Notice(w, "repo", "Repository name cannot be empty.") 592 return 593 } 594 595 // Check for valid repository name (GitHub-like rules) 596 // No spaces, only alphanumeric characters, dashes, and underscores 597 for _, char := range repoName { 598 if !((char >= 'a' && char <= 'z') || 599 (char >= 'A' && char <= 'Z') || 600 (char >= '0' && char <= '9') || 601 char == '-' || char == '_' || char == '.') { 602 s.pages.Notice(w, "repo", "Repository name can only contain alphanumeric characters, periods, hyphens, and underscores.") 603 return 604 } 605 } 606 607 defaultBranch := r.FormValue("branch") 608 if defaultBranch == "" { 609 defaultBranch = "main" 610 } 611 612 description := r.FormValue("description") 613 614 ok, err := s.enforcer.E.Enforce(user.Did, domain, domain, "repo:create") 615 if err != nil || !ok { 616 s.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.") 617 return 618 } 619 620 existingRepo, err := db.GetRepo(s.db, user.Did, repoName) 621 if err == nil && existingRepo != nil { 622 s.pages.Notice(w, "repo", fmt.Sprintf("A repo by this name already exists on %s", existingRepo.Knot)) 623 return 624 } 625 626 secret, err := db.GetRegistrationKey(s.db, domain) 627 if err != nil { 628 s.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", domain)) 629 return 630 } 631 632 client, err := NewSignedClient(domain, secret, s.config.Dev) 633 if err != nil { 634 s.pages.Notice(w, "repo", "Failed to connect to knot server.") 635 return 636 } 637 638 rkey := s.TID() 639 repo := &db.Repo{ 640 Did: user.Did, 641 Name: repoName, 642 Knot: domain, 643 Rkey: rkey, 644 Description: description, 645 } 646 647 xrpcClient, _ := s.auth.AuthorizedClient(r) 648 649 addedAt := time.Now().Format(time.RFC3339) 650 atresp, err := comatproto.RepoPutRecord(r.Context(), xrpcClient, &comatproto.RepoPutRecord_Input{ 651 Collection: tangled.RepoNSID, 652 Repo: user.Did, 653 Rkey: rkey, 654 Record: &lexutil.LexiconTypeDecoder{ 655 Val: &tangled.Repo{ 656 Knot: repo.Knot, 657 Name: repoName, 658 AddedAt: &addedAt, 659 Owner: user.Did, 660 }}, 661 }) 662 if err != nil { 663 log.Printf("failed to create record: %s", err) 664 s.pages.Notice(w, "repo", "Failed to announce repository creation.") 665 return 666 } 667 log.Println("created repo record: ", atresp.Uri) 668 669 tx, err := s.db.BeginTx(r.Context(), nil) 670 if err != nil { 671 log.Println(err) 672 s.pages.Notice(w, "repo", "Failed to save repository information.") 673 return 674 } 675 defer func() { 676 tx.Rollback() 677 err = s.enforcer.E.LoadPolicy() 678 if err != nil { 679 log.Println("failed to rollback policies") 680 } 681 }() 682 683 resp, err := client.NewRepo(user.Did, repoName, defaultBranch) 684 if err != nil { 685 s.pages.Notice(w, "repo", "Failed to create repository on knot server.") 686 return 687 } 688 689 switch resp.StatusCode { 690 case http.StatusConflict: 691 s.pages.Notice(w, "repo", "A repository with that name already exists.") 692 return 693 case http.StatusInternalServerError: 694 s.pages.Notice(w, "repo", "Failed to create repository on knot. Try again later.") 695 case http.StatusNoContent: 696 // continue 697 } 698 699 repo.AtUri = atresp.Uri 700 err = db.AddRepo(tx, repo) 701 if err != nil { 702 log.Println(err) 703 s.pages.Notice(w, "repo", "Failed to save repository information.") 704 return 705 } 706 707 // acls 708 p, _ := securejoin.SecureJoin(user.Did, repoName) 709 err = s.enforcer.AddRepo(user.Did, domain, p) 710 if err != nil { 711 log.Println(err) 712 s.pages.Notice(w, "repo", "Failed to set up repository permissions.") 713 return 714 } 715 716 err = tx.Commit() 717 if err != nil { 718 log.Println("failed to commit changes", err) 719 http.Error(w, err.Error(), http.StatusInternalServerError) 720 return 721 } 722 723 err = s.enforcer.E.SavePolicy() 724 if err != nil { 725 log.Println("failed to update ACLs", err) 726 http.Error(w, err.Error(), http.StatusInternalServerError) 727 return 728 } 729 730 s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s", user.Handle, repoName)) 731 return 732 } 733} 734 735func (s *State) ProfilePage(w http.ResponseWriter, r *http.Request) { 736 didOrHandle := chi.URLParam(r, "user") 737 if didOrHandle == "" { 738 http.Error(w, "Bad request", http.StatusBadRequest) 739 return 740 } 741 742 ident, err := s.resolver.ResolveIdent(r.Context(), didOrHandle) 743 if err != nil { 744 log.Printf("resolving identity: %s", err) 745 w.WriteHeader(http.StatusNotFound) 746 return 747 } 748 749 repos, err := db.GetAllReposByDid(s.db, ident.DID.String()) 750 if err != nil { 751 log.Printf("getting repos for %s: %s", ident.DID.String(), err) 752 } 753 754 collaboratingRepos, err := db.CollaboratingIn(s.db, ident.DID.String()) 755 if err != nil { 756 log.Printf("getting collaborating repos for %s: %s", ident.DID.String(), err) 757 } 758 var didsToResolve []string 759 for _, r := range collaboratingRepos { 760 didsToResolve = append(didsToResolve, r.Did) 761 } 762 resolvedIds := s.resolver.ResolveIdents(r.Context(), didsToResolve) 763 didHandleMap := make(map[string]string) 764 for _, identity := range resolvedIds { 765 if !identity.Handle.IsInvalidHandle() { 766 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 767 } else { 768 didHandleMap[identity.DID.String()] = identity.DID.String() 769 } 770 } 771 772 followers, following, err := db.GetFollowerFollowing(s.db, ident.DID.String()) 773 if err != nil { 774 log.Printf("getting follow stats repos for %s: %s", ident.DID.String(), err) 775 } 776 777 loggedInUser := s.auth.GetUser(r) 778 followStatus := db.IsNotFollowing 779 if loggedInUser != nil { 780 followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, ident.DID.String()) 781 } 782 783 profileAvatarUri, err := GetAvatarUri(ident.Handle.String()) 784 if err != nil { 785 log.Println("failed to fetch bsky avatar", err) 786 } 787 788 s.pages.ProfilePage(w, pages.ProfilePageParams{ 789 LoggedInUser: loggedInUser, 790 UserDid: ident.DID.String(), 791 UserHandle: ident.Handle.String(), 792 Repos: repos, 793 CollaboratingRepos: collaboratingRepos, 794 ProfileStats: pages.ProfileStats{ 795 Followers: followers, 796 Following: following, 797 }, 798 FollowStatus: db.FollowStatus(followStatus), 799 DidHandleMap: didHandleMap, 800 AvatarUri: profileAvatarUri, 801 }) 802} 803 804func GetAvatarUri(handle string) (string, error) { 805 return fmt.Sprintf("https://avatars.dog/%s@webp", handle), nil 806}