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