diff --git a/Cargo.lock b/Cargo.lock index c801a9b..4e1461f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2016,7 +2016,7 @@ dependencies = [ [[package]] name = "scootaloo" -version = "0.7.2" +version = "0.8.0" dependencies = [ "chrono", "clap", diff --git a/Cargo.toml b/Cargo.toml index d866bd5..0920dd7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "scootaloo" -version = "0.7.2" +version = "0.8.0" authors = ["VC "] edition = "2021" diff --git a/src/lib.rs b/src/lib.rs index 4c68bb7..b7edac0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,16 +20,16 @@ pub use state::{init_db, migrate_db}; use state::{read_state, write_state, TweetToToot}; use elefren::{prelude::*, status_builder::StatusBuilder}; -use log::{debug, info}; +use log::info; use rusqlite::Connection; use std::sync::Arc; -use tokio::{spawn, sync::Mutex, task::JoinHandle}; +use tokio::{spawn, sync::Mutex}; /// This is where the magic happens #[tokio::main] pub async fn run(config: Config) { // create the task vector for handling multiple accounts - let mut mtask: Vec> = vec![]; + let mut mtask = vec![]; // open the SQLite connection let conn = Arc::new(Mutex::new( @@ -48,7 +48,7 @@ pub async fn run(config: Config) { let task_conn = conn.clone(); let task = spawn(async move { - debug!("Starting treating {}", &mastodon_config.twitter_screen_name); + info!("Starting treating {}", &mastodon_config.twitter_screen_name); // retrieve the last tweet ID for the username let lconn = task_conn.lock().await; @@ -81,7 +81,7 @@ pub async fn run(config: Config) { feed.reverse(); for tweet in &feed { - debug!("Treating Tweet {} inside feed", tweet.id); + info!("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 @@ -113,7 +113,7 @@ pub async fn run(config: Config) { status_text = status_text.replace(&media_url, ""); - debug!("Building corresponding Mastodon status"); + info!("Building corresponding Mastodon status"); let mut status_builder = StatusBuilder::new(); diff --git a/src/mastodon.rs b/src/mastodon.rs index d903b0e..5434f40 100644 --- a/src/mastodon.rs +++ b/src/mastodon.rs @@ -101,9 +101,9 @@ pub fn register(host: &str, screen_name: &str) { println!( "Please insert the following block at the end of your configuration file: - \n[mastodon.{}] - \ntwitter_screen_name = \"{}\" - \n{}", +[mastodon.{}] +twitter_screen_name = \"{}\" +{}", screen_name.to_lowercase(), screen_name, toml diff --git a/src/util.rs b/src/util.rs index 8aa34a8..e994e2e 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,9 +1,15 @@ use crate::{twitter::get_tweet_media, ScootalooError}; -use egg_mode::tweet::Tweet; -use elefren::prelude::*; -use log::{error, warn}; -use reqwest::Url; + use std::{borrow::Cow, error::Error}; + +use egg_mode::tweet::Tweet; + +use elefren::prelude::*; + +use log::{error, info, warn}; + +use reqwest::Url; + use tokio::{ fs::{create_dir_all, remove_file, File}, io::copy, @@ -15,45 +21,77 @@ pub async fn generate_media_ids( cache_path: &str, mastodon: &Mastodon, ) -> (String, Vec) { + let mut media_url = "".to_string(); let mut media_ids: Vec = vec![]; - let mut media_url: String = "".to_string(); if let Some(m) = &tweet.extended_entities { - for media in &m.media { - // attribute the media url + // create tasks list + let mut tasks = vec![]; + + // size of media_ids vector, should be equal to the media vector + media_ids.resize(m.media.len(), String::new()); + + info!("{} medias in tweet", m.media.len()); + + for (i, media) in m.media.iter().enumerate() { + // attribute media url media_url = media.url.clone(); - let local_tweet_media_path = match get_tweet_media(media, 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 - ); - // file is no longer useful, deleting - 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)); - continue; - } - }; + // clone everything we need + let cache_path = String::from(cache_path); + let media = media.clone(); + let mastodon = mastodon.clone(); - media_ids.push(mastodon_media_ids); + let task = tokio::task::spawn(async move { + info!("Start treating {}", media.media_url_https); + // get the tweet embedded media + let local_tweet_media_path = match get_tweet_media(&media, &cache_path).await { + Ok(l) => l, + Err(e) => { + return Err(ScootalooError::new(&format!( + "Cannot get tweet media for {}: {}", + &media.url, e + ))) + } + }; + + // upload media to Mastodon + let mastodon_media = mastodon.media(Cow::from(local_tweet_media_path.to_owned())); + // at this point, we can safely erase the original file + // it doesn’t matter if we can’t remove, cache_media fn is idempotent + remove_file(&local_tweet_media_path).await.ok(); + + let mastodon_media = match mastodon_media { + Ok(m) => m, + Err(e) => { + return Err(ScootalooError::new(&format!( + "Attachment {} cannot be uploaded to Mastodon Instance: {}", + &local_tweet_media_path, e + ))) + } + }; + + Ok((i, mastodon_media.id)) + }); + + tasks.push(task); } + + for task in tasks { + match task.await { + // insert the media at the right place + Ok(Ok((i, v))) => media_ids[i] = v, + Ok(Err(e)) => warn!("{}", e), + Err(e) => error!("Something went wrong when joining the main thread: {}", e), + } + } + } else { + info!("No media in tweet"); } + // in case some media_ids slot remained empty due to errors, remove them + media_ids.retain(|x| !x.is_empty()); + (media_url, media_ids) }