use log::debug; mod error; pub use error::OolatoocsError; mod config; pub use config::{parse_toml, Config}; mod state; use state::{delete_state, read_all_state, read_state, write_state, TootRecord}; pub use state::{init_db, migrate_db}; mod mastodon; pub use mastodon::register; use mastodon::{get_mastodon_instance, get_mastodon_timeline_since, get_status_edited_at}; mod utils; use utils::{generate_multi_tweets, strip_everything}; mod bsky; use bsky::{ build_post_record, generate_embed_records, generate_media_records, get_session, BskyReply, }; use rusqlite::Connection; #[tokio::main] pub async fn run(config: &Config) { let conn = Connection::open(&config.oolatoocs.db_path) .unwrap_or_else(|e| panic!("Cannot open DB: {}", e)); let mastodon = get_mastodon_instance(&config.mastodon) .unwrap_or_else(|e| panic!("Cannot instantiate Mastodon: {}", e)); let bluesky = get_session(&config.bluesky) .await .unwrap_or_else(|e| panic!("Cannot get Bsky session: {}", e)); let last_entry = read_state(&conn, None).unwrap_or_else(|e| panic!("Cannot get last toot id: {}", e)); let last_toot_id: Option = match last_entry { None => None, // Does not exist, this is the same as previously Some(t) => { match get_status_edited_at(&mastodon, t.toot_id).await { None => Some(t.toot_id), Some(d) => { // a date has been found if d > t.datetime.unwrap() { debug!("Last toot date is posterior to the previously written tweet, deleting…"); let local_record_uris = read_all_state(&conn, t.toot_id).unwrap_or_else(|e| { panic!( "Cannot fetch all records associated with Toot ID {}: {}", t.toot_id, e ) }); for local_record_uri in local_record_uris.into_iter() { bluesky .delete_record(&local_record_uri) .await .unwrap_or_else(|e| { panic!("Cannot delete record ID ({}): {}", &t.record_uri, e) }); } delete_state(&conn, t.toot_id).unwrap_or_else(|e| { panic!("Cannot delete Toot ID ({}): {}", t.toot_id, e) }); read_state(&conn, None) .unwrap_or_else(|e| panic!("Cannot get last toot id: {}", e)) .map(|a| a.toot_id) } else { Some(t.toot_id) } } } } }; let timeline = get_mastodon_timeline_since(&mastodon, last_toot_id) .await .unwrap_or_else(|e| panic!("Cannot get instance: {}", e)); for toot in timeline { // detecting tag #NoTweet and skipping the toot if toot.tags.iter().any(|f| &f.name == "notweet") { continue; } // form tweet_content and strip everything useless in it let Ok(mut tweet_content) = strip_everything(&toot.content, &toot.tags) else { continue; // skip in case we can’t strip something }; // threads if necessary let mut record_reply_to = toot.in_reply_to_id.and_then(|t| { read_state(&conn, Some(t.parse::().unwrap())) .ok() .flatten() .map(|s| BskyReply { record_uri: s.record_uri.to_owned(), root_record_uri: s.root_record_uri.to_owned(), }) }); // if the toot is too long, we cut it in half here if let Some((first_half, second_half)) = generate_multi_tweets(&tweet_content) { tweet_content = second_half; // post the first half let record = build_post_record( &config.bluesky, &first_half, &toot.language, None, &record_reply_to, ) .await .unwrap_or_else(|e| panic!("Cannot create valid record for {}: {}", &toot.id, e)); let record_reply_id = bluesky.create_record(record).await.unwrap_or_else(|e| { panic!( "Cannot post the first half of {} for Bluesky: {}", &toot.id, e ) }); // write it to db write_state( &conn, TootRecord { toot_id: toot.id.parse::().unwrap(), record_uri: record_reply_id.data.uri.to_owned(), root_record_uri: record_reply_to .as_ref() .map_or(record_reply_id.data.uri.to_owned(), |v| { v.root_record_uri.to_owned() }), datetime: None, }, ) .unwrap_or_else(|e| { panic!( "Cannot store Toot/Tweet/Record ({}/{}): {}", &toot.id, &record_reply_id.data.uri, e ) }); record_reply_to = Some(BskyReply { record_uri: record_reply_id.data.uri.to_owned(), root_record_uri: record_reply_to .as_ref() .map_or(record_reply_id.data.uri.clone(), |v| { v.root_record_uri.clone() }), }); }; // treats medias let mut record_embed = generate_media_records(&bluesky, &toot.media_attachments).await; // treats embed cards if any if let Some(card) = &toot.card { if record_embed.is_none() { record_embed = generate_embed_records(&bluesky, card).await; } } // posts corresponding tweet let record = build_post_record( &config.bluesky, &tweet_content, &toot.language, record_embed, &record_reply_to, ) .await .unwrap_or_else(|e| panic!("Cannot build record for {}: {}", &toot.id, e)); let created_record = bluesky .create_record(record) .await .unwrap_or_else(|e| panic!("Cannot put record {}: {}", &toot.id, e)); // writes the current state of the tweet write_state( &conn, TootRecord { toot_id: toot.id.parse::().unwrap(), record_uri: created_record.data.uri.clone(), root_record_uri: record_reply_to .as_ref() .map_or(created_record.data.uri.clone(), |v| { v.root_record_uri.clone() }), datetime: None, }, ) .unwrap_or_else(|e| panic!("Cannot store Toot/Tweet ({}): {}", &toot.id, e)); } }