This repository has no description
0

Configure Feed

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

at main 6.2 kB View raw
1mod bsky; 2mod cron; 3mod utils; 4 5use anyhow::{Result, Context, anyhow}; 6use bsky_sdk::BskyAgent; 7use bsky_sdk::agent::config::Config; 8use chrono::Local; 9use dotenvy::dotenv; 10use serde::{Deserialize, Serialize}; 11use std::collections::HashMap; 12use std::env; 13use std::fs; 14use tracing::{info, error, warn}; 15use tracing_subscriber::{fmt, prelude::*, EnvFilter}; 16use atrium_api::app::bsky::actor::profile::{Record, RecordData}; 17use atrium_api::types::{BlobRef, TypedBlobRef, Unknown}; 18 19#[derive(Serialize, Deserialize, Debug)] 20struct HourEntry { 21 avatar: String, 22 banner: Option<String>, 23} 24 25#[tokio::main] 26async fn main() -> Result<()> { 27 // Define the paths 28 let base_dir = env::current_dir()?; 29 let assets_dir = base_dir.join("assets"); 30 let json_path = assets_dir.join("cids.json"); 31 let log_dir = base_dir.join("logs"); 32 33 if !log_dir.exists() { 34 fs::create_dir_all(&log_dir)?; 35 } 36 37 // Set up tracing 38 let file_appender = tracing_appender::rolling::daily(&log_dir, "update.log"); 39 let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender); 40 41 let filter = EnvFilter::try_from_default_env() 42 .unwrap_or_else(|_| EnvFilter::new("info")); 43 44 tracing_subscriber::registry() 45 .with(filter) 46 .with(fmt::layer().with_writer(std::io::stdout)) 47 .with(fmt::layer().with_writer(non_blocking).with_ansi(false)) 48 .init(); 49 50 info!("Script started."); 51 52 // Setup cron job 53 cron::setup_cron_job(); 54 55 // Load environment variables 56 dotenv().ok(); 57 // Also try loading from assets/.env if it exists (per original script) 58 let env_path = assets_dir.join(".env"); 59 if env_path.exists() { 60 dotenvy::from_path(&env_path).ok(); 61 info!("Loaded environment from {:?}", env_path); 62 } 63 64 let env_vars = match utils::validate_environment_variables() { 65 Some(v) => v, 66 None => return Ok(()), 67 }; 68 69 let endpoint = utils::ensure_https(&env_vars.endpoint); 70 if !utils::is_endpoint_alive(&endpoint).await { 71 error!("Endpoint {} is not responding.", endpoint); 72 return Ok(()); 73 } 74 75 // Load CID mapping 76 let blob_dict: HashMap<String, HourEntry> = if json_path.exists() { 77 let content = fs::read_to_string(&json_path)?; 78 serde_json::from_str(&content).context("Failed to parse cids.json")? 79 } else { 80 error!("Missing cids.json at {:?}", json_path); 81 return Ok(()); 82 }; 83 84 let current_hour = Local::now().format("%H").to_string(); 85 info!("Current hour: {}", current_hour); 86 87 let current_entry = match blob_dict.get(&current_hour) { 88 Some(entry) => entry, 89 None => { 90 warn!("No entry found for hour {} in cids.json", current_hour); 91 return Ok(()); 92 } 93 }; 94 95 let new_avatar_cid = &current_entry.avatar; 96 let new_banner_cid = current_entry.banner.as_ref(); 97 98 info!("Selected avatar CID: {}", new_avatar_cid); 99 if env_vars.update_banner { 100 if let Some(bcid) = new_banner_cid { 101 info!("Selected banner CID: {}", bcid); 102 } 103 } 104 105 // Authenticate 106 let agent = BskyAgent::builder() 107 .config(Config { 108 endpoint: endpoint.clone(), 109 ..Default::default() 110 }) 111 .build() 112 .await?; 113 114 agent.login(env_vars.handle.clone(), env_vars.password.clone()).await?; 115 info!("Authentication successful."); 116 117 // Fetch current profile 118 let me = agent.api.com.atproto.repo.get_record( 119 atrium_api::com::atproto::repo::get_record::ParametersData { 120 collection: "app.bsky.actor.profile".parse().map_err(|e| anyhow!("{:?}", e))?, 121 repo: env_vars.did.clone().parse().map_err(|e| anyhow!("{:?}", e))?, 122 rkey: "self".parse().map_err(|e| anyhow!("{:?}", e))?, 123 cid: None, 124 }.into() 125 ).await; 126 127 let (mut current_record_data, swap_record_cid) = match me { 128 Ok(output) => { 129 let record = serde_json::from_value::<Record>(serde_json::to_value(&output.data.value)?)?; 130 (record.data, output.data.cid) 131 } 132 Err(e) => { 133 warn!("Failed to fetch current profile record: {:?}", e); 134 // Default empty record data 135 (RecordData { 136 avatar: None, 137 banner: None, 138 created_at: None, 139 description: None, 140 display_name: None, 141 joined_via_starter_pack: None, 142 labels: None, 143 pinned_post: None, 144 pronouns: None, 145 website: None, 146 }, None) 147 } 148 }; 149 150 // Update avatar 151 match bsky::get_blob_metadata(new_avatar_cid, &env_vars.did, &endpoint).await { 152 Ok(blob) => { 153 current_record_data.avatar = Some(BlobRef::Typed(TypedBlobRef::Blob(blob))); 154 } 155 Err(e) => { 156 error!("Could not retrieve metadata for avatar blob CID: {}. Error: {:?}", new_avatar_cid, e); 157 return Ok(()); 158 } 159 } 160 161 // Update banner if needed 162 if env_vars.update_banner { 163 if let Some(bcid) = new_banner_cid { 164 match bsky::get_blob_metadata(bcid, &env_vars.did, &endpoint).await { 165 Ok(blob) => { 166 current_record_data.banner = Some(BlobRef::Typed(TypedBlobRef::Blob(blob))); 167 } 168 Err(e) => { 169 warn!("Could not retrieve metadata for banner blob CID: {}. Error: {:?}", bcid, e); 170 } 171 } 172 } 173 } 174 175 // Put record back 176 agent.api.com.atproto.repo.put_record( 177 atrium_api::com::atproto::repo::put_record::InputData { 178 collection: "app.bsky.actor.profile".parse().map_err(|e| anyhow!("{:?}", e))?, 179 repo: env_vars.did.parse().map_err(|e| anyhow!("{:?}", e))?, 180 rkey: "self".parse().map_err(|e| anyhow!("{:?}", e))?, 181 record: serde_json::from_value::<Unknown>(serde_json::to_value(current_record_data)?)?, 182 swap_record: swap_record_cid, 183 validate: None, 184 swap_commit: None, 185 }.into() 186 ).await?; 187 188 info!("Profile updated successfully."); 189 190 Ok(()) 191}