like malachite (atproto-lastfm-importer) but in go and bluer
go spotify tealfm lastfm atproto
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

atproto/client: upstreamed the refresh fix

karitham c7ec171f 8fdef6b6

+5 -212
-103
atproto/client.go
··· 10 10 "time" 11 11 12 12 "github.com/bluesky-social/indigo/atproto/atclient" 13 - "github.com/bluesky-social/indigo/atproto/syntax" 14 13 ) 15 14 16 15 const ( ··· 127 126 return result.DID, result.PDS, result.SigningKey, nil 128 127 } 129 128 130 - type FixedPasswordAuth struct { 131 - *atclient.PasswordAuth 132 - lk sync.RWMutex 133 - RefreshCallback func(ctx context.Context, session atclient.PasswordSessionData) 134 - } 135 - 136 - func (a *FixedPasswordAuth) DoWithAuth(c *http.Client, req *http.Request, endpoint syntax.NSID) (*http.Response, error) { 137 - accessToken, refreshToken := a.GetTokens() 138 - req.Header.Set("Authorization", "Bearer "+accessToken) 139 - resp, err := c.Do(req) 140 - if err != nil { 141 - return nil, err 142 - } 143 - 144 - if resp.StatusCode != http.StatusBadRequest { 145 - return resp, nil 146 - } 147 - 148 - if !hasJSONContent(resp.Header) { 149 - return resp, nil 150 - } 151 - 152 - defer resp.Body.Close() 153 - var eb atclient.ErrorBody 154 - if err := json.NewDecoder(resp.Body).Decode(&eb); err != nil { 155 - return nil, &atclient.APIError{StatusCode: resp.StatusCode} 156 - } 157 - if eb.Name != "ExpiredToken" { 158 - return nil, eb.APIError(resp.StatusCode) 159 - } 160 - 161 - if err := a.Refresh(req.Context(), c, refreshToken); err != nil { 162 - return nil, err 163 - } 164 - 165 - retry := req.Clone(req.Context()) 166 - if req.GetBody != nil { 167 - retryBody, err := req.GetBody() 168 - if err != nil { 169 - return nil, fmt.Errorf("API request retry GetBody failed: %w", err) 170 - } 171 - retry.Body = retryBody 172 - } 173 - 174 - accessToken, _ = a.GetTokens() 175 - retry.Header.Set("Authorization", "Bearer "+accessToken) 176 - return c.Do(retry) 177 - } 178 - 179 129 func hasJSONContent(header http.Header) bool { 180 130 return len(header.Get("Content-Type")) > 0 && header.Get("Content-Type")[0:19] == "application/json" 181 131 } 182 132 183 - func (a *FixedPasswordAuth) Refresh(ctx context.Context, c *http.Client, priorRefreshToken string) error { 184 - a.lk.Lock() 185 - defer a.lk.Unlock() 186 - 187 - if priorRefreshToken != "" && priorRefreshToken != a.Session.RefreshToken { 188 - return nil 189 - } 190 - 191 - u := a.Session.Host + "/xrpc/com.atproto.server.refreshSession" 192 - req, err := http.NewRequestWithContext(ctx, http.MethodPost, u, nil) 193 - if err != nil { 194 - return err 195 - } 196 - req.Header.Set("User-Agent", "indigo-sdk") 197 - req.Header.Set("Authorization", "Bearer "+a.Session.RefreshToken) 198 - 199 - resp, err := c.Do(req) 200 - if err != nil { 201 - return err 202 - } 203 - defer resp.Body.Close() 204 - 205 - if resp.StatusCode < 200 || resp.StatusCode >= 300 { 206 - var eb atclient.ErrorBody 207 - if err := json.NewDecoder(resp.Body).Decode(&eb); err != nil { 208 - return &atclient.APIError{StatusCode: resp.StatusCode} 209 - } 210 - return eb.APIError(resp.StatusCode) 211 - } 212 - 213 - var out struct { 214 - AccessJwt string `json:"accessJwt"` 215 - RefreshJwt string `json:"refreshJwt"` 216 - } 217 - if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { 218 - return err 219 - } 220 - 221 - a.Session.AccessToken = out.AccessJwt 222 - a.Session.RefreshToken = out.RefreshJwt 223 - 224 - if a.RefreshCallback != nil { 225 - snapshot := a.Session.Clone() 226 - a.RefreshCallback(ctx, snapshot) 227 - } 228 - 229 - return nil 230 - } 231 - 232 133 func ResolveIdentity(ctx context.Context, handle string, opts *ClientOptions) (resolvedIdentity, error) { 233 134 if handle == "" { 234 135 return resolvedIdentity{}, fmt.Errorf("handle cannot be empty") ··· 274 175 client, err := atclient.LoginWithPasswordHost(ctx, pdsURL, handle, password, "", nil) 275 176 if err != nil { 276 177 return nil, fmt.Errorf("login failed: %w", err) 277 - } 278 - 279 - if pa, ok := client.Auth.(*atclient.PasswordAuth); ok { 280 - client.Auth = &FixedPasswordAuth{PasswordAuth: pa} 281 178 } 282 179 283 180 return &Client{
-104
atproto/client_test.go
··· 598 598 func (m *mockRepoClient[T]) DeleteRecord(ctx context.Context, collection, rkey string) error { 599 599 return nil 600 600 } 601 - 602 - func TestFixedPasswordAuth_RefreshUsesPOST(t *testing.T) { 603 - var capturedMethod string 604 - var capturedPath string 605 - 606 - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 607 - capturedMethod = r.Method 608 - capturedPath = r.URL.Path 609 - 610 - if capturedMethod == http.MethodPost && capturedPath == "/xrpc/com.atproto.server.refreshSession" { 611 - w.Header().Set("Content-Type", "application/json") 612 - json.NewEncoder(w).Encode(map[string]any{ 613 - "accessJwt": "new-access-token", 614 - "refreshJwt": "new-refresh-token", 615 - }) 616 - return 617 - } 618 - 619 - if capturedPath == "/xrpc/com.atproto.server.createSession" { 620 - w.Header().Set("Content-Type", "application/json") 621 - json.NewEncoder(w).Encode(map[string]any{ 622 - "accessJwt": "access-token", 623 - "refreshJwt": "refresh-token", 624 - "did": "did:plc:test", 625 - }) 626 - return 627 - } 628 - 629 - w.WriteHeader(http.StatusBadRequest) 630 - })) 631 - defer server.Close() 632 - 633 - pdsURL := server.URL 634 - 635 - session := atclient.PasswordSessionData{ 636 - AccessToken: "old-access-token", 637 - RefreshToken: "refresh-token", 638 - Host: pdsURL, 639 - } 640 - 641 - auth := &FixedPasswordAuth{ 642 - PasswordAuth: &atclient.PasswordAuth{Session: session}, 643 - } 644 - 645 - httpClient := server.Client() 646 - 647 - ctx := context.Background() 648 - err := auth.Refresh(ctx, httpClient, "refresh-token") 649 - if err != nil { 650 - t.Fatalf("Refresh failed: %v", err) 651 - } 652 - 653 - if capturedMethod != http.MethodPost { 654 - t.Errorf("Refresh used %s, want POST", capturedMethod) 655 - } 656 - 657 - if capturedPath != "/xrpc/com.atproto.server.refreshSession" { 658 - t.Errorf("Refresh hit %s, want /xrpc/com.atproto.server.refreshSession", capturedPath) 659 - } 660 - } 661 - 662 - func TestFixedPasswordAuth_IndigoBugCheck(t *testing.T) { 663 - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 664 - if r.URL.Path == "/xrpc/com.atproto.server.refreshSession" { 665 - if r.Method != http.MethodPost { 666 - t.Errorf("REFRESH BUG: indigo uses %s for refreshSession, should use POST", r.Method) 667 - } 668 - w.Header().Set("Content-Type", "application/json") 669 - json.NewEncoder(w).Encode(map[string]any{ 670 - "accessJwt": "new-access-token", 671 - "refreshJwt": "new-refresh-token", 672 - }) 673 - return 674 - } 675 - if r.URL.Path == "/xrpc/com.atproto.server.createSession" { 676 - w.Header().Set("Content-Type", "application/json") 677 - json.NewEncoder(w).Encode(map[string]any{ 678 - "accessJwt": "access-token", 679 - "refreshJwt": "refresh-token", 680 - "did": "did:plc:test", 681 - }) 682 - return 683 - } 684 - w.WriteHeader(http.StatusBadRequest) 685 - })) 686 - defer server.Close() 687 - 688 - pdsURL := server.URL 689 - 690 - pa := &atclient.PasswordAuth{ 691 - Session: atclient.PasswordSessionData{ 692 - AccessToken: "old-access-token", 693 - RefreshToken: "refresh-token", 694 - Host: pdsURL, 695 - }, 696 - } 697 - 698 - fixedAuth := &FixedPasswordAuth{PasswordAuth: pa} 699 - 700 - err := fixedAuth.Refresh(context.Background(), server.Client(), "refresh-token") 701 - if err != nil { 702 - t.Fatalf("FixedPasswordAuth.Refresh failed: %v", err) 703 - } 704 - }
+2 -2
flake.nix
··· 17 17 let 18 18 lazuli = pkgs.buildGoModule rec { 19 19 name = "lazuli"; 20 - version = "0.1.4"; 20 + version = "0.1.5"; 21 21 src = pkgs.nix-gitignore.gitignoreSource [ "*.csv" "*.zip" "*.json" ] ./.; 22 - vendorHash = "sha256-MfBPv/L7wHuUGXx4BDd+DFq0RB11KuMHCzPjFv6FMgs="; 22 + vendorHash = "sha256-O6R8jC8Ms5gsY2FUmuL8lTGTODfMW1CsSWuWbN27zeY="; 23 23 ldflags = [ 24 24 "-X" 25 25 "main.Version=${version}"
+1 -1
go.mod
··· 3 3 go 1.25.5 4 4 5 5 require ( 6 - github.com/bluesky-social/indigo v0.0.0-20260120225912-12d69fa4d209 6 + github.com/bluesky-social/indigo v0.0.0-20260122235001-7f2e6b43efbb 7 7 github.com/failsafe-go/failsafe-go v0.9.5 8 8 github.com/urfave/cli/v3 v3.6.2 9 9 go.etcd.io/bbolt v1.4.3
+2 -2
go.sum
··· 2 2 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 3 3 github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE= 4 4 github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= 5 - github.com/bluesky-social/indigo v0.0.0-20260120225912-12d69fa4d209 h1:W01PGqjCexVBzIZ4FoNe4iO8OhI9XbSE7ieWL0QnMu8= 6 - github.com/bluesky-social/indigo v0.0.0-20260120225912-12d69fa4d209/go.mod h1:KIy0FgNQacp4uv2Z7xhNkV3qZiUSGuRky97s7Pa4v+o= 5 + github.com/bluesky-social/indigo v0.0.0-20260122235001-7f2e6b43efbb h1:3FvzRkxe85/HsnQubXgdg8Vf38J5d1Sk9XmOkm2TCvY= 6 + github.com/bluesky-social/indigo v0.0.0-20260122235001-7f2e6b43efbb/go.mod h1:KIy0FgNQacp4uv2Z7xhNkV3qZiUSGuRky97s7Pa4v+o= 7 7 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 8 8 github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 9 9 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=