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 "encoding/json" 9 "fmt" 10 "log" 11 "log/slog" 12 "net/http" 13 "strings" 14 "time" 15 16 comatproto "github.com/bluesky-social/indigo/api/atproto" 17 "github.com/bluesky-social/indigo/atproto/syntax" 18 lexutil "github.com/bluesky-social/indigo/lex/util" 19 securejoin "github.com/cyphar/filepath-securejoin" 20 "github.com/go-chi/chi/v5" 21 tangled "github.com/sotangled/tangled/api/tangled" 22 "github.com/sotangled/tangled/appview" 23 "github.com/sotangled/tangled/appview/auth" 24 "github.com/sotangled/tangled/appview/db" 25 "github.com/sotangled/tangled/appview/pages" 26 "github.com/sotangled/tangled/jetstream" 27 "github.com/sotangled/tangled/rbac" 28) 29 30type State struct { 31 db *db.DB 32 auth *auth.Auth 33 enforcer *rbac.Enforcer 34 tidClock *syntax.TIDClock 35 pages *pages.Pages 36 resolver *appview.Resolver 37 jc *jetstream.JetstreamClient 38 config *appview.Config 39} 40 41func Make(config *appview.Config) (*State, error) { 42 d, err := db.Make(config.DbPath) 43 if err != nil { 44 return nil, err 45 } 46 47 auth, err := auth.Make(config.CookieSecret) 48 if err != nil { 49 return nil, err 50 } 51 52 enforcer, err := rbac.NewEnforcer(config.DbPath) 53 if err != nil { 54 return nil, err 55 } 56 57 clock := syntax.NewTIDClock(0) 58 59 pgs := pages.NewPages() 60 61 resolver := appview.NewResolver() 62 63 wrapper := db.DbWrapper{d} 64 jc, err := jetstream.NewJetstreamClient("appview", []string{tangled.GraphFollowNSID}, nil, slog.Default(), wrapper, false) 65 if err != nil { 66 return nil, fmt.Errorf("failed to create jetstream client: %w", err) 67 } 68 err = jc.StartJetstream(context.Background(), jetstreamIngester(wrapper)) 69 if err != nil { 70 return nil, fmt.Errorf("failed to start jetstream watcher: %w", err) 71 } 72 73 state := &State{ 74 d, 75 auth, 76 enforcer, 77 clock, 78 pgs, 79 resolver, 80 jc, 81 config, 82 } 83 84 return state, nil 85} 86 87func (s *State) TID() string { 88 return s.tidClock.Next().String() 89} 90 91func (s *State) Login(w http.ResponseWriter, r *http.Request) { 92 ctx := r.Context() 93 94 switch r.Method { 95 case http.MethodGet: 96 err := s.pages.Login(w, pages.LoginParams{}) 97 if err != nil { 98 log.Printf("rendering login page: %s", err) 99 } 100 101 return 102 case http.MethodPost: 103 handle := strings.TrimPrefix(r.FormValue("handle"), "@") 104 appPassword := r.FormValue("app_password") 105 106 resolved, err := s.resolver.ResolveIdent(ctx, handle) 107 if err != nil { 108 log.Println("failed to resolve handle:", err) 109 s.pages.Notice(w, "login-msg", fmt.Sprintf("\"%s\" is an invalid handle.", handle)) 110 return 111 } 112 113 atSession, err := s.auth.CreateInitialSession(ctx, resolved, appPassword) 114 if err != nil { 115 s.pages.Notice(w, "login-msg", "Invalid handle or password.") 116 return 117 } 118 sessionish := auth.CreateSessionWrapper{ServerCreateSession_Output: atSession} 119 120 err = s.auth.StoreSession(r, w, &sessionish, resolved.PDSEndpoint()) 121 if err != nil { 122 s.pages.Notice(w, "login-msg", "Failed to login, try again later.") 123 return 124 } 125 126 log.Printf("successfully saved session for %s (%s)", atSession.Handle, atSession.Did) 127 128 did := resolved.DID.String() 129 defaultKnot := "knot1.tangled.sh" 130 131 go func() { 132 log.Printf("adding %s to default knot", did) 133 err = s.enforcer.AddMember(defaultKnot, did) 134 if err != nil { 135 log.Println("failed to add user to knot1.tangled.sh: ", err) 136 return 137 } 138 err = s.enforcer.E.SavePolicy() 139 if err != nil { 140 log.Println("failed to add user to knot1.tangled.sh: ", err) 141 return 142 } 143 144 secret, err := db.GetRegistrationKey(s.db, defaultKnot) 145 if err != nil { 146 log.Println("failed to get registration key for knot1.tangled.sh") 147 return 148 } 149 signedClient, err := NewSignedClient(defaultKnot, secret, s.config.Dev) 150 resp, err := signedClient.AddMember(did) 151 if err != nil { 152 log.Println("failed to add user to knot1.tangled.sh: ", err) 153 return 154 } 155 156 if resp.StatusCode != http.StatusNoContent { 157 log.Println("failed to add user to knot1.tangled.sh: ", resp.StatusCode) 158 return 159 } 160 }() 161 162 s.pages.HxRedirect(w, "/") 163 return 164 } 165} 166 167func (s *State) Logout(w http.ResponseWriter, r *http.Request) { 168 s.auth.ClearSession(r, w) 169 http.Redirect(w, r, "/login", http.StatusTemporaryRedirect) 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) 188 didsToResolve = append(didsToResolve, ev.Follow.SubjectDid) 189 } 190 } 191 192 resolvedIds := s.resolver.ResolveIdents(r.Context(), didsToResolve) 193 didHandleMap := make(map[string]string) 194 for _, identity := range resolvedIds { 195 if !identity.Handle.IsInvalidHandle() { 196 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 197 } else { 198 didHandleMap[identity.DID.String()] = identity.DID.String() 199 } 200 } 201 202 s.pages.Timeline(w, pages.TimelineParams{ 203 LoggedInUser: user, 204 Timeline: timeline, 205 DidHandleMap: didHandleMap, 206 }) 207 208 return 209} 210 211// requires auth 212func (s *State) RegistrationKey(w http.ResponseWriter, r *http.Request) { 213 switch r.Method { 214 case http.MethodGet: 215 // list open registrations under this did 216 217 return 218 case http.MethodPost: 219 session, err := s.auth.Store.Get(r, appview.SessionName) 220 if err != nil || session.IsNew { 221 log.Println("unauthorized attempt to generate registration key") 222 http.Error(w, "Forbidden", http.StatusUnauthorized) 223 return 224 } 225 226 did := session.Values[appview.SessionDid].(string) 227 228 // check if domain is valid url, and strip extra bits down to just host 229 domain := r.FormValue("domain") 230 if domain == "" { 231 http.Error(w, "Invalid form", http.StatusBadRequest) 232 return 233 } 234 235 key, err := db.GenerateRegistrationKey(s.db, domain, did) 236 237 if err != nil { 238 log.Println(err) 239 http.Error(w, "unable to register this domain", http.StatusNotAcceptable) 240 return 241 } 242 243 w.Write([]byte(key)) 244 } 245} 246 247func (s *State) Keys(w http.ResponseWriter, r *http.Request) { 248 user := chi.URLParam(r, "user") 249 user = strings.TrimPrefix(user, "@") 250 251 if user == "" { 252 w.WriteHeader(http.StatusBadRequest) 253 return 254 } 255 256 id, err := s.resolver.ResolveIdent(r.Context(), user) 257 if err != nil { 258 w.WriteHeader(http.StatusInternalServerError) 259 return 260 } 261 262 pubKeys, err := db.GetPublicKeys(s.db, id.DID.String()) 263 if err != nil { 264 w.WriteHeader(http.StatusNotFound) 265 return 266 } 267 268 if len(pubKeys) == 0 { 269 w.WriteHeader(http.StatusNotFound) 270 return 271 } 272 273 for _, k := range pubKeys { 274 key := strings.TrimRight(k.Key, "\n") 275 w.Write([]byte(fmt.Sprintln(key))) 276 } 277} 278 279// create a signed request and check if a node responds to that 280func (s *State) InitKnotServer(w http.ResponseWriter, r *http.Request) { 281 user := s.auth.GetUser(r) 282 283 domain := chi.URLParam(r, "domain") 284 if domain == "" { 285 http.Error(w, "malformed url", http.StatusBadRequest) 286 return 287 } 288 log.Println("checking ", domain) 289 290 secret, err := db.GetRegistrationKey(s.db, domain) 291 if err != nil { 292 log.Printf("no key found for domain %s: %s\n", domain, err) 293 return 294 } 295 296 client, err := NewSignedClient(domain, secret, s.config.Dev) 297 if err != nil { 298 log.Println("failed to create client to ", domain) 299 } 300 301 resp, err := client.Init(user.Did) 302 if err != nil { 303 w.Write([]byte("no dice")) 304 log.Println("domain was unreachable after 5 seconds") 305 return 306 } 307 308 if resp.StatusCode == http.StatusConflict { 309 log.Println("status conflict", resp.StatusCode) 310 w.Write([]byte("already registered, sorry!")) 311 return 312 } 313 314 if resp.StatusCode != http.StatusNoContent { 315 log.Println("status nok", resp.StatusCode) 316 w.Write([]byte("no dice")) 317 return 318 } 319 320 // verify response mac 321 signature := resp.Header.Get("X-Signature") 322 signatureBytes, err := hex.DecodeString(signature) 323 if err != nil { 324 return 325 } 326 327 expectedMac := hmac.New(sha256.New, []byte(secret)) 328 expectedMac.Write([]byte("ok")) 329 330 if !hmac.Equal(expectedMac.Sum(nil), signatureBytes) { 331 log.Printf("response body signature mismatch: %x\n", signatureBytes) 332 return 333 } 334 335 tx, err := s.db.BeginTx(r.Context(), nil) 336 if err != nil { 337 log.Println("failed to start tx", err) 338 http.Error(w, err.Error(), http.StatusInternalServerError) 339 return 340 } 341 defer func() { 342 tx.Rollback() 343 err = s.enforcer.E.LoadPolicy() 344 if err != nil { 345 log.Println("failed to rollback policies") 346 } 347 }() 348 349 // mark as registered 350 err = db.Register(tx, domain) 351 if err != nil { 352 log.Println("failed to register domain", err) 353 http.Error(w, err.Error(), http.StatusInternalServerError) 354 return 355 } 356 357 // set permissions for this did as owner 358 reg, err := db.RegistrationByDomain(tx, domain) 359 if err != nil { 360 log.Println("failed to register domain", err) 361 http.Error(w, err.Error(), http.StatusInternalServerError) 362 return 363 } 364 365 // add basic acls for this domain 366 err = s.enforcer.AddDomain(domain) 367 if err != nil { 368 log.Println("failed to setup owner of domain", err) 369 http.Error(w, err.Error(), http.StatusInternalServerError) 370 return 371 } 372 373 // add this did as owner of this domain 374 err = s.enforcer.AddOwner(domain, reg.ByDid) 375 if err != nil { 376 log.Println("failed to setup owner of domain", err) 377 http.Error(w, err.Error(), http.StatusInternalServerError) 378 return 379 } 380 381 err = tx.Commit() 382 if err != nil { 383 log.Println("failed to commit changes", err) 384 http.Error(w, err.Error(), http.StatusInternalServerError) 385 return 386 } 387 388 err = s.enforcer.E.SavePolicy() 389 if err != nil { 390 log.Println("failed to update ACLs", err) 391 http.Error(w, err.Error(), http.StatusInternalServerError) 392 return 393 } 394 395 w.Write([]byte("check success")) 396} 397 398func (s *State) KnotServerInfo(w http.ResponseWriter, r *http.Request) { 399 domain := chi.URLParam(r, "domain") 400 if domain == "" { 401 http.Error(w, "malformed url", http.StatusBadRequest) 402 return 403 } 404 405 user := s.auth.GetUser(r) 406 reg, err := db.RegistrationByDomain(s.db, domain) 407 if err != nil { 408 w.Write([]byte("failed to pull up registration info")) 409 return 410 } 411 412 var members []string 413 if reg.Registered != nil { 414 members, err = s.enforcer.GetUserByRole("server:member", domain) 415 if err != nil { 416 w.Write([]byte("failed to fetch member list")) 417 return 418 } 419 } 420 421 ok, err := s.enforcer.IsServerOwner(user.Did, domain) 422 isOwner := err == nil && ok 423 424 p := pages.KnotParams{ 425 LoggedInUser: user, 426 Registration: reg, 427 Members: members, 428 IsOwner: isOwner, 429 } 430 431 s.pages.Knot(w, p) 432} 433 434// get knots registered by this user 435func (s *State) Knots(w http.ResponseWriter, r *http.Request) { 436 // for now, this is just pubkeys 437 user := s.auth.GetUser(r) 438 registrations, err := db.RegistrationsByDid(s.db, user.Did) 439 if err != nil { 440 log.Println(err) 441 } 442 443 s.pages.Knots(w, pages.KnotsParams{ 444 LoggedInUser: user, 445 Registrations: registrations, 446 }) 447} 448 449// list members of domain, requires auth and requires owner status 450func (s *State) ListMembers(w http.ResponseWriter, r *http.Request) { 451 domain := chi.URLParam(r, "domain") 452 if domain == "" { 453 http.Error(w, "malformed url", http.StatusBadRequest) 454 return 455 } 456 457 // list all members for this domain 458 memberDids, err := s.enforcer.GetUserByRole("server:member", domain) 459 if err != nil { 460 w.Write([]byte("failed to fetch member list")) 461 return 462 } 463 464 w.Write([]byte(strings.Join(memberDids, "\n"))) 465 return 466} 467 468// add member to domain, requires auth and requires invite access 469func (s *State) AddMember(w http.ResponseWriter, r *http.Request) { 470 domain := chi.URLParam(r, "domain") 471 if domain == "" { 472 http.Error(w, "malformed url", http.StatusBadRequest) 473 return 474 } 475 476 memberDid := r.FormValue("member") 477 if memberDid == "" { 478 http.Error(w, "malformed form", http.StatusBadRequest) 479 return 480 } 481 482 memberIdent, err := s.resolver.ResolveIdent(r.Context(), memberDid) 483 if err != nil { 484 w.Write([]byte("failed to resolve member did to a handle")) 485 return 486 } 487 log.Printf("adding %s to %s\n", memberIdent.Handle.String(), domain) 488 489 // announce this relation into the firehose, store into owners' pds 490 client, _ := s.auth.AuthorizedClient(r) 491 currentUser := s.auth.GetUser(r) 492 addedAt := time.Now().Format(time.RFC3339) 493 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 494 Collection: tangled.KnotMemberNSID, 495 Repo: currentUser.Did, 496 Rkey: s.TID(), 497 Record: &lexutil.LexiconTypeDecoder{ 498 Val: &tangled.KnotMember{ 499 Member: memberIdent.DID.String(), 500 Domain: domain, 501 AddedAt: &addedAt, 502 }}, 503 }) 504 505 // invalid record 506 if err != nil { 507 log.Printf("failed to create record: %s", err) 508 return 509 } 510 log.Println("created atproto record: ", resp.Uri) 511 512 secret, err := db.GetRegistrationKey(s.db, domain) 513 if err != nil { 514 log.Printf("no key found for domain %s: %s\n", domain, err) 515 return 516 } 517 518 ksClient, err := NewSignedClient(domain, secret, s.config.Dev) 519 if err != nil { 520 log.Println("failed to create client to ", domain) 521 return 522 } 523 524 ksResp, err := ksClient.AddMember(memberIdent.DID.String()) 525 if err != nil { 526 log.Printf("failed to make request to %s: %s", domain, err) 527 return 528 } 529 530 if ksResp.StatusCode != http.StatusNoContent { 531 w.Write([]byte(fmt.Sprint("knotserver failed to add member: ", err))) 532 return 533 } 534 535 err = s.enforcer.AddMember(domain, memberIdent.DID.String()) 536 if err != nil { 537 w.Write([]byte(fmt.Sprint("failed to add member: ", err))) 538 return 539 } 540 541 w.Write([]byte(fmt.Sprint("added member: ", memberIdent.Handle.String()))) 542} 543 544func (s *State) RemoveMember(w http.ResponseWriter, r *http.Request) { 545} 546 547func (s *State) NewRepo(w http.ResponseWriter, r *http.Request) { 548 switch r.Method { 549 case http.MethodGet: 550 user := s.auth.GetUser(r) 551 knots, err := s.enforcer.GetDomainsForUser(user.Did) 552 if err != nil { 553 s.pages.Notice(w, "repo", "Invalid user account.") 554 return 555 } 556 557 s.pages.NewRepo(w, pages.NewRepoParams{ 558 LoggedInUser: user, 559 Knots: knots, 560 }) 561 562 case http.MethodPost: 563 user := s.auth.GetUser(r) 564 565 domain := r.FormValue("domain") 566 if domain == "" { 567 s.pages.Notice(w, "repo", "Invalid form submission—missing knot domain.") 568 return 569 } 570 571 repoName := r.FormValue("name") 572 if repoName == "" { 573 s.pages.Notice(w, "repo", "Invalid repo name.") 574 return 575 } 576 577 defaultBranch := r.FormValue("branch") 578 if defaultBranch == "" { 579 defaultBranch = "main" 580 } 581 582 ok, err := s.enforcer.E.Enforce(user.Did, domain, domain, "repo:create") 583 if err != nil || !ok { 584 s.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.") 585 return 586 } 587 588 existingRepo, err := db.GetRepo(s.db, user.Did, repoName) 589 if err == nil && existingRepo != nil { 590 s.pages.Notice(w, "repo", fmt.Sprintf("A repo by this name already exists on %s", existingRepo.Knot)) 591 return 592 } 593 594 secret, err := db.GetRegistrationKey(s.db, domain) 595 if err != nil { 596 s.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", domain)) 597 return 598 } 599 600 client, err := NewSignedClient(domain, secret, s.config.Dev) 601 if err != nil { 602 s.pages.Notice(w, "repo", "Failed to connect to knot server.") 603 return 604 } 605 606 rkey := s.TID() 607 repo := &db.Repo{ 608 Did: user.Did, 609 Name: repoName, 610 Knot: domain, 611 Rkey: rkey, 612 } 613 614 xrpcClient, _ := s.auth.AuthorizedClient(r) 615 616 addedAt := time.Now().Format(time.RFC3339) 617 atresp, err := comatproto.RepoPutRecord(r.Context(), xrpcClient, &comatproto.RepoPutRecord_Input{ 618 Collection: tangled.RepoNSID, 619 Repo: user.Did, 620 Rkey: rkey, 621 Record: &lexutil.LexiconTypeDecoder{ 622 Val: &tangled.Repo{ 623 Knot: repo.Knot, 624 Name: repoName, 625 AddedAt: &addedAt, 626 Owner: user.Did, 627 }}, 628 }) 629 if err != nil { 630 log.Printf("failed to create record: %s", err) 631 s.pages.Notice(w, "repo", "Failed to announce repository creation.") 632 return 633 } 634 log.Println("created repo record: ", atresp.Uri) 635 636 tx, err := s.db.BeginTx(r.Context(), nil) 637 if err != nil { 638 log.Println(err) 639 s.pages.Notice(w, "repo", "Failed to save repository information.") 640 return 641 } 642 defer func() { 643 tx.Rollback() 644 err = s.enforcer.E.LoadPolicy() 645 if err != nil { 646 log.Println("failed to rollback policies") 647 } 648 }() 649 650 resp, err := client.NewRepo(user.Did, repoName, defaultBranch) 651 if err != nil { 652 s.pages.Notice(w, "repo", "Failed to create repository on knot server.") 653 return 654 } 655 656 switch resp.StatusCode { 657 case http.StatusConflict: 658 s.pages.Notice(w, "repo", "A repository with that name already exists.") 659 return 660 case http.StatusInternalServerError: 661 s.pages.Notice(w, "repo", "Failed to create repository on knot. Try again later.") 662 case http.StatusNoContent: 663 // continue 664 } 665 666 repo.AtUri = atresp.Uri 667 err = db.AddRepo(tx, repo) 668 if err != nil { 669 log.Println(err) 670 s.pages.Notice(w, "repo", "Failed to save repository information.") 671 return 672 } 673 674 // acls 675 p, _ := securejoin.SecureJoin(user.Did, repoName) 676 err = s.enforcer.AddRepo(user.Did, domain, p) 677 if err != nil { 678 log.Println(err) 679 s.pages.Notice(w, "repo", "Failed to set up repository permissions.") 680 return 681 } 682 683 err = tx.Commit() 684 if err != nil { 685 log.Println("failed to commit changes", err) 686 http.Error(w, err.Error(), http.StatusInternalServerError) 687 return 688 } 689 690 err = s.enforcer.E.SavePolicy() 691 if err != nil { 692 log.Println("failed to update ACLs", err) 693 http.Error(w, err.Error(), http.StatusInternalServerError) 694 return 695 } 696 697 s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s", user.Handle, repoName)) 698 return 699 } 700} 701 702func (s *State) ProfilePage(w http.ResponseWriter, r *http.Request) { 703 didOrHandle := chi.URLParam(r, "user") 704 if didOrHandle == "" { 705 http.Error(w, "Bad request", http.StatusBadRequest) 706 return 707 } 708 709 ident, err := s.resolver.ResolveIdent(r.Context(), didOrHandle) 710 if err != nil { 711 log.Printf("resolving identity: %s", err) 712 w.WriteHeader(http.StatusNotFound) 713 return 714 } 715 716 repos, err := db.GetAllReposByDid(s.db, ident.DID.String()) 717 if err != nil { 718 log.Printf("getting repos for %s: %s", ident.DID.String(), err) 719 } 720 721 collaboratingRepos, err := db.CollaboratingIn(s.db, ident.DID.String()) 722 if err != nil { 723 log.Printf("getting collaborating repos for %s: %s", ident.DID.String(), err) 724 } 725 var didsToResolve []string 726 for _, r := range collaboratingRepos { 727 didsToResolve = append(didsToResolve, r.Did) 728 } 729 resolvedIds := s.resolver.ResolveIdents(r.Context(), didsToResolve) 730 didHandleMap := make(map[string]string) 731 for _, identity := range resolvedIds { 732 if !identity.Handle.IsInvalidHandle() { 733 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 734 } else { 735 didHandleMap[identity.DID.String()] = identity.DID.String() 736 } 737 } 738 739 followers, following, err := db.GetFollowerFollowing(s.db, ident.DID.String()) 740 if err != nil { 741 log.Printf("getting follow stats repos for %s: %s", ident.DID.String(), err) 742 } 743 744 loggedInUser := s.auth.GetUser(r) 745 followStatus := db.IsNotFollowing 746 if loggedInUser != nil { 747 followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, ident.DID.String()) 748 } 749 750 profileAvatarUri, err := GetAvatarUri(ident.DID.String()) 751 if err != nil { 752 log.Println("failed to fetch bsky avatar", err) 753 } 754 755 s.pages.ProfilePage(w, pages.ProfilePageParams{ 756 LoggedInUser: loggedInUser, 757 UserDid: ident.DID.String(), 758 UserHandle: ident.Handle.String(), 759 Repos: repos, 760 CollaboratingRepos: collaboratingRepos, 761 ProfileStats: pages.ProfileStats{ 762 Followers: followers, 763 Following: following, 764 }, 765 FollowStatus: db.FollowStatus(followStatus), 766 DidHandleMap: didHandleMap, 767 AvatarUri: profileAvatarUri, 768 }) 769} 770 771func GetAvatarUri(did string) (string, error) { 772 recordURL := fmt.Sprintf("https://bsky.social/xrpc/com.atproto.repo.getRecord?repo=%s&collection=app.bsky.actor.profile&rkey=self", did) 773 774 recordResp, err := http.Get(recordURL) 775 if err != nil { 776 return "", err 777 } 778 defer recordResp.Body.Close() 779 780 if recordResp.StatusCode != http.StatusOK { 781 return "", fmt.Errorf("getRecord API returned status code %d", recordResp.StatusCode) 782 } 783 784 var profileResp map[string]any 785 if err := json.NewDecoder(recordResp.Body).Decode(&profileResp); err != nil { 786 return "", err 787 } 788 789 value, ok := profileResp["value"].(map[string]any) 790 if !ok { 791 log.Println(profileResp) 792 return "", fmt.Errorf("no value found for handle %s", did) 793 } 794 795 avatar, ok := value["avatar"].(map[string]any) 796 if !ok { 797 log.Println(profileResp) 798 return "", fmt.Errorf("no avatar found for handle %s", did) 799 } 800 801 blobRef, ok := avatar["ref"].(map[string]any) 802 if !ok { 803 log.Println(profileResp) 804 return "", fmt.Errorf("no ref found for handle %s", did) 805 } 806 807 link, ok := blobRef["$link"].(string) 808 if !ok { 809 log.Println(profileResp) 810 return "", fmt.Errorf("no link found for handle %s", did) 811 } 812 813 return fmt.Sprintf("https://cdn.bsky.app/img/feed_thumbnail/plain/%s/%s", did, link), nil 814} 815 816func (s *State) Router() http.Handler { 817 router := chi.NewRouter() 818 819 router.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) { 820 pat := chi.URLParam(r, "*") 821 if strings.HasPrefix(pat, "did:") || strings.HasPrefix(pat, "@") { 822 s.UserRouter().ServeHTTP(w, r) 823 } else { 824 s.StandardRouter().ServeHTTP(w, r) 825 } 826 }) 827 828 return router 829} 830 831func (s *State) UserRouter() http.Handler { 832 r := chi.NewRouter() 833 834 // strip @ from user 835 r.Use(StripLeadingAt) 836 837 r.With(ResolveIdent(s)).Route("/{user}", func(r chi.Router) { 838 r.Get("/", s.ProfilePage) 839 r.With(ResolveRepoKnot(s)).Route("/{repo}", func(r chi.Router) { 840 r.Get("/", s.RepoIndex) 841 r.Get("/commits/{ref}", s.RepoLog) 842 r.Route("/tree/{ref}", func(r chi.Router) { 843 r.Get("/", s.RepoIndex) 844 r.Get("/*", s.RepoTree) 845 }) 846 r.Get("/commit/{ref}", s.RepoCommit) 847 r.Get("/branches", s.RepoBranches) 848 r.Get("/tags", s.RepoTags) 849 r.Get("/blob/{ref}/*", s.RepoBlob) 850 851 r.Route("/issues", func(r chi.Router) { 852 r.Get("/", s.RepoIssues) 853 r.Get("/{issue}", s.RepoSingleIssue) 854 855 r.Group(func(r chi.Router) { 856 r.Use(AuthMiddleware(s)) 857 r.Get("/new", s.NewIssue) 858 r.Post("/new", s.NewIssue) 859 r.Post("/{issue}/comment", s.IssueComment) 860 r.Post("/{issue}/close", s.CloseIssue) 861 r.Post("/{issue}/reopen", s.ReopenIssue) 862 }) 863 }) 864 865 r.Route("/pulls", func(r chi.Router) { 866 r.Get("/", s.RepoPulls) 867 }) 868 869 // These routes get proxied to the knot 870 r.Get("/info/refs", s.InfoRefs) 871 r.Post("/git-upload-pack", s.UploadPack) 872 873 // settings routes, needs auth 874 r.Group(func(r chi.Router) { 875 r.Use(AuthMiddleware(s)) 876 r.With(RepoPermissionMiddleware(s, "repo:settings")).Route("/settings", func(r chi.Router) { 877 r.Get("/", s.RepoSettings) 878 r.With(RepoPermissionMiddleware(s, "repo:invite")).Put("/collaborator", s.AddCollaborator) 879 }) 880 }) 881 }) 882 }) 883 884 r.NotFound(func(w http.ResponseWriter, r *http.Request) { 885 s.pages.Error404(w) 886 }) 887 888 return r 889} 890 891func (s *State) StandardRouter() http.Handler { 892 r := chi.NewRouter() 893 894 r.Handle("/static/*", s.pages.Static()) 895 896 r.Get("/", s.Timeline) 897 898 r.With(AuthMiddleware(s)).Get("/logout", s.Logout) 899 900 r.Route("/login", func(r chi.Router) { 901 r.Get("/", s.Login) 902 r.Post("/", s.Login) 903 }) 904 905 r.Route("/knots", func(r chi.Router) { 906 r.Use(AuthMiddleware(s)) 907 r.Get("/", s.Knots) 908 r.Post("/key", s.RegistrationKey) 909 910 r.Route("/{domain}", func(r chi.Router) { 911 r.Post("/init", s.InitKnotServer) 912 r.Get("/", s.KnotServerInfo) 913 r.Route("/member", func(r chi.Router) { 914 r.Use(RoleMiddleware(s, "server:owner")) 915 r.Get("/", s.ListMembers) 916 r.Put("/", s.AddMember) 917 r.Delete("/", s.RemoveMember) 918 }) 919 }) 920 }) 921 922 r.Route("/repo", func(r chi.Router) { 923 r.Route("/new", func(r chi.Router) { 924 r.Use(AuthMiddleware(s)) 925 r.Get("/", s.NewRepo) 926 r.Post("/", s.NewRepo) 927 }) 928 // r.Post("/import", s.ImportRepo) 929 }) 930 931 r.With(AuthMiddleware(s)).Route("/follow", func(r chi.Router) { 932 r.Post("/", s.Follow) 933 r.Delete("/", s.Follow) 934 }) 935 936 r.Route("/settings", func(r chi.Router) { 937 r.Use(AuthMiddleware(s)) 938 r.Get("/", s.Settings) 939 r.Put("/keys", s.SettingsKeys) 940 }) 941 942 r.Get("/keys/{user}", s.Keys) 943 944 r.NotFound(func(w http.ResponseWriter, r *http.Request) { 945 s.pages.Error404(w) 946 }) 947 return r 948}