mod error; use error::ScootalooError; mod config; pub use config::parse_toml; use config::Config; mod mastodon; pub use mastodon::register; use mastodon::{build_basic_status, get_mastodon_token}; mod twitter; use twitter::*; mod util; mod state; pub use state::init_db; use state::{read_state, write_state, TweetToToot}; use elefren::{prelude::*, status_builder::StatusBuilder}; use log::{debug, error, info, warn}; use rusqlite::Connection; use std::borrow::Cow; use tokio::fs::remove_file; /// This is where the magic happens #[tokio::main] pub async fn run(config: Config) { // open the SQLite connection let conn = Connection::open(&config.scootaloo.db_path).unwrap_or_else(|e| { panic!( "Something went wrong when opening the DB {}: {}", &config.scootaloo.db_path, e ) }); // retrieve the last tweet ID for the username let last_tweet_id = read_state(&conn, None) .unwrap_or_else(|e| panic!("Cannot retrieve last_tweet_id: {}", e)) .map(|s| s.tweet_id); // get OAuth2 token let token = get_oauth2_token(&config.twitter); // get Mastodon instance let mastodon = get_mastodon_token(&config.mastodon); // get user timeline feed (Vec) let mut feed = get_user_timeline(&config.twitter, token, last_tweet_id) .await .unwrap_or_else(|e| { panic!( "Something went wrong when trying to retrieve {}’s timeline: {}", &config.twitter.username, e ) }); // empty feed -> exiting if feed.is_empty() { info!("Nothing to retrieve since last time, exiting…"); return; } // order needs to be chronological feed.reverse(); for tweet in &feed { debug!("Treating Tweet {} inside feed", tweet.id); // initiate the toot_reply_id var let mut toot_reply_id: Option = None; // determine if the tweet is part of a thread (response to self) or a standard response if let Some(r) = &tweet.in_reply_to_screen_name { if r.to_lowercase() != config.twitter.username.to_lowercase() { // we are responding not threading info!("Tweet is a direct response, skipping"); continue; } info!("Tweet is a thread"); toot_reply_id = read_state(&conn, tweet.in_reply_to_status_id) .unwrap_or(None) .map(|s| s.toot_id); }; // build basic status by just yielding text and dereferencing contained urls let mut status_text = build_basic_status(tweet); let mut status_medias: Vec = vec![]; // reupload the attachments if any if let Some(m) = &tweet.extended_entities { for media in &m.media { let local_tweet_media_path = match get_tweet_media(media, &config.scootaloo.cache_path).await { Ok(m) => m, Err(e) => { error!("Cannot get tweet media for {}: {}", &media.url, e); continue; } }; let mastodon_media_ids = match mastodon .media(Cow::from(local_tweet_media_path.to_owned())) { Ok(m) => { remove_file(&local_tweet_media_path) .await .unwrap_or_else(|e| warn!("Attachment for {} has been uploaded, but I’m unable to remove the existing file: {}", &local_tweet_media_path, e) ); m.id } Err(e) => { error!( "Attachment {} cannot be uploaded to Mastodon Instance: {}", &local_tweet_media_path, e ); continue; } }; status_medias.push(mastodon_media_ids); // last step, removing the reference to the media from with the toot’s text status_text = status_text.replace(&media.url, ""); } } // finished reuploading attachments, now let’s do the toot baby! debug!("Building corresponding Mastodon status"); let mut status_builder = StatusBuilder::new(); status_builder.status(&status_text).media_ids(status_medias); if let Some(i) = toot_reply_id { status_builder.in_reply_to(&i); } let status = status_builder .build() .unwrap_or_else(|_| panic!("Cannot build status with text {}", &status_text)); // publish status // again unwrap is safe here as we are in the main thread let published_status = mastodon.new_status(status).unwrap(); // this will panic if it cannot publish the status, which is a good thing, it allows the // last_tweet gathered not to be written let ttt_towrite = TweetToToot { tweet_id: tweet.id, toot_id: published_status.id, }; // write the current state (tweet ID and toot ID) to avoid copying it another time write_state(&conn, ttt_towrite) .unwrap_or_else(|e| panic!("Can’t write the last tweet retrieved: {}", e)); } }