An engagement based washing machine that spins you round and round!
6
fork

Configure Feed

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

Fix token persistence

Brooke 1264af30 b56e6f44

+80 -22
+10
cmd/brooke-spin/main.go
··· 8 8 "math" 9 9 "os" 10 10 "os/signal" 11 + "strings" 11 12 "syscall" 12 13 "time" 13 14 ··· 94 95 case <-ticker.C: 95 96 if err := checkAndRotate(ctx, cfg, blueskyClient, imageProcessor, stateManager, currentState); err != nil { 96 97 log.Printf("Error during polling: %v", err) 98 + 99 + if strings.Contains(err.Error(), "ExpiredToken") { 100 + log.Println("Token expired. Re-authenticating...") 101 + if authErr := blueskyClient.Authenticate(cfg.Bluesky.Handle, cfg.Bluesky.AppPassword); authErr != nil { 102 + log.Printf("Re-authentication failed: %v", authErr) 103 + } else { 104 + log.Println("Re-authentication successful") 105 + } 106 + } 97 107 // Continue polling despite errors 98 108 } 99 109 }
+70 -22
internal/client/bluesky.go
··· 6 6 "fmt" 7 7 "io" 8 8 "net/http" 9 + "strings" 9 10 "time" 10 11 ) 11 12 ··· 19 20 accessToken string 20 21 did string 21 22 client *http.Client 23 + handle string 24 + appPassword string 22 25 } 23 26 24 27 // NewClient creates a new Bluesky client ··· 33 36 34 37 // Authenticate authenticates with the Bluesky API 35 38 func (c *Client) Authenticate(handle, appPassword string) error { 39 + c.handle = handle 40 + c.appPassword = appPassword 41 + 36 42 payload := map[string]string{ 37 43 "identifier": handle, 38 44 "password": appPassword, ··· 73 79 return nil 74 80 } 75 81 82 + // doRequest executes a request with automatic token refresh 83 + func (c *Client) doRequest(req *http.Request) (*http.Response, error) { 84 + // Set authorization header 85 + req.Header.Set("Authorization", "Bearer "+c.accessToken) 86 + 87 + resp, err := c.client.Do(req) 88 + if err != nil { 89 + return nil, err 90 + } 91 + 92 + // Check for token expiration (status 400 with ExpiredToken error) 93 + if resp.StatusCode == http.StatusBadRequest { 94 + bodyBytes, err := io.ReadAll(resp.Body) 95 + resp.Body.Close() 96 + if err != nil { 97 + return nil, fmt.Errorf("failed to read response body: %w", err) 98 + } 99 + 100 + if strings.Contains(string(bodyBytes), "ExpiredToken") { 101 + // Token expired, attempt to re-authenticate 102 + if authErr := c.Authenticate(c.handle, c.appPassword); authErr != nil { 103 + // If re-auth fails, return original error/response 104 + resp.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) 105 + return resp, nil 106 + } 107 + 108 + // Re-auth successful, update token and retry request 109 + req.Header.Set("Authorization", "Bearer "+c.accessToken) 110 + 111 + // Reset request body if needed 112 + if req.GetBody != nil { 113 + bodyRC, err := req.GetBody() 114 + if err != nil { 115 + return nil, fmt.Errorf("failed to reset request body: %w", err) 116 + } 117 + req.Body = bodyRC 118 + } 119 + 120 + return c.client.Do(req) 121 + } 122 + 123 + // Restore body for caller if not expired token 124 + resp.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) 125 + } 126 + 127 + return resp, nil 128 + } 129 + 76 130 // Notification represents a Bluesky notification 77 131 type Notification struct { 78 - URI string `json:"uri"` 79 - Cid string `json:"cid"` 80 - Author Actor `json:"author"` 81 - Reason string `json:"reason"` 82 - ReasonSubject string `json:"reasonSubject,omitempty"` 83 - Record any `json:"record"` 84 - IsRead bool `json:"isRead"` 85 - IndexedAt time.Time `json:"indexedAt"` 132 + URI string `json:"uri"` 133 + Cid string `json:"cid"` 134 + Author Actor `json:"author"` 135 + Reason string `json:"reason"` 136 + ReasonSubject string `json:"reasonSubject,omitempty"` 137 + Record any `json:"record"` 138 + IsRead bool `json:"isRead"` 139 + IndexedAt time.Time `json:"indexedAt"` 86 140 } 87 141 88 142 // Actor represents a Bluesky actor ··· 103 157 return nil, "", fmt.Errorf("failed to create request: %w", err) 104 158 } 105 159 106 - req.Header.Set("Authorization", "Bearer "+c.accessToken) 107 - 108 - resp, err := c.client.Do(req) 160 + resp, err := c.doRequest(req) 109 161 if err != nil { 110 162 return nil, "", fmt.Errorf("request failed: %w", err) 111 163 } ··· 130 182 131 183 // ProfileRecord represents a user profile record 132 184 type ProfileRecord struct { 133 - DisplayName string `json:"displayName,omitempty"` 134 - Description string `json:"description,omitempty"` 135 - Avatar *BlobRef `json:"avatar,omitempty"` 136 - Banner *BlobRef `json:"banner,omitempty"` 185 + DisplayName string `json:"displayName,omitempty"` 186 + Description string `json:"description,omitempty"` 187 + Avatar *BlobRef `json:"avatar,omitempty"` 188 + Banner *BlobRef `json:"banner,omitempty"` 137 189 } 138 190 139 191 // BlobRef represents a blob reference ··· 159 211 return nil, fmt.Errorf("failed to create request: %w", err) 160 212 } 161 213 162 - req.Header.Set("Authorization", "Bearer "+c.accessToken) 163 - 164 - resp, err := c.client.Do(req) 214 + resp, err := c.doRequest(req) 165 215 if err != nil { 166 216 return nil, fmt.Errorf("request failed: %w", err) 167 217 } ··· 202 252 return nil, fmt.Errorf("failed to create request: %w", err) 203 253 } 204 254 205 - req.Header.Set("Authorization", "Bearer "+c.accessToken) 206 255 req.Header.Set("Content-Type", mimeType) 207 256 208 - resp, err := c.client.Do(req) 257 + resp, err := c.doRequest(req) 209 258 if err != nil { 210 259 return nil, fmt.Errorf("request failed: %w", err) 211 260 } ··· 248 297 return fmt.Errorf("failed to create request: %w", err) 249 298 } 250 299 251 - req.Header.Set("Authorization", "Bearer "+c.accessToken) 252 300 req.Header.Set("Content-Type", "application/json") 253 301 254 - resp, err := c.client.Do(req) 302 + resp, err := c.doRequest(req) 255 303 if err != nil { 256 304 return fmt.Errorf("request failed: %w", err) 257 305 }