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