···2233## Overview
4455-This repository contains a Python script intended to update your Bluesky avatar automatically based on the current hour. The script utilises environment variables for configuration and reads a JSON file of blob CIDs to determine the appropriate avatar for each hour. Please note that the implementation is not yet fully operational, as several issues remain to be resolved. This script was inspired by [@dame.is](https://bsky.app/profile/dame.is)'s blog post ['How I made an automated dynamic avatar for my Bluesky profile'](https://dame.is/blog/how-i-made-an-automated-dynamic-avatar-for-my-bluesky-profile).
55+This repository contains a Python script designed to automatically update your Bluesky avatar based on the current hour. The script leverages environment variables for configuration and reads a JSON file of blob CIDs to determine the appropriate avatar. This script was inspired by [@dame.is](https://bsky.app/profile/dame.is)'s blog post ['How I made an automated dynamic avatar for my Bluesky profile'](https://dame.is/blog/how-i-made-an-automated-dynamic-avatar-for-my-bluesky-profile).
66+77+The script has been tested and is fully functional. It was developed on macOS but is intended for deployment on Linux.
6879## Prerequisites
810911Before running the script, ensure you have the following:
10121113- Python 3.6 or later installed.
1212-1313-- The required Python packages:
1414+- The required Python packages (automatically installed if missing):
1415 - `python-dotenv`
1516 - `atproto`
1616- - Standard libraries such as `os`, `json`, `logging`, and `datetime`
1717+ - `requests`
1818+ - `python-magic`
1719- A valid Bluesky account with the necessary API credentials.
18201921## Installation
···4143 ENDPOINT=your_endpoint
4244 HANDLE=your_handle
4345 PASSWORD=your_password
4646+ DID=your_did
4447 ```
454846494. **Prepare the JSON file:**
4747- - Ensure that a `cids.json` file is located in the `../assets` directory. This file should map each hour (in two-digit format) to a corresponding blob CID.
5050+ - Ensure that a `cids.json` file is located in the `../assets` directory. This file should map each hour (in two-digit format) to a corresponding blob CID. Example:
5151+5252+ ```json
5353+ {
5454+ "00": "cid_for_midnight",
5555+ "01": "cid_for_1am",
5656+ "02": "cid_for_2am"
5757+ }
5858+ ```
48594960## Usage
5061···56675768The script will:
58695959-- Load the environment configuration from `./assets/.env`.
6060-- Read the blob CIDs from `./assets/cids.json`.
7070+- Load the environment configuration from `../assets/.env`.
7171+- Read the blob CIDs from `../assets/cids.json`.
6172- Determine the current hour and select the appropriate blob CID.
6262-- Attempt to authenticate and update the avatar using the AT Protocol.
7373+- Authenticate using the AT Protocol.
7474+- Update the Bluesky avatar accordingly.
7575+7676+Execution logs will be recorded in `logs/avatar_update.log` for your review.
7777+7878+## Automating with Cron (Linux)
63796464-Execution logs will be recorded in `avatar_update.log` for your review.
8080+To run the script automatically every hour, a cron job is set up within the script. If you need to manually verify it, run:
65816666-## Known Issues
8282+```bash
8383+crontab -l
8484+```
8585+8686+If you need to remove or modify the cron job, use:
8787+8888+```bash
8989+crontab -e
9090+```
67916868-At present, the script isn’t fully working. We’ve noticed an error when updating the profile—specifically, the `put_record()` method is missing a required parameter. There have also been occasional authentication hiccoughs, which might be due to configuration issues or [API](https://atproto.blue) quirks.
9292+## Troubleshooting
69937070-If you’re keen to help sort these out or have ideas for improvements, please open a Pull Request. Your contributions are very welcome!
9494+- **Environment variables not loading?** Ensure the `.env` file is correctly placed in `../assets/`.
9595+- **Script exits with missing dependencies?** The script will attempt to install missing packages, but you can manually install them using:
9696+9797+ ```bash
9898+ pip install -r requirements.txt
9999+ ```
711007272----
101101+- **Endpoint not responding?** Verify that the Bluesky API endpoint is correct and accessible.
7310274103## Licence
75104
+25-25
src/main.py
···11+#!/usr/bin/env python3
12import os
23import sys
34import subprocess
···78import magic
89from datetime import datetime
910from dotenv import load_dotenv
1111+from logging.handlers import RotatingFileHandler
1012from atproto import Client, models
1113from atproto.exceptions import BadRequestError
1214···2325ASSETS_DIR = os.path.join(BASE_DIR, "../assets")
2426ENV_PATH = os.path.join(ASSETS_DIR, ".env")
2527JSON_PATH = os.path.join(ASSETS_DIR, "cids.json")
2626-LOG_PATH = os.path.join(BASE_DIR, "avatar_update.log")
2828+LOG_PATH = os.path.join(BASE_DIR, "logs", "avatar_update.log")
2929+3030+# Ensure necessary directories exist
3131+os.makedirs(os.path.dirname(LOG_PATH), exist_ok=True)
3232+os.makedirs(ASSETS_DIR, exist_ok=True)
3333+3434+# Configure logging with log rotation (5MB per file, keeps last 5 logs)
3535+log_handler = RotatingFileHandler(LOG_PATH, maxBytes=5 * 1024 * 1024, backupCount=5)
3636+log_handler.setLevel(logging.DEBUG)
3737+log_formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
3838+log_handler.setFormatter(log_formatter)
27392828-# Configure logging
2929-logging.basicConfig(
3030- filename=LOG_PATH,
3131- level=logging.DEBUG,
3232- format="%(asctime)s - %(levelname)s - %(message)s",
3333-)
3440console_handler = logging.StreamHandler()
3541console_handler.setLevel(logging.INFO)
3636-formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
3737-console_handler.setFormatter(formatter)
3838-logging.getLogger().addHandler(console_handler)
4242+console_handler.setFormatter(log_formatter)
39434444+logging.basicConfig(level=logging.DEBUG, handlers=[log_handler, console_handler])
4545+logging.info("Starting script with log rotation enabled.")
40464147def install_and_rerun():
4248 """Install missing packages from requirements.txt and re-run the script."""
···5359 logging.error(f"requirements.txt not found at {REQ_PATH}, cannot install missing packages.")
5460 sys.exit(1)
55615656-5757-# Try to import external packages; if any are missing, install them.
6262+# Check for required packages and install if missing
5863try:
5964 import requests
6065 import magic
···6570 logging.error(f"Missing package(s): {e}")
6671 install_and_rerun()
67726868-6973def ensure_https(url):
7074 """Ensure the URL starts with https://"""
7175 if not url.startswith("http://") and not url.startswith("https://"):
···7377 if url.startswith("http://"):
7478 return "https://" + url[7:]
7579 return url
7676-77807881def is_endpoint_alive(url):
7982 """Check if the endpoint is alive by making a health check request."""
···9093 except requests.RequestException as e:
9194 logging.error(f"Health check failed for {health_url}: {e}")
9295 return False
9393-94969597def fetch_blob(did, cid, endpoint):
9698 """Fetch blob data from the given endpoint."""
···105107 logging.error(f"Failed to fetch blob {cid} for DID {did}: {e}")
106108 return None
107109108108-109110def get_blob_metadata(cid, did, endpoint):
110111 """Retrieve metadata for a given blob CID."""
111112 blob_data = fetch_blob(did, cid, endpoint)
···125126 "mimeType": mime_type,
126127 "size": size,
127128 }
128128-129129130130def setup_cron_job():
131131 """Set up a cron job to run the script hourly."""
···145145 logging.info("Cron job added successfully.")
146146 except Exception as e:
147147 logging.error(f"Failed to set up cron job: {e}")
148148-149148150149def main():
151150 logging.info("Starting avatar update script...")
152151153153- if os.path.exists(ENV_PATH):
154154- load_dotenv(ENV_PATH)
155155- logging.info("Loaded .env file successfully.")
156156- else:
152152+ if not os.path.exists(ENV_PATH):
157153 logging.error(f"Missing .env file at {ENV_PATH}")
158154 return
155155+ load_dotenv(ENV_PATH)
156156+ logging.info("Loaded .env file successfully.")
159157160158 endpoint = os.getenv("ENDPOINT")
161159 handle = os.getenv("HANDLE")
···169167 endpoint = ensure_https(endpoint)
170168 if not is_endpoint_alive(endpoint):
171169 logging.error(f"Endpoint {endpoint} is not responding.")
170170+ return
171171+172172+ if not os.path.exists(JSON_PATH) or os.path.getsize(JSON_PATH) == 0:
173173+ logging.error(f"Error: {JSON_PATH} is missing or empty.")
172174 return
173175174176 try:
···216218 except Exception as e:
217219 logging.error(f"Failed to update profile record: {e}")
218220219219-220221if __name__ == "__main__":
221222 setup_cron_job()
222222- main()
223223-223223+ main()