This repository has no description
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(¤t_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 = ¤t_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}