brooke-spin#
A background service that automatically rotates your Bluesky profile picture by a configurable number of degrees each time you receive a notification.
Features#
- Automated Profile Picture Rotation: Rotates your avatar every time you get likes, reposts, replies, follows, mentions, or quotes
- Configurable Rotation: Set your preferred rotation angle (default: 2 degrees)
- Selective Notifications: Choose which notification types trigger rotation
- State Persistence: Tracks processed notifications to avoid duplicates
- Docker Support: Runs as a lightweight container on Raspberry Pi (ARM64/ARMv7)
- Graceful Shutdown: Handles SIGTERM/SIGINT properly
- Secure: App password can be set via environment variable
Prerequisites#
- A Bluesky account
- An app password from Bluesky Settings > App Passwords
- Docker and Docker Compose (for containerized deployment)
- OR Go 1.22+ (for local development)
Quick Start#
1. Clone the Repository#
git clone <repository-url>
cd brooke-spin
2. Create Configuration#
Copy the example configuration and edit it:
cp config.example.yaml config.yaml
nano config.yaml
Update the following fields:
bluesky.handle: Your Bluesky handle (e.g., username.bsky.social)bluesky.app_password: Your app password (or set via environment variable)rotation.degrees: Rotation angle per notification (default: 2.0)
3. Deploy with Docker#
# Build and start the service
docker compose up -d
# View logs
docker compose logs -f
# Stop the service
docker compose down
4. Monitor#
Watch the logs to see it working:
docker compose logs -f brooke-spin
You should see messages like:
Starting brooke-spin service...
Authenticating with Bluesky...
Authentication successful!
Polling for notifications every 30s
Found 2 new relevant notification(s)
Downloading current avatar...
Rotating avatar by 2.00 degrees...
Uploading rotated avatar...
Updating profile...
✓ Profile picture rotated successfully! (Total rotation: 42.00°)
Configuration#
Configuration File (config.yaml)#
bluesky:
handle: "yourhandle.bsky.social"
app_password: "xxxx-xxxx-xxxx-xxxx"
rotation:
degrees: 2.0 # Rotation per notification
polling:
interval: "30s" # How often to check for notifications
notifications:
types:
like: true
repost: true
reply: true
follow: true
mention: true
quote: true
state:
storage: "sqlite" # or "json"
path: "./data/state.db"
Environment Variables#
You can override configuration values with environment variables:
BLUESKY_APP_PASSWORD: Override the app password (recommended for Docker)
Example:
export BLUESKY_APP_PASSWORD="your-app-password"
docker compose up -d
Or in docker compose.yml:
environment:
- BLUESKY_APP_PASSWORD=${BLUESKY_APP_PASSWORD}
Advanced Usage#
Running Locally (Without Docker)#
# Install dependencies
go mod download
# Build
go build -o brooke-spin ./cmd/brooke-spin
# Run
./brooke-spin -config config.yaml
Building for Raspberry Pi#
# Using Docker buildx for ARM64
docker buildx build --platform linux/arm64 -t brooke-spin:latest .
# Or build natively on Raspberry Pi
go build -o brooke-spin ./cmd/brooke-spin
Custom Polling Interval#
Adjust how often the service checks for new notifications:
polling:
interval: "1m" # Check every minute
# interval: "30s" # Check every 30 seconds (default)
# interval: "5m" # Check every 5 minutes
Selective Notification Types#
Only rotate on specific interactions:
notifications:
types:
like: true # Rotate on likes
repost: false # Ignore reposts
reply: true # Rotate on replies
follow: true # Rotate on follows
mention: false # Ignore mentions
quote: true # Rotate on quotes
Change Rotation Direction#
Use negative degrees to rotate counter-clockwise:
rotation:
degrees: -2.0 # Rotate 2 degrees counter-clockwise
State Storage Options#
Choose between SQLite (default) or JSON:
state:
storage: "sqlite" # Recommended: atomic writes, queryable
path: "./data/state.db"
# OR use JSON (simpler, but less robust)
# storage: "json"
# path: "./data/state.json"
Deployment on Raspberry Pi#
1. Copy Files to Pi#
# From your local machine
scp -r brooke-spin/ pi@raspberrypi.local:~/
2. SSH to Pi and Deploy#
ssh pi@raspberrypi.local
cd ~/brooke-spin
# Edit configuration
nano config.yaml
# Start the service
docker compose up -d
# Check logs
docker compose logs -f
3. Auto-Start on Boot#
The service will automatically restart with restart: unless-stopped in docker compose.yml.
To ensure Docker Compose starts on boot:
# Enable Docker service
sudo systemctl enable docker
# Add to crontab
crontab -e
# Add this line:
@reboot cd /home/pi/brooke-spin && docker compose up -d
Troubleshooting#
Authentication Failed#
- Double-check your handle and app password
- Make sure you're using an app password, not your main account password
- Generate a new app password from Bluesky Settings > App Passwords
Profile Picture Not Rotating#
- Check logs:
docker compose logs -f - Verify you're receiving notifications (test by liking one of your own posts from another account)
- Ensure notification types are enabled in config
- Check that your profile has an avatar set
Container Won't Start#
# Check container status
docker compose ps
# View full logs
docker compose logs
# Rebuild the container
docker compose down
docker compose build --no-cache
docker compose up -d
State Not Persisting#
- Ensure the
./datadirectory exists and is writable - Check volume mounts in
docker compose.yml - Verify the state path in
config.yamlmatches the mounted volume
Development#
Project Structure#
brooke-spin/
├── cmd/
│ └── brooke-spin/
│ └── main.go # Entry point
├── internal/
│ ├── config/
│ │ └── config.go # Configuration loading
│ ├── client/
│ │ └── bluesky.go # Bluesky API client
│ ├── image/
│ │ └── processor.go # Image rotation
│ └── state/
│ └── manager.go # State persistence
├── config.example.yaml # Example configuration
├── docker compose.yml # Docker Compose config
├── Dockerfile # Docker image definition
├── Makefile # Build helpers
└── README.md # This file
Running Tests#
make test
Building#
# Local build
make build
# Docker build
make docker-build
# Docker build for ARM (Raspberry Pi)
make docker-build-arm
How It Works#
- Polling: Every N seconds (configurable), the service polls the Bluesky notifications API
- Filtering: Unread notifications are filtered by configured types (like, repost, etc.)
- Detection: If new relevant notifications are found, the rotation process begins
- Download: Current profile picture is downloaded from Bluesky
- Rotation: Image is rotated by configured degrees using Go image libraries
- Upload: Rotated image is uploaded as a new blob
- Update: Profile is updated with the new avatar blob reference
- State: Notification cursor and cumulative rotation are saved to disk
Security#
- Never commit
config.yaml: It's in.gitignoreby default - Use environment variables: Set
BLUESKY_APP_PASSWORDinstead of storing in config - App passwords: Always use app passwords, never your main account password
- Non-root container: Docker image runs as non-root user for security
License#
MIT License - See LICENSE file for details
Contributing#
Contributions welcome! Please open an issue or pull request.
Credits#
Built with:
- Go
- disintegration/imaging for image processing
- mattn/go-sqlite3 for state persistence
- gopkg.in/yaml.v3 for configuration
Support#
Having issues? Please open an issue on GitHub with:
- Your configuration (without passwords!)
- Log output
- Expected vs actual behavior