forked from tangled.org/core
Monorepo for Tangled — https://tangled.org
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, 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 ok, err := s.enforcer.IsServerOwner(user.Did, domain) 424 isOwner := err == nil && ok 425 426 p := pages.KnotParams{ 427 LoggedInUser: user, 428 Registration: reg, 429 Members: members, 430 IsOwner: isOwner, 431 } 432 433 s.pages.Knot(w, p) 434} 435 436// get knots registered by this user 437func (s *State) Knots(w http.ResponseWriter, r *http.Request) { 438 // for now, this is just pubkeys 439 user := s.auth.GetUser(r) 440 registrations, err := db.RegistrationsByDid(s.db, user.Did) 441 if err != nil { 442 log.Println(err) 443 } 444 445 s.pages.Knots(w, pages.KnotsParams{ 446 LoggedInUser: user, 447 Registrations: registrations, 448 }) 449} 450 451// list members of domain, requires auth and requires owner status 452func (s *State) ListMembers(w http.ResponseWriter, r *http.Request) { 453 domain := chi.URLParam(r, "domain") 454 if domain == "" { 455 http.Error(w, "malformed url", http.StatusBadRequest) 456 return 457 } 458 459 // list all members for this domain 460 memberDids, err := s.enforcer.GetUserByRole("server:member", domain) 461 if err != nil { 462 w.Write([]byte("failed to fetch member list")) 463 return 464 } 465 466 w.Write([]byte(strings.Join(memberDids, "\n"))) 467 return 468} 469 470// add member to domain, requires auth and requires invite access 471func (s *State) AddMember(w http.ResponseWriter, r *http.Request) { 472 domain := chi.URLParam(r, "domain") 473 if domain == "" { 474 http.Error(w, "malformed url", http.StatusBadRequest) 475 return 476 } 477 478 memberDid := r.FormValue("member") 479 if memberDid == "" { 480 http.Error(w, "malformed form", http.StatusBadRequest) 481 return 482 } 483 484 memberIdent, err := s.resolver.ResolveIdent(r.Context(), memberDid) 485 if err != nil { 486 w.Write([]byte("failed to resolve member did to a handle")) 487 return 488 } 489 log.Printf("adding %s to %s\n", memberIdent.Handle.String(), domain) 490 491 // announce this relation into the firehose, store into owners' pds 492 client, _ := s.auth.AuthorizedClient(r) 493 currentUser := s.auth.GetUser(r) 494 addedAt := time.Now().Format(time.RFC3339) 495 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 496 Collection: tangled.KnotMemberNSID, 497 Repo: currentUser.Did, 498 Rkey: s.TID(), 499 Record: &lexutil.LexiconTypeDecoder{ 500 Val: &tangled.KnotMember{ 501 Member: memberIdent.DID.String(), 502 Domain: domain, 503 AddedAt: &addedAt, 504 }}, 505 }) 506 507 // invalid record 508 if err != nil { 509 log.Printf("failed to create record: %s", err) 510 return 511 } 512 log.Println("created atproto record: ", resp.Uri) 513 514 secret, err := db.GetRegistrationKey(s.db, domain) 515 if err != nil { 516 log.Printf("no key found for domain %s: %s\n", domain, err) 517 return 518 } 519 520 ksClient, err := NewSignedClient(domain, secret, s.config.Dev) 521 if err != nil { 522 log.Println("failed to create client to ", domain) 523 return 524 } 525 526 ksResp, err := ksClient.AddMember(memberIdent.DID.String()) 527 if err != nil { 528 log.Printf("failed to make request to %s: %s", domain, err) 529 return 530 } 531 532 if ksResp.StatusCode != http.StatusNoContent { 533 w.Write([]byte(fmt.Sprint("knotserver failed to add member: ", err))) 534 return 535 } 536 537 err = s.enforcer.AddMember(domain, memberIdent.DID.String()) 538 if err != nil { 539 w.Write([]byte(fmt.Sprint("failed to add member: ", err))) 540 return 541 } 542 543 w.Write([]byte(fmt.Sprint("added member: ", memberIdent.Handle.String()))) 544} 545 546func (s *State) RemoveMember(w http.ResponseWriter, r *http.Request) { 547} 548 549func (s *State) NewRepo(w http.ResponseWriter, r *http.Request) { 550 switch r.Method { 551 case http.MethodGet: 552 user := s.auth.GetUser(r) 553 knots, err := s.enforcer.GetDomainsForUser(user.Did) 554 if err != nil { 555 s.pages.Notice(w, "repo", "Invalid user account.") 556 return 557 } 558 559 s.pages.NewRepo(w, pages.NewRepoParams{ 560 LoggedInUser: user, 561 Knots: knots, 562 }) 563 564 case http.MethodPost: 565 user := s.auth.GetUser(r) 566 567 domain := r.FormValue("domain") 568 if domain == "" { 569 s.pages.Notice(w, "repo", "Invalid form submission—missing knot domain.") 570 return 571 } 572 573 repoName := r.FormValue("name") 574 if repoName == "" { 575 s.pages.Notice(w, "repo", "Invalid repo name.") 576 return 577 } 578 579 defaultBranch := r.FormValue("branch") 580 if defaultBranch == "" { 581 defaultBranch = "main" 582 } 583 584 description := r.FormValue("description") 585 586 ok, err := s.enforcer.E.Enforce(user.Did, domain, domain, "repo:create") 587 if err != nil || !ok { 588 s.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.") 589 return 590 } 591 592 existingRepo, err := db.GetRepo(s.db, user.Did, repoName) 593 if err == nil && existingRepo != nil { 594 s.pages.Notice(w, "repo", fmt.Sprintf("A repo by this name already exists on %s", existingRepo.Knot)) 595 return 596 } 597 598 secret, err := db.GetRegistrationKey(s.db, domain) 599 if err != nil { 600 s.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", domain)) 601 return 602 } 603 604 client, err := NewSignedClient(domain, secret, s.config.Dev) 605 if err != nil { 606 s.pages.Notice(w, "repo", "Failed to connect to knot server.") 607 return 608 } 609 610 rkey := s.TID() 611 repo := &db.Repo{ 612 Did: user.Did, 613 Name: repoName, 614 Knot: domain, 615 Rkey: rkey, 616 Description: description, 617 } 618 619 xrpcClient, _ := s.auth.AuthorizedClient(r) 620 621 addedAt := time.Now().Format(time.RFC3339) 622 atresp, err := comatproto.RepoPutRecord(r.Context(), xrpcClient, &comatproto.RepoPutRecord_Input{ 623 Collection: tangled.RepoNSID, 624 Repo: user.Did, 625 Rkey: rkey, 626 Record: &lexutil.LexiconTypeDecoder{ 627 Val: &tangled.Repo{ 628 Knot: repo.Knot, 629 Name: repoName, 630 AddedAt: &addedAt, 631 Owner: user.Did, 632 }}, 633 }) 634 if err != nil { 635 log.Printf("failed to create record: %s", err) 636 s.pages.Notice(w, "repo", "Failed to announce repository creation.") 637 return 638 } 639 log.Println("created repo record: ", atresp.Uri) 640 641 tx, err := s.db.BeginTx(r.Context(), nil) 642 if err != nil { 643 log.Println(err) 644 s.pages.Notice(w, "repo", "Failed to save repository information.") 645 return 646 } 647 defer func() { 648 tx.Rollback() 649 err = s.enforcer.E.LoadPolicy() 650 if err != nil { 651 log.Println("failed to rollback policies") 652 } 653 }() 654 655 resp, err := client.NewRepo(user.Did, repoName, defaultBranch) 656 if err != nil { 657 s.pages.Notice(w, "repo", "Failed to create repository on knot server.") 658 return 659 } 660 661 switch resp.StatusCode { 662 case http.StatusConflict: 663 s.pages.Notice(w, "repo", "A repository with that name already exists.") 664 return 665 case http.StatusInternalServerError: 666 s.pages.Notice(w, "repo", "Failed to create repository on knot. Try again later.") 667 case http.StatusNoContent: 668 // continue 669 } 670 671 repo.AtUri = atresp.Uri 672 err = db.AddRepo(tx, repo) 673 if err != nil { 674 log.Println(err) 675 s.pages.Notice(w, "repo", "Failed to save repository information.") 676 return 677 } 678 679 // acls 680 p, _ := securejoin.SecureJoin(user.Did, repoName) 681 err = s.enforcer.AddRepo(user.Did, domain, p) 682 if err != nil { 683 log.Println(err) 684 s.pages.Notice(w, "repo", "Failed to set up repository permissions.") 685 return 686 } 687 688 err = tx.Commit() 689 if err != nil { 690 log.Println("failed to commit changes", err) 691 http.Error(w, err.Error(), http.StatusInternalServerError) 692 return 693 } 694 695 err = s.enforcer.E.SavePolicy() 696 if err != nil { 697 log.Println("failed to update ACLs", err) 698 http.Error(w, err.Error(), http.StatusInternalServerError) 699 return 700 } 701 702 s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s", user.Handle, repoName)) 703 return 704 } 705} 706 707func (s *State) ProfilePage(w http.ResponseWriter, r *http.Request) { 708 didOrHandle := chi.URLParam(r, "user") 709 if didOrHandle == "" { 710 http.Error(w, "Bad request", http.StatusBadRequest) 711 return 712 } 713 714 ident, err := s.resolver.ResolveIdent(r.Context(), didOrHandle) 715 if err != nil { 716 log.Printf("resolving identity: %s", err) 717 w.WriteHeader(http.StatusNotFound) 718 return 719 } 720 721 repos, err := db.GetAllReposByDid(s.db, ident.DID.String()) 722 if err != nil { 723 log.Printf("getting repos for %s: %s", ident.DID.String(), err) 724 } 725 726 collaboratingRepos, err := db.CollaboratingIn(s.db, ident.DID.String()) 727 if err != nil { 728 log.Printf("getting collaborating repos for %s: %s", ident.DID.String(), err) 729 } 730 var didsToResolve []string 731 for _, r := range collaboratingRepos { 732 didsToResolve = append(didsToResolve, r.Did) 733 } 734 resolvedIds := s.resolver.ResolveIdents(r.Context(), didsToResolve) 735 didHandleMap := make(map[string]string) 736 for _, identity := range resolvedIds { 737 if !identity.Handle.IsInvalidHandle() { 738 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 739 } else { 740 didHandleMap[identity.DID.String()] = identity.DID.String() 741 } 742 } 743 744 followers, following, err := db.GetFollowerFollowing(s.db, ident.DID.String()) 745 if err != nil { 746 log.Printf("getting follow stats repos for %s: %s", ident.DID.String(), err) 747 } 748 749 loggedInUser := s.auth.GetUser(r) 750 followStatus := db.IsNotFollowing 751 if loggedInUser != nil { 752 followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, ident.DID.String()) 753 } 754 755 profileAvatarUri, err := GetAvatarUri(ident.DID.String()) 756 if err != nil { 757 log.Println("failed to fetch bsky avatar", err) 758 } 759 760 s.pages.ProfilePage(w, pages.ProfilePageParams{ 761 LoggedInUser: loggedInUser, 762 UserDid: ident.DID.String(), 763 UserHandle: ident.Handle.String(), 764 Repos: repos, 765 CollaboratingRepos: collaboratingRepos, 766 ProfileStats: pages.ProfileStats{ 767 Followers: followers, 768 Following: following, 769 }, 770 FollowStatus: db.FollowStatus(followStatus), 771 DidHandleMap: didHandleMap, 772 AvatarUri: profileAvatarUri, 773 }) 774} 775 776func GetAvatarUri(did string) (string, error) { 777 recordURL := fmt.Sprintf("https://bsky.social/xrpc/com.atproto.repo.getRecord?repo=%s&collection=app.bsky.actor.profile&rkey=self", did) 778 779 recordResp, err := http.Get(recordURL) 780 if err != nil { 781 return "", err 782 } 783 defer recordResp.Body.Close() 784 785 if recordResp.StatusCode != http.StatusOK { 786 return "", fmt.Errorf("getRecord API returned status code %d", recordResp.StatusCode) 787 } 788 789 var profileResp map[string]any 790 if err := json.NewDecoder(recordResp.Body).Decode(&profileResp); err != nil { 791 return "", err 792 } 793 794 value, ok := profileResp["value"].(map[string]any) 795 if !ok { 796 log.Println(profileResp) 797 return "", fmt.Errorf("no value found for handle %s", did) 798 } 799 800 avatar, ok := value["avatar"].(map[string]any) 801 if !ok { 802 log.Println(profileResp) 803 return "", fmt.Errorf("no avatar found for handle %s", did) 804 } 805 806 blobRef, ok := avatar["ref"].(map[string]any) 807 if !ok { 808 log.Println(profileResp) 809 return "", fmt.Errorf("no ref found for handle %s", did) 810 } 811 812 link, ok := blobRef["$link"].(string) 813 if !ok { 814 log.Println(profileResp) 815 return "", fmt.Errorf("no link found for handle %s", did) 816 } 817 818 return fmt.Sprintf("https://cdn.bsky.app/img/feed_thumbnail/plain/%s/%s", did, link), nil 819} 820 821func (s *State) Router() http.Handler { 822 router := chi.NewRouter() 823 824 router.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) { 825 pat := chi.URLParam(r, "*") 826 if strings.HasPrefix(pat, "did:") || strings.HasPrefix(pat, "@") { 827 s.UserRouter().ServeHTTP(w, r) 828 } else { 829 s.StandardRouter().ServeHTTP(w, r) 830 } 831 }) 832 833 return router 834} 835 836func (s *State) UserRouter() http.Handler { 837 r := chi.NewRouter() 838 839 // strip @ from user 840 r.Use(StripLeadingAt) 841 842 r.With(ResolveIdent(s)).Route("/{user}", func(r chi.Router) { 843 r.Get("/", s.ProfilePage) 844 r.With(ResolveRepoKnot(s)).Route("/{repo}", func(r chi.Router) { 845 r.Get("/", s.RepoIndex) 846 r.Get("/commits/{ref}", s.RepoLog) 847 r.Route("/tree/{ref}", func(r chi.Router) { 848 r.Get("/", s.RepoIndex) 849 r.Get("/*", s.RepoTree) 850 }) 851 r.Get("/commit/{ref}", s.RepoCommit) 852 r.Get("/branches", s.RepoBranches) 853 r.Get("/tags", s.RepoTags) 854 r.Get("/blob/{ref}/*", s.RepoBlob) 855 856 r.Route("/issues", func(r chi.Router) { 857 r.Get("/", s.RepoIssues) 858 r.Get("/{issue}", s.RepoSingleIssue) 859 860 r.Group(func(r chi.Router) { 861 r.Use(AuthMiddleware(s)) 862 r.Get("/new", s.NewIssue) 863 r.Post("/new", s.NewIssue) 864 r.Post("/{issue}/comment", s.IssueComment) 865 r.Post("/{issue}/close", s.CloseIssue) 866 r.Post("/{issue}/reopen", s.ReopenIssue) 867 }) 868 }) 869 870 r.Route("/pulls", func(r chi.Router) { 871 r.Get("/", s.RepoPulls) 872 }) 873 874 // These routes get proxied to the knot 875 r.Get("/info/refs", s.InfoRefs) 876 r.Post("/git-upload-pack", s.UploadPack) 877 878 // settings routes, needs auth 879 r.Group(func(r chi.Router) { 880 r.Use(AuthMiddleware(s)) 881 // repo description can only be edited by owner 882 r.With(RepoPermissionMiddleware(s, "repo:owner")).Route("/description", func(r chi.Router) { 883 r.Put("/", s.RepoDescription) 884 r.Get("/", s.RepoDescription) 885 r.Get("/edit", s.RepoDescriptionEdit) 886 }) 887 r.With(RepoPermissionMiddleware(s, "repo:settings")).Route("/settings", func(r chi.Router) { 888 r.Get("/", s.RepoSettings) 889 r.With(RepoPermissionMiddleware(s, "repo:invite")).Put("/collaborator", s.AddCollaborator) 890 }) 891 }) 892 }) 893 }) 894 895 r.NotFound(func(w http.ResponseWriter, r *http.Request) { 896 s.pages.Error404(w) 897 }) 898 899 return r 900} 901 902func (s *State) StandardRouter() http.Handler { 903 r := chi.NewRouter() 904 905 r.Handle("/static/*", s.pages.Static()) 906 907 r.Get("/", s.Timeline) 908 909 r.With(AuthMiddleware(s)).Get("/logout", s.Logout) 910 911 r.Route("/login", func(r chi.Router) { 912 r.Get("/", s.Login) 913 r.Post("/", s.Login) 914 }) 915 916 r.Route("/knots", func(r chi.Router) { 917 r.Use(AuthMiddleware(s)) 918 r.Get("/", s.Knots) 919 r.Post("/key", s.RegistrationKey) 920 921 r.Route("/{domain}", func(r chi.Router) { 922 r.Post("/init", s.InitKnotServer) 923 r.Get("/", s.KnotServerInfo) 924 r.Route("/member", func(r chi.Router) { 925 r.Use(RoleMiddleware(s, "server:owner")) 926 r.Get("/", s.ListMembers) 927 r.Put("/", s.AddMember) 928 r.Delete("/", s.RemoveMember) 929 }) 930 }) 931 }) 932 933 r.Route("/repo", func(r chi.Router) { 934 r.Route("/new", func(r chi.Router) { 935 r.Use(AuthMiddleware(s)) 936 r.Get("/", s.NewRepo) 937 r.Post("/", s.NewRepo) 938 }) 939 // r.Post("/import", s.ImportRepo) 940 }) 941 942 r.With(AuthMiddleware(s)).Route("/follow", func(r chi.Router) { 943 r.Post("/", s.Follow) 944 r.Delete("/", s.Follow) 945 }) 946 947 r.With(AuthMiddleware(s)).Route("/star", func(r chi.Router) { 948 r.Post("/", s.Star) 949 r.Delete("/", s.Star) 950 }) 951 952 r.Route("/settings", func(r chi.Router) { 953 r.Use(AuthMiddleware(s)) 954 r.Get("/", s.Settings) 955 r.Put("/keys", s.SettingsKeys) 956 }) 957 958 r.Get("/keys/{user}", s.Keys) 959 960 r.NotFound(func(w http.ResponseWriter, r *http.Request) { 961 s.pages.Error404(w) 962 }) 963 return r 964}