Subscribe and post RSS feeds to Bluesky
rss bluesky

fix: refresh bsky auth tokens

Changed files
+62 -1
internal
bluesky
+1 -1
.gitignore
··· 4 4 *.dll 5 5 *.so 6 6 *.dylib 7 - bksy-rss-post 7 + bskyrss 8 8 9 9 # Test binary, built with `go test -c` 10 10 *.test
+61
internal/bluesky/client.go
··· 4 4 "context" 5 5 "fmt" 6 6 "strings" 7 + "sync" 7 8 "time" 8 9 9 10 "github.com/bluesky-social/indigo/api/atproto" ··· 17 18 xrpcClient *xrpc.Client 18 19 handle string 19 20 did string 21 + config Config 22 + mu sync.Mutex // protects token refresh 20 23 } 21 24 22 25 // Config holds configuration for Bluesky client ··· 56 59 xrpcClient: xrpcClient, 57 60 handle: auth.Handle, 58 61 did: auth.Did, 62 + config: cfg, 59 63 }, nil 60 64 } 61 65 ··· 85 89 86 90 _, err := atproto.RepoCreateRecord(ctx, c.xrpcClient, input) 87 91 if err != nil { 92 + // Check if token expired and retry once after refresh 93 + if c.isExpiredTokenError(err) { 94 + if refreshErr := c.refreshSession(ctx); refreshErr != nil { 95 + return fmt.Errorf("failed to create post: %w (refresh failed: %v)", err, refreshErr) 96 + } 97 + // Retry the post after refreshing 98 + _, err = atproto.RepoCreateRecord(ctx, c.xrpcClient, input) 99 + if err != nil { 100 + return fmt.Errorf("failed to create post after refresh: %w", err) 101 + } 102 + return nil 103 + } 88 104 return fmt.Errorf("failed to create post: %w", err) 105 + } 106 + 107 + return nil 108 + } 109 + 110 + // isExpiredTokenError checks if the error is due to an expired token 111 + func (c *Client) isExpiredTokenError(err error) bool { 112 + if err == nil { 113 + return false 114 + } 115 + errStr := err.Error() 116 + return strings.Contains(errStr, "ExpiredToken") || strings.Contains(errStr, "Token has expired") 117 + } 118 + 119 + // refreshSession refreshes the authentication session 120 + func (c *Client) refreshSession(ctx context.Context) error { 121 + c.mu.Lock() 122 + defer c.mu.Unlock() 123 + 124 + // Check if someone else already refreshed while we were waiting 125 + if c.xrpcClient.Auth != nil && c.xrpcClient.Auth.RefreshJwt != "" { 126 + // Try to use the refresh token 127 + refresh, err := atproto.ServerRefreshSession(ctx, c.xrpcClient) 128 + if err == nil { 129 + c.xrpcClient.Auth.AccessJwt = refresh.AccessJwt 130 + c.xrpcClient.Auth.RefreshJwt = refresh.RefreshJwt 131 + return nil 132 + } 133 + // If refresh failed, fall through to re-authentication 134 + } 135 + 136 + // If refresh token doesn't work, re-authenticate with password 137 + auth, err := atproto.ServerCreateSession(ctx, c.xrpcClient, &atproto.ServerCreateSession_Input{ 138 + Identifier: c.config.Handle, 139 + Password: c.config.Password, 140 + }) 141 + if err != nil { 142 + return fmt.Errorf("failed to re-authenticate: %w", err) 143 + } 144 + 145 + c.xrpcClient.Auth = &xrpc.AuthInfo{ 146 + AccessJwt: auth.AccessJwt, 147 + RefreshJwt: auth.RefreshJwt, 148 + Handle: auth.Handle, 149 + Did: auth.Did, 89 150 } 90 151 91 152 return nil