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.

Process all unprocessed notifications by tracking individual URIs

Brooke 1996b120 49b5c748

+97 -43
+59 -38
cmd/brooke-spin/main.go
··· 109 109 return fmt.Errorf("failed to fetch notifications: %w", err) 110 110 } 111 111 112 - // Filter for unread notifications that match configured types 112 + // Filter for unprocessed notifications that match configured types 113 113 var relevantNotifications []client.Notification 114 114 for _, notif := range notifications { 115 - if !notif.IsRead && cfg.ShouldProcessNotification(notif.Reason) { 115 + // Skip if already processed 116 + if currentState.ProcessedNotifications[notif.URI] { 117 + continue 118 + } 119 + // Check if notification type matches configured filters 120 + if cfg.ShouldProcessNotification(notif.Reason) { 116 121 relevantNotifications = append(relevantNotifications, notif) 117 122 } 118 123 } 119 124 120 125 if len(relevantNotifications) == 0 { 121 - // No new relevant notifications 126 + // Update cursor even if no relevant notifications to process 127 + if newCursor != "" && newCursor != currentState.LastNotificationCursor { 128 + currentState.LastNotificationCursor = newCursor 129 + if err := stateManager.Save(currentState); err != nil { 130 + log.Printf("Warning: failed to save cursor: %v", err) 131 + } 132 + } 122 133 return nil 123 134 } 124 135 125 136 log.Printf("Found %d new relevant notification(s)", len(relevantNotifications)) 126 137 127 - // Get current profile 128 - profile, err := blueskyClient.GetProfile() 129 - if err != nil { 130 - return fmt.Errorf("failed to get profile: %w", err) 131 - } 138 + // Process each notification separately 139 + for i, notif := range relevantNotifications { 140 + log.Printf("Processing notification %d/%d (from @%s, type: %s)", i+1, len(relevantNotifications), notif.Author.Handle, notif.Reason) 141 + 142 + // Get current profile 143 + profile, err := blueskyClient.GetProfile() 144 + if err != nil { 145 + return fmt.Errorf("failed to get profile: %w", err) 146 + } 147 + 148 + // Get avatar URL 149 + avatarURL, err := blueskyClient.GetAvatarURL(profile) 150 + if err != nil { 151 + return fmt.Errorf("failed to get avatar URL: %w", err) 152 + } 132 153 133 - // Get avatar URL 134 - avatarURL, err := blueskyClient.GetAvatarURL(profile) 135 - if err != nil { 136 - return fmt.Errorf("failed to get avatar URL: %w", err) 137 - } 154 + // Download current avatar 155 + log.Println("Downloading current avatar...") 156 + avatarData, err := imageProcessor.DownloadImage(avatarURL) 157 + if err != nil { 158 + return fmt.Errorf("failed to download avatar: %w", err) 159 + } 160 + 161 + // Rotate image 162 + log.Printf("Rotating avatar by %.2f degrees...", cfg.Rotation.Degrees) 163 + rotatedData, format, err := imageProcessor.RotateImage(avatarData, cfg.Rotation.Degrees) 164 + if err != nil { 165 + return fmt.Errorf("failed to rotate image: %w", err) 166 + } 138 167 139 - // Download current avatar 140 - log.Println("Downloading current avatar...") 141 - avatarData, err := imageProcessor.DownloadImage(avatarURL) 142 - if err != nil { 143 - return fmt.Errorf("failed to download avatar: %w", err) 144 - } 168 + // Upload new avatar 169 + log.Println("Uploading rotated avatar...") 170 + blobRef, err := blueskyClient.UploadBlob(rotatedData, image.GetMimeType(format)) 171 + if err != nil { 172 + return fmt.Errorf("failed to upload blob: %w", err) 173 + } 145 174 146 - // Rotate image 147 - log.Printf("Rotating avatar by %.2f degrees...", cfg.Rotation.Degrees) 148 - rotatedData, format, err := imageProcessor.RotateImage(avatarData, cfg.Rotation.Degrees) 149 - if err != nil { 150 - return fmt.Errorf("failed to rotate image: %w", err) 151 - } 175 + // Update profile with new avatar 176 + profile.Avatar = blobRef 177 + log.Println("Updating profile...") 178 + if err := blueskyClient.UpdateProfile(profile); err != nil { 179 + return fmt.Errorf("failed to update profile: %w", err) 180 + } 152 181 153 - // Upload new avatar 154 - log.Println("Uploading rotated avatar...") 155 - blobRef, err := blueskyClient.UploadBlob(rotatedData, image.GetMimeType(format)) 156 - if err != nil { 157 - return fmt.Errorf("failed to upload blob: %w", err) 158 - } 182 + // Update state for this notification 183 + currentState.CumulativeRotation += cfg.Rotation.Degrees 184 + currentState.ProcessedNotifications[notif.URI] = true 159 185 160 - // Update profile with new avatar 161 - profile.Avatar = blobRef 162 - log.Println("Updating profile...") 163 - if err := blueskyClient.UpdateProfile(profile); err != nil { 164 - return fmt.Errorf("failed to update profile: %w", err) 186 + log.Printf("✓ Notification %d/%d processed! (Total rotation: %.2f°)", i+1, len(relevantNotifications), currentState.CumulativeRotation) 165 187 } 166 188 167 189 // Update state 168 190 currentState.LastNotificationCursor = newCursor 169 191 currentState.LastProcessedAt = time.Now().Format(time.RFC3339) 170 - currentState.CumulativeRotation += cfg.Rotation.Degrees 171 192 172 193 // Normalize cumulative rotation to 0-360 range 173 194 currentState.CumulativeRotation = math.Mod(currentState.CumulativeRotation, 360) ··· 179 200 return fmt.Errorf("failed to save state: %w", err) 180 201 } 181 202 182 - log.Printf("✓ Profile picture rotated successfully! (Total rotation: %.2f°)", currentState.CumulativeRotation) 203 + log.Printf("✓ All %d notification(s) processed! (Total rotation: %.2f°)", len(relevantNotifications), currentState.CumulativeRotation) 183 204 184 205 return nil 185 206 }
+38 -5
internal/state/manager.go
··· 21 21 22 22 // State represents the application state 23 23 type State struct { 24 - LastNotificationCursor string `json:"last_notification_cursor"` 25 - LastProcessedAt string `json:"last_processed_at"` 26 - CumulativeRotation float64 `json:"cumulative_rotation"` 24 + LastNotificationCursor string `json:"last_notification_cursor"` 25 + LastProcessedAt string `json:"last_processed_at"` 26 + CumulativeRotation float64 `json:"cumulative_rotation"` 27 + ProcessedNotifications map[string]bool `json:"processed_notifications"` // Track URIs we've processed 27 28 } 28 29 29 30 // NewManager creates a new state manager ··· 95 96 96 97 // loadFromSQLite loads state from SQLite database 97 98 func (m *Manager) loadFromSQLite() (*State, error) { 98 - state := &State{} 99 + state := &State{ 100 + ProcessedNotifications: make(map[string]bool), 101 + } 99 102 100 103 row := m.db.QueryRow("SELECT value FROM state WHERE key = ?", "last_notification_cursor") 101 104 var cursor string ··· 118 121 } 119 122 if rotation != "" { 120 123 fmt.Sscanf(rotation, "%f", &state.CumulativeRotation) 124 + } 125 + 126 + row = m.db.QueryRow("SELECT value FROM state WHERE key = ?", "processed_notifications") 127 + var processedJSON string 128 + if err := row.Scan(&processedJSON); err != nil && err != sql.ErrNoRows { 129 + return nil, fmt.Errorf("failed to load processed_notifications: %w", err) 130 + } 131 + if processedJSON != "" { 132 + if err := json.Unmarshal([]byte(processedJSON), &state.ProcessedNotifications); err != nil { 133 + return nil, fmt.Errorf("failed to parse processed_notifications: %w", err) 134 + } 121 135 } 122 136 123 137 return state, nil ··· 152 166 return fmt.Errorf("failed to save cumulative_rotation: %w", err) 153 167 } 154 168 169 + processedJSON, err := json.Marshal(state.ProcessedNotifications) 170 + if err != nil { 171 + return fmt.Errorf("failed to marshal processed_notifications: %w", err) 172 + } 173 + 174 + _, err = tx.Exec(` 175 + INSERT OR REPLACE INTO state (key, value) VALUES (?, ?) 176 + `, "processed_notifications", string(processedJSON)) 177 + if err != nil { 178 + return fmt.Errorf("failed to save processed_notifications: %w", err) 179 + } 180 + 155 181 return tx.Commit() 156 182 } 157 183 ··· 160 186 data, err := os.ReadFile(m.path) 161 187 if err != nil { 162 188 if os.IsNotExist(err) { 163 - return &State{}, nil 189 + return &State{ 190 + ProcessedNotifications: make(map[string]bool), 191 + }, nil 164 192 } 165 193 return nil, fmt.Errorf("failed to read state file: %w", err) 166 194 } ··· 168 196 var state State 169 197 if err := json.Unmarshal(data, &state); err != nil { 170 198 return nil, fmt.Errorf("failed to parse state file: %w", err) 199 + } 200 + 201 + // Initialize map if nil 202 + if state.ProcessedNotifications == nil { 203 + state.ProcessedNotifications = make(map[string]bool) 171 204 } 172 205 173 206 return &state, nil