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_media_records, generate_quote_records, generate_webcard_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 toot_tags: Vec = match &config.oolatoocs.remove_hashtags { true => toot.tags.clone(), false => vec![], }; let Ok(mut tweet_content) = strip_everything(&toot.content, &toot_tags, &config.mastodon.base) 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() }), }); }; // handle quote if any let quote_embed = match toot.reblog { Some(r) => { let quote_record = read_state(&conn, Some(r.id.parse::().unwrap())); match quote_record { Ok(Some(q)) => generate_quote_records(&config.bluesky, &q.record_uri) .await .ok(), _ => None, } } None => None, }; // handle medias if any let media_embed = if toot.media_attachments.len() > usize::from(0u8) { generate_media_records(&bluesky, &toot.media_attachments) .await .ok() } else { None }; // handle webcard if any let webcard_embed = match toot.card { Some(t) => generate_webcard_records(&bluesky, &t).await.ok(), None => None, }; let record_embed = if quote_embed.is_some() { if media_embed.is_some() { let medias_mapped = match media_embed.unwrap() { atrium_api::app::bsky::feed::post::RecordEmbedRefs::AppBskyEmbedImagesMain(a) => atrium_api::app::bsky::embed::record_with_media::MainMediaRefs::AppBskyEmbedImagesMain(a), atrium_api::app::bsky::feed::post::RecordEmbedRefs::AppBskyEmbedVideoMain(a) => atrium_api::app::bsky::embed::record_with_media::MainMediaRefs::AppBskyEmbedVideoMain(a), _ => continue, // this should NEVER happen as Media are either Video or // Images at this point }; let quote_mapped = match quote_embed.unwrap() { atrium_api::app::bsky::feed::post::RecordEmbedRefs::AppBskyEmbedRecordMain( a, ) => a, _ => continue, // again, this should NEVER happen }; Some(atrium_api::types::Union::Refs( atrium_api::app::bsky::feed::post::RecordEmbedRefs::AppBskyEmbedRecordWithMediaMain( Box::new( atrium_api::app::bsky::embed::record_with_media::MainData { media: atrium_api::types::Union::Refs(medias_mapped), record: (*quote_mapped), }.into() ) ) )) } else { quote_embed.map(atrium_api::types::Union::Refs) } } else if media_embed.is_some() { media_embed.map(atrium_api::types::Union::Refs) } else if webcard_embed.is_some() { webcard_embed.map(atrium_api::types::Union::Refs) } else { None }; // 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)); } }