This repository has no description
0

Configure Feed

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

add optional banner update functionality

+107 -86
+51 -56
README.md
··· 2 2 3 3 ## Overview 4 4 5 - This repository contains a Python script designed to automatically update your Bluesky avatar based on the current hour. The script uses 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). 5 + This repository contains a Python script that automatically updates your Bluesky avatar (and, optionally, your banner) based on the current hour. The script utilises environment variables for configuration and reads a JSON file mapping blob CIDs to specific hours. In addition to updating your avatar, the script performs several supportive functions including a health check of the API endpoint, comprehensive logging (both to console and to a rotating file system that deletes logs older than 30 days), and the automatic setup of a cron job to ensure regular updates. This project 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). 6 6 7 - The script has been tested and is fully functional. It was developed on macOS but is intended for deployment on Linux. 7 + Developed primarily on macOS and intended for Linux deployment, this tool is designed to run within a virtual environment to isolate dependencies and ensure smooth operation. 8 8 9 9 ## Prerequisites 10 10 11 - Before running the script, ensure you have the following: 11 + Before running the script, please ensure you have the following: 12 12 13 - - Python 3.6 or later installed. If using Ubuntu, run `sudo apt update && sudo apt install -y python3 python3-pip python3-dev` 14 - - The required Python packages (automatically installed if missing): 13 + - Python 3.6 or later installed. For Ubuntu, run: 14 + 15 + ```bash 16 + sudo apt update && sudo apt install -y python3 python3-pip python3-dev 17 + ``` 18 + 19 + - The following Python packages (automatically installed if missing): 15 20 - `python-dotenv` 16 21 - `atproto` 17 22 - `requests` 18 23 - `python-magic` 19 24 - `python-crontab` 20 25 - A valid Bluesky account with the necessary API credentials. 26 + - The script must be executed within a virtual environment. 21 27 22 28 ## Installation 23 29 24 - 1. **Clone the repository:** 30 + 1. **Clone the Repository:** 25 31 26 32 ```bash 27 33 git clone https://github.com/ewanc26/bluesky-avatar-updater.git 28 34 cd bluesky-avatar-updater 29 35 ``` 30 36 31 - 2. **Ensure the required package is installed (if necessary):** 32 - 33 - Before creating the virtual environment, make sure that the `python3-venv` package is installed (this is necessary on Debian/Ubuntu systems to create virtual environments). 37 + 2. **Ensure Virtual Environment Support:** 38 + On Debian/Ubuntu systems, ensure that the `python3-venv` package is installed: 34 39 35 40 ```bash 36 - sudo apt install python3.10-venv # Adjust the version if necessary (e.g., python3.9-venv) 41 + sudo apt install python3-venv # Adjust the version if necessary (e.g., python3.10-venv) 37 42 ``` 38 43 39 - 3. **Create a virtual environment and install dependencies:** 40 - 41 - Now, create a new virtual environment and activate it. This isolates the package dependencies for your project. 44 + 3. **Create and Activate a Virtual Environment:** 42 45 43 46 ```bash 44 47 python3 -m venv .venv 45 48 source .venv/bin/activate # On Windows use: .venv\Scripts\activate 46 49 ``` 47 50 48 - 4. **Install dependencies within the virtual environment:** 49 - 50 - With the virtual environment activated, install the required packages listed in the `requirements.txt` file: 51 + 4. **Install Dependencies:** 52 + With the virtual environment activated, install the required packages: 51 53 52 54 ```bash 53 55 pip install -r requirements.txt 54 56 ``` 55 57 56 - 5. **Configure environment variables:** 57 - - Place your `.env` file in the `../assets` directory relative to the script. 58 + 5. **Configure Environment Variables:** 59 + - Place your `.env` file in the `assets` directory. 58 60 - The `.env` file should contain the following entries: 59 61 60 62 ```env 61 63 ENDPOINT=your_endpoint 62 64 HANDLE=your_handle 63 - PASSWORD=your_password (app passwords are recommended) 65 + PASSWORD=your_password # App passwords are recommended 64 66 DID=your_did 67 + UPDATE_BANNER=true # Set to 'true' to update the banner, or 'false' otherwise 65 68 ``` 66 69 67 - 6. **Prepare the JSON file:** 68 - - 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: 70 + 6. **Prepare the JSON File:** 71 + Ensure that a `cids.json` file is located in the `assets` directory. This file should map each hour (in two-digit format) to the corresponding blob CIDs for the avatar (and optionally, the banner). For example: 69 72 70 - ```json 71 - { 72 - "00": "cid_for_midnight", 73 - "01": "cid_for_1am", 74 - "02": "cid_for_2am" 75 - } 76 - ``` 73 + ```json 74 + { 75 + "00": { "avatar": "cid_for_midnight", "banner": "banner_cid_for_midnight" }, 76 + "01": { "avatar": "cid_for_1am", "banner": "banner_cid_for_1am" } 77 + } 78 + ``` 77 79 78 80 ## Usage 79 81 ··· 83 85 python -u ./src/main.py 84 86 ``` 85 87 86 - The script will: 88 + Upon execution, the script will: 87 89 88 - - Load the environment configuration from `../assets/.env`. 89 - - Read the blob CIDs from `../assets/cids.json`. 90 - - Determine the current hour and select the appropriate blob CID. 91 - - Authenticate using the AT Protocol. 92 - - Update the Bluesky avatar accordingly. 93 - 94 - Execution logs will be displayed directly in the console, and a log file will be created in the `./logs/` directory. The log file will rotate every 14 days, keeping up to 5 backup log files. Logs older than 30 days will be automatically deleted. 90 + - Verify that it is running within a virtual environment. 91 + - Load environment variables from the `.env` file located in the `assets` directory. 92 + - Read the blob CIDs from the `cids.json` file. 93 + - Determine the current hour and select the appropriate blob CIDs. 94 + - Perform a health check on the provided API endpoint. 95 + - Authenticate using the AT Protocol and update your Bluesky profile with the new avatar (and banner, if enabled). 96 + - Automatically set up a cron job to run the script every hour. 97 + - Log activity to both the console and a rotating log file in the `logs` directory. The log file rotates every 14 days (with up to 5 backups) and automatically deletes files older than 30 days. 95 98 96 99 ## Automating with Cron (Linux) 97 100 98 - The script will automatically set up a cron job to run every hour. If you need to manually verify it, run: 101 + The script is designed to automatically configure a cron job to run at the top of every hour. To verify the cron job, use: 99 102 100 103 ```bash 101 104 crontab -l 102 105 ``` 103 106 104 - If you need to remove or modify the cron job, use: 105 - 106 - ```bash 107 - crontab -e 108 - ``` 109 - 110 - ### Manually Set Up Cron Job 111 - 112 - If you wish to manually set up the cron job instead of relying on the script, follow these steps: 107 + If you prefer to manually set up or modify the cron job, follow these steps: 113 108 114 109 1. Open the crontab editor: 115 110 ··· 117 112 crontab -e 118 113 ``` 119 114 120 - 2. Add the following line to run the script every hour at the top of the hour: 115 + 2. Add the following line (adjusting paths as necessary): 121 116 122 117 ```bash 123 118 0 * * * * /path/to/your/.venv/bin/python3 /path/to/bluesky-avatar-updater/src/main.py 124 119 ``` 125 - 126 - Replace `/path/to/your/.venv/bin/python3` with the path to your virtual environment's Python interpreter and `/path/to/bluesky-avatar-updater/src/main.py` with the full path to the `main.py` script. 127 120 128 121 ## Troubleshooting 129 122 130 - - **Environment variables not loading?** Ensure the `.env` file is correctly placed in `../assets/`. 131 - - **Script exits with missing dependencies?** The script will attempt to install missing packages, but you can manually install them using: 132 - 123 + - **Virtual Environment Issues:** The script will exit if it is not run within a virtual environment. Ensure you activate your virtual environment before running the script. 124 + - **Environment Variables Not Loading:** Verify that the `.env` file is correctly placed in the `assets` directory and contains all required entries. 125 + - **Missing Dependencies:** If the script encounters missing packages, run: 126 + 133 127 ```bash 134 128 pip install -r requirements.txt 135 129 ``` 136 130 137 - - **Endpoint not responding?** Verify that the Bluesky API endpoint is correct and accessible. 138 - - **Cron job not running?** Verify that the cron job was properly set up using `crontab -l` or set it up manually. 139 - - **Old logs not deleting?** Ensure the script has the necessary permissions to delete files in the `./logs/` directory. 131 + within your virtual environment. 132 + - **Endpoint Issues:** Ensure that the provided API endpoint is correct and accessible. The script performs a health check and will log an error if the endpoint is unresponsive. 133 + - **Cron Job Not Running:** If the cron job is not automatically set up, check with `crontab -l` or set it up manually using `crontab -e`. 134 + - **Log File Management:** The script manages log rotation and deletion automatically. If logs are not being deleted as expected, verify the file permissions in the `logs` directory. 140 135 141 136 ## License 142 137 143 - This project is released under the MIT License. See the [LICENSE](./LICENSE) file for full details. 138 + This project is released under the MIT License. Please refer to the [LICENSE](./LICENSE) file for full details.
+56 -30
src/main.py
··· 35 35 def cleanup_old_logs(log_directory, days=30): 36 36 """Deletes log files older than the specified number of days.""" 37 37 cutoff = time.time() - (days * 86400) # Convert days to seconds 38 - for log_file in glob.glob(os.path.join(log_directory, "avatar_update*.log")): 38 + for log_file in glob.glob(os.path.join(log_directory, "update.log")): 39 39 if os.path.isfile(log_file) and os.path.getmtime(log_file) < cutoff: 40 40 os.remove(log_file) 41 41 print(f"Deleted old log: {log_file}") ··· 44 44 cleanup_old_logs(log_dir, days=30) 45 45 46 46 # Use a fixed log file name for the current log; TimedRotatingFileHandler will manage rotations. 47 - log_file_path = os.path.join(log_dir, "avatar_update.log") 47 + log_file_path = os.path.join(log_dir, "update.log") 48 48 49 49 # Configure logging to both console and file with bi-weekly rotation 50 50 console_handler = logging.StreamHandler() ··· 77 77 78 78 # Suppress httpx logging (this stops httpx internal logs) 79 79 logging.getLogger("httpx").setLevel(logging.WARNING) # Suppress INFO and DEBUG logs from httpx 80 - 81 - # Log the start of the script 82 - logger.info("Avatar update script started.") 83 80 84 81 def ensure_https(url): 85 82 """Ensure the URL starts with https://.""" ··· 145 142 handle = os.getenv("HANDLE") 146 143 password = os.getenv("PASSWORD") 147 144 did = os.getenv("DID") 145 + update_banner = os.getenv("UPDATE_BANNER", "false").lower() == "true" 148 146 149 147 if not all([endpoint, handle, password, did]): 150 148 logger.error("Missing environment variables. Ensure ENDPOINT, HANDLE, PASSWORD, and DID are set in .env file.") ··· 153 151 "endpoint": endpoint, 154 152 "handle": handle, 155 153 "password": password, 156 - "did": did 154 + "did": did, 155 + "update_banner": update_banner 157 156 } 158 157 159 158 def setup_cron_job(): ··· 180 179 logger.info("Cron job already exists.") 181 180 182 181 def main(): 183 - """Main function to run the avatar update process.""" 182 + """Main function to run the avatar and banner update process.""" 184 183 # Set up the cron job (only once) 185 - setup_cron_job() 184 + try: 185 + setup_cron_job() 186 + except Exception as e: 187 + logger.error(f"Error setting cron job: {e}") 188 + pass 186 189 187 - logger.info("Starting avatar update process...") 190 + logger.info("Script started.") 191 + logger.info("Starting update process...") 188 192 189 193 # Load environment variables from the .env file 190 194 if os.path.exists(ENV_PATH): ··· 213 217 logger.error(f"Error loading cids.json from {JSON_PATH}: {e}") 214 218 return 215 219 216 - # Determine the blob CID for the current hour 220 + # Determine the blob CIDs for the current hour from the modified structure 217 221 current_hour = datetime.now().strftime("%H") 218 222 logger.info(f"Current hour: {current_hour}") 219 - 220 - new_blob_cid = blob_dict.get(current_hour) 221 - if not new_blob_cid: 222 - logger.warning(f"No blob CID found for hour {current_hour}") 223 + 224 + current_entry = blob_dict.get(current_hour) 225 + if not current_entry: 226 + logger.warning(f"No entry found for hour {current_hour} in cids.json") 223 227 return 224 228 225 - logger.info(f"Selected blob CID: {new_blob_cid}") 229 + new_avatar_cid = current_entry.get("avatar") 230 + new_banner_cid = current_entry.get("banner") if env_vars["update_banner"] else None 231 + 232 + if not new_avatar_cid: 233 + logger.warning(f"No avatar CID found for hour {current_hour}") 234 + return 235 + 236 + logger.info(f"Selected avatar CID: {new_avatar_cid}") 237 + if env_vars["update_banner"]: 238 + if new_banner_cid: 239 + logger.info(f"Selected banner CID: {new_banner_cid}") 240 + else: 241 + logger.warning(f"UPDATE_BANNER is enabled, but no banner CID found for hour {current_hour}") 226 242 227 243 # Authenticate with the endpoint 228 244 client = Client(endpoint) ··· 233 249 logger.error(f"Authentication failed: {e}") 234 250 return 235 251 236 - # Fetch the current profile and update it with the new avatar 252 + # Fetch the current profile and update it with the new avatar (and optionally banner) 237 253 try: 238 254 current_profile_record = client.app.bsky.actor.profile.get( 239 255 client.me.did, "self" 240 256 ) 241 257 current_profile = current_profile_record.value 242 258 swap_record_cid = current_profile_record.cid 243 - logger.info(f"Current profile record fetched successfully.") 259 + logger.info("Current profile record fetched successfully.") 244 260 except BadRequestError: 245 261 current_profile = swap_record_cid = None 246 - logger.warning(f"Failed to fetch current profile record.") 262 + logger.warning("Failed to fetch current profile record.") 247 263 248 - old_description = old_display_name = None 249 - if current_profile: 250 - old_description = current_profile.description 251 - old_display_name = current_profile.display_name 264 + old_description = current_profile.description if current_profile else None 265 + old_display_name = current_profile.display_name if current_profile else None 266 + old_banner = current_profile.banner if current_profile else None 252 267 253 - blob_metadata = get_blob_metadata(new_blob_cid, env_vars["did"], endpoint) 268 + avatar_metadata = get_blob_metadata(new_avatar_cid, env_vars["did"], endpoint) 269 + if avatar_metadata is None: 270 + logger.error(f"Could not retrieve metadata for avatar blob CID: {new_avatar_cid}") 271 + return 254 272 255 - if blob_metadata is None: 256 - logger.error(f"Could not retrieve metadata for blob CID: {new_blob_cid}") 257 - return 273 + banner_metadata = None 274 + if env_vars["update_banner"] and new_banner_cid: 275 + banner_metadata = get_blob_metadata(new_banner_cid, env_vars["did"], endpoint) 276 + if banner_metadata is None: 277 + logger.warning(f"Could not retrieve metadata for banner blob CID: {new_banner_cid}") 278 + banner_metadata = old_banner 279 + else: 280 + banner_metadata = old_banner 258 281 259 - # Update the profile with the new avatar 282 + # Update the profile with the new avatar and optionally the new banner 260 283 try: 261 284 client.com.atproto.repo.put_record( 262 285 models.ComAtprotoRepoPutRecord.Data( ··· 265 288 rkey="self", 266 289 swap_record=swap_record_cid, 267 290 record=models.AppBskyActorProfile.Record( 268 - avatar=blob_metadata, 269 - banner=current_profile.banner if current_profile else None, 291 + avatar=avatar_metadata, 292 + banner=banner_metadata, 270 293 description=old_description, 271 294 display_name=old_display_name, 272 295 ), 273 296 ) 274 297 ) 275 - logger.info(f"Avatar updated successfully with CID: {new_blob_cid}") 298 + if env_vars["update_banner"] and new_banner_cid: 299 + logger.info(f"Profile updated successfully with avatar CID {new_avatar_cid} and banner CID {new_banner_cid}") 300 + else: 301 + logger.info(f"Profile updated successfully with avatar CID: {new_avatar_cid}") 276 302 except Exception as e: 277 303 logger.error(f"Failed to update profile record: {e}") 278 304