diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 94bf8af..71d49ab 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,15 +1,5 @@ --- - -stages: - - build - -rust-latest: - stage: build - artifacts: - paths: - - target/release/scootaloo - image: rust:latest - script: - - cargo test - - cargo build --release --verbose - - strip target/release/${CI_PROJECT_NAME} +include: + project: 'veretcle/ci-common' + ref: 'main' + file: 'ci_rust.yml' diff --git a/Cargo.lock b/Cargo.lock index f39fb48..bcaf49f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2016,7 +2016,7 @@ dependencies = [ [[package]] name = "scootaloo" -version = "0.5.2" +version = "0.6.0" dependencies = [ "chrono", "clap", diff --git a/Cargo.toml b/Cargo.toml index f0d75b7..07bba49 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "scootaloo" -version = "0.5.2" +version = "0.6.0" authors = ["VC "] edition = "2021" @@ -21,3 +21,5 @@ reqwest = "^0.11" log = "^0.4" simple_logger = "^2.1" +[profile.release] +strip = true diff --git a/src/config.rs b/src/config.rs index ba7a60c..86e2f51 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,5 @@ -use std::fs::read_to_string; use serde::Deserialize; +use std::fs::read_to_string; /// General configuration Struct #[derive(Debug, Deserialize)] @@ -35,14 +35,11 @@ pub struct ScootalooConfig { /// Parses the TOML file into a Config Struct pub fn parse_toml(toml_file: &str) -> Config { - let toml_config = read_to_string(toml_file).unwrap_or_else(|e| - panic!("Cannot open config file {}: {}", toml_file, e) - ); + let toml_config = read_to_string(toml_file) + .unwrap_or_else(|e| panic!("Cannot open config file {}: {}", toml_file, e)); - let config: Config = toml::from_str(&toml_config).unwrap_or_else(|e| - panic!("Cannot parse TOML file {}: {}", toml_file, e) - ); + let config: Config = toml::from_str(&toml_config) + .unwrap_or_else(|e| panic!("Cannot parse TOML file {}: {}", toml_file, e)); config } - diff --git a/src/error.rs b/src/error.rs index 4edca87..9055477 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,6 +1,6 @@ use std::{ - fmt::{Display, Formatter, Result}, error::Error, + fmt::{Display, Formatter, Result}, }; #[derive(Debug)] @@ -23,4 +23,3 @@ impl Display for ScootalooError { write!(f, "{}", self.details) } } - diff --git a/src/lib.rs b/src/lib.rs index dd0b3d6..39427ab 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,12 +2,12 @@ mod error; use error::ScootalooError; mod config; -use config::Config; pub use config::parse_toml; +use config::Config; mod mastodon; -use mastodon::{get_mastodon_token, build_basic_status}; pub use mastodon::register; +use mastodon::{build_basic_status, get_mastodon_token}; mod twitter; use twitter::*; @@ -15,31 +15,28 @@ use twitter::*; mod util; mod state; -use state::{read_state, write_state, TweetToToot}; 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; -use elefren::{ - prelude::*, - status_builder::StatusBuilder, -}; -use log::{info, warn, error, debug}; -use rusqlite::Connection; /// 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) - ); + 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) - ) + .unwrap_or_else(|e| panic!("Cannot retrieve last_tweet_id: {}", e)) .map(|s| s.tweet_id); // get OAuth2 token @@ -51,9 +48,12 @@ pub async fn run(config: Config) { // 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) - ); + .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() { @@ -69,34 +69,37 @@ pub async fn run(config: Config) { // 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); + 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 + // 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 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())) { + let mastodon_media_ids = match mastodon + .media(Cow::from(local_tweet_media_path.to_owned())) + { Ok(m) => { remove_file(&local_tweet_media_path) .await @@ -104,9 +107,12 @@ pub async fn run(config: Config) { 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); + error!( + "Attachment {} cannot be uploaded to Mastodon Instance: {}", + &local_tweet_media_path, e + ); continue; } }; @@ -123,15 +129,15 @@ pub async fn run(config: Config) { let mut status_builder = StatusBuilder::new(); - status_builder.status(&status_text) - .media_ids(status_medias); + 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() - .expect(&format!("Cannot build status with text {}", &status_text)); + 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 @@ -145,9 +151,7 @@ pub async fn run(config: Config) { }; // 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) - ); + write_state(&conn, ttt_towrite) + .unwrap_or_else(|e| panic!("Can’t write the last tweet retrieved: {}", e)); } } - diff --git a/src/main.rs b/src/main.rs index d31f57c..269e887 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,70 +1,91 @@ -use scootaloo::*; use clap::{App, Arg, SubCommand}; -use log::{LevelFilter, error}; +use log::{error, LevelFilter}; +use scootaloo::*; use simple_logger::SimpleLogger; use std::str::FromStr; -const DEFAULT_CONFIG_PATH: &'static str = "/usr/local/etc/scootaloo.toml"; +const DEFAULT_CONFIG_PATH: &str = "/usr/local/etc/scootaloo.toml"; fn main() { let matches = App::new(env!("CARGO_PKG_NAME")) - .version(env!("CARGO_PKG_VERSION")) - .about("A Twitter to Mastodon bot") - .arg(Arg::with_name("config") - .short("c") - .long("config") - .value_name("CONFIG_FILE") - .help(&format!("TOML config file for scootaloo (default {})", DEFAULT_CONFIG_PATH)) - .takes_value(true) - .display_order(1)) - .arg(Arg::with_name("log_level") - .short("l") - .long("loglevel") - .value_name("LOGLEVEL") - .help("Log level. Valid values are: Off, Warn, Error, Info, Debug") - .takes_value(true) - .display_order(2)) - .subcommand(SubCommand::with_name("register") - .version(env!("CARGO_PKG_VERSION")) - .about("Command to register to a Mastodon Instance") - .arg(Arg::with_name("host") - .short("H") - .long("host") - .value_name("HOST") - .help("Base URL of the Mastodon instance to register to (no default)") - .takes_value(true) - .required(true) - .display_order(1))) - .subcommand(SubCommand::with_name("init") - .version(env!("CARGO_PKG_VERSION")) - .about("Command to init Scootaloo DB") - .arg(Arg::with_name("config") - .short("c") - .long("config") - .value_name("CONFIG_FILE") - .help(&format!("TOML config file for scootaloo (default {})", DEFAULT_CONFIG_PATH)) - .takes_value(true) - .display_order(1))) - .get_matches(); + .version(env!("CARGO_PKG_VERSION")) + .about("A Twitter to Mastodon bot") + .arg( + Arg::with_name("config") + .short("c") + .long("config") + .value_name("CONFIG_FILE") + .help(&format!( + "TOML config file for scootaloo (default {})", + DEFAULT_CONFIG_PATH + )) + .takes_value(true) + .display_order(1), + ) + .arg( + Arg::with_name("log_level") + .short("l") + .long("loglevel") + .value_name("LOGLEVEL") + .help("Log level. Valid values are: Off, Warn, Error, Info, Debug") + .takes_value(true) + .display_order(2), + ) + .subcommand( + SubCommand::with_name("register") + .version(env!("CARGO_PKG_VERSION")) + .about("Command to register to a Mastodon Instance") + .arg( + Arg::with_name("host") + .short("H") + .long("host") + .value_name("HOST") + .help("Base URL of the Mastodon instance to register to (no default)") + .takes_value(true) + .required(true) + .display_order(1), + ), + ) + .subcommand( + SubCommand::with_name("init") + .version(env!("CARGO_PKG_VERSION")) + .about("Command to init Scootaloo DB") + .arg( + Arg::with_name("config") + .short("c") + .long("config") + .value_name("CONFIG_FILE") + .help(&format!( + "TOML config file for scootaloo (default {})", + DEFAULT_CONFIG_PATH + )) + .takes_value(true) + .display_order(1), + ), + ) + .get_matches(); match matches.subcommand() { ("register", Some(sub_m)) => { register(sub_m.value_of("host").unwrap()); return; - }, + } ("init", Some(sub_m)) => { let config = parse_toml(sub_m.value_of("config").unwrap_or(DEFAULT_CONFIG_PATH)); init_db(&config.scootaloo.db_path).unwrap(); return; - }, + } _ => (), } if matches.is_present("log_level") { match LevelFilter::from_str(matches.value_of("log_level").unwrap()) { - Ok(level) => { SimpleLogger::new().with_level(level).init().unwrap()}, + Ok(level) => SimpleLogger::new().with_level(level).init().unwrap(), Err(e) => { - SimpleLogger::new().with_level(LevelFilter::Error).init().unwrap(); + SimpleLogger::new() + .with_level(LevelFilter::Error) + .init() + .unwrap(); error!("Unknown log level filter: {}", e); } }; @@ -74,4 +95,3 @@ fn main() { run(config); } - diff --git a/src/mastodon.rs b/src/mastodon.rs index e46e79a..f464ef6 100644 --- a/src/mastodon.rs +++ b/src/mastodon.rs @@ -1,36 +1,37 @@ use crate::config::MastodonConfig; -use std::{ - borrow::Cow, - collections::HashMap, - io::stdin, -}; -use html_escape::decode_html_entities; use egg_mode::{ + entities::{MentionEntity, UrlEntity}, tweet::Tweet, - entities::{UrlEntity, MentionEntity}, -}; -use elefren::{ - prelude::*, - apps::App, - scopes::Scopes, }; +use elefren::{apps::App, prelude::*, scopes::Scopes}; +use html_escape::decode_html_entities; +use std::{borrow::Cow, collections::HashMap, io::stdin}; /// Decodes the Twitter mention to something that will make sense once Twitter has joined the /// Fediverse -fn twitter_mentions(ums: &Vec) -> HashMap { - ums.iter().map(|s| - (format!("@{}", s.screen_name), format!("@{}@twitter.com", s.screen_name)) - ).collect() +fn twitter_mentions(ums: &[MentionEntity]) -> HashMap { + ums.iter() + .map(|s| { + ( + format!("@{}", s.screen_name), + format!("@{}@twitter.com", s.screen_name), + ) + }) + .collect() } /// Decodes urls from UrlEntities -fn decode_urls(urls: &Vec) -> HashMap { +fn decode_urls(urls: &[UrlEntity]) -> HashMap { urls.iter() .filter(|s| s.expanded_url.is_some()) - .map(|s| - (s.url.to_owned(), s.expanded_url.as_deref().unwrap().to_owned()) - ).collect() + .map(|s| { + ( + s.url.to_owned(), + s.expanded_url.as_deref().unwrap().to_owned(), + ) + }) + .collect() } /// Gets Mastodon Data @@ -66,36 +67,50 @@ pub fn build_basic_status(tweet: &Tweet) -> String { /// Most of this function is a direct copy/paste of the official `elefren` crate pub fn register(host: &str) { let mut builder = App::builder(); - builder.client_name(Cow::from(env!("CARGO_PKG_NAME").to_string())) + builder + .client_name(Cow::from(env!("CARGO_PKG_NAME").to_string())) .redirect_uris(Cow::from("urn:ietf:wg:oauth:2.0:oob".to_string())) .scopes(Scopes::write_all()) - .website(Cow::from("https://framagit.org/veretcle/scootaloo".to_string())); + .website(Cow::from( + "https://framagit.org/veretcle/scootaloo".to_string(), + )); let app = builder.build().expect("Cannot build the app"); - let registration = Registration::new(host).register(app).expect("Cannot build registration object"); - let url = registration.authorize_url().expect("Cannot generate registration URI!"); + let registration = Registration::new(host) + .register(app) + .expect("Cannot build registration object"); + let url = registration + .authorize_url() + .expect("Cannot generate registration URI!"); println!("Click this link to authorize on Mastodon: {}", url); println!("Paste the returned authorization code: "); let mut input = String::new(); - stdin().read_line(&mut input).expect("Unable to read back registration code!"); + stdin() + .read_line(&mut input) + .expect("Unable to read back registration code!"); let code = input.trim(); - let mastodon = registration.complete(code).expect("Unable to create access token!"); + let mastodon = registration + .complete(code) + .expect("Unable to create access token!"); let toml = toml::to_string(&*mastodon).unwrap(); - println!("Please insert the following block at the end of your configuration file:\n[mastodon]\n{}", toml); + println!( + "Please insert the following block at the end of your configuration file:\n[mastodon]\n{}", + toml + ); } #[cfg(test)] mod tests { use super::*; - use egg_mode::tweet::TweetEntities; use chrono::prelude::*; + use egg_mode::tweet::TweetEntities; #[test] fn test_twitter_mentions() { @@ -109,7 +124,10 @@ mod tests { let twitter_ums = vec![mention_entity]; let mut expected_mentions = HashMap::new(); - expected_mentions.insert("@tamerelol".to_string(), "@tamerelol@twitter.com".to_string()); + expected_mentions.insert( + "@tamerelol".to_string(), + "@tamerelol@twitter.com".to_string(), + ); let decoded_mentions = twitter_mentions(&twitter_ums); @@ -135,7 +153,10 @@ mod tests { let twitter_urls = vec![url_entity1, url_entity2]; let mut expected_urls = HashMap::new(); - expected_urls.insert("https://t.me/tamerelol".to_string(), "https://www.nintendojo.fr/dojobar".to_string()); + expected_urls.insert( + "https://t.me/tamerelol".to_string(), + "https://www.nintendojo.fr/dojobar".to_string(), + ); let decoded_urls = decode_urls(&twitter_urls); @@ -200,4 +221,3 @@ mod tests { assert_eq!(&t_out, "Mother 1 & 2 sur le NES/SNES online !\nDispo maintenant. cc @NintendoFrance@twitter.com https://www.youtube.com/watch?v=w5TrSaoYmZ8"); } } - diff --git a/src/state.rs b/src/state.rs index 011a3ad..5518210 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,6 +1,6 @@ -use std::error::Error; use log::debug; -use rusqlite::{Connection, params, OptionalExtension}; +use rusqlite::{params, Connection, OptionalExtension}; +use std::error::Error; /// Struct for each query line #[derive(Debug)] @@ -11,22 +11,26 @@ pub struct TweetToToot { /// if None is passed, read the last tweet from DB /// if a tweet_id is passed, read this particular tweet from DB -pub fn read_state(conn: &Connection, s: Option) -> Result, Box> { +pub fn read_state( + conn: &Connection, + s: Option, +) -> Result, Box> { debug!("Reading tweet_id {:?}", s); - let query: String; - match s { - Some(i) => query = format!("SELECT * FROM tweet_to_toot WHERE tweet_id = {}", i), - None => query = "SELECT * FROM tweet_to_toot ORDER BY tweet_id DESC LIMIT 1".to_string(), + let query: String = match s { + Some(i) => format!("SELECT * FROM tweet_to_toot WHERE tweet_id = {}", i), + None => "SELECT * FROM tweet_to_toot ORDER BY tweet_id DESC LIMIT 1".to_string(), }; let mut stmt = conn.prepare(&query)?; - let t = stmt.query_row([], |row| { - Ok(TweetToToot { - tweet_id: row.get(0)?, - toot_id: row.get(1)?, + let t = stmt + .query_row([], |row| { + Ok(TweetToToot { + tweet_id: row.get(0)?, + toot_id: row.get(1)?, + }) }) - }).optional()?; + .optional()?; Ok(t) } @@ -37,7 +41,7 @@ pub fn write_state(conn: &Connection, t: TweetToToot) -> Result<(), Box Result<(), Box> { #[cfg(test)] mod tests { use super::*; - use std::{ - fs::remove_file, - path::Path, - }; + use std::{fs::remove_file, path::Path}; #[test] fn test_init_db() { @@ -77,10 +78,7 @@ mod tests { // open said file let conn = Connection::open(d).unwrap(); - conn.execute( - "SELECT * from tweet_to_toot;", - [], - ).unwrap(); + conn.execute("SELECT * from tweet_to_toot;", []).unwrap(); remove_file(d).unwrap(); } @@ -99,7 +97,8 @@ mod tests { VALUES (100, 'A');", [], - ).unwrap(); + ) + .unwrap(); init_db(d).unwrap(); @@ -123,12 +122,14 @@ mod tests { let mut stmt = conn.prepare("SELECT * FROM tweet_to_toot;").unwrap(); - let t_out = stmt.query_row([], |row| { - Ok(TweetToToot { - tweet_id: row.get(0).unwrap(), - toot_id: row.get(1).unwrap(), + let t_out = stmt + .query_row([], |row| { + Ok(TweetToToot { + tweet_id: row.get(0).unwrap(), + toot_id: row.get(1).unwrap(), + }) }) - }).unwrap(); + .unwrap(); assert_eq!(t_out.tweet_id, 123456789); assert_eq!(t_out.toot_id, "987654321".to_string()); @@ -150,7 +151,8 @@ mod tests { (101, 'A'), (102, 'B');", [], - ).unwrap(); + ) + .unwrap(); let t_out = read_state(&conn, None).unwrap().unwrap(); @@ -188,7 +190,8 @@ mod tests { VALUES (100, 'A');", [], - ).unwrap(); + ) + .unwrap(); let t_out = read_state(&conn, Some(101)).unwrap(); @@ -210,7 +213,8 @@ mod tests { VALUES (100, 'A');", [], - ).unwrap(); + ) + .unwrap(); let t_out = read_state(&conn, Some(100)).unwrap().unwrap(); @@ -220,4 +224,3 @@ mod tests { assert_eq!(t_out.toot_id, "A"); } } - diff --git a/src/twitter.rs b/src/twitter.rs index cdeb66b..cf530ca 100644 --- a/src/twitter.rs +++ b/src/twitter.rs @@ -1,23 +1,25 @@ -use crate::ScootalooError; use crate::config::TwitterConfig; use crate::util::cache_media; +use crate::ScootalooError; -use std::error::Error; use egg_mode::{ - Token, - KeyPair, entities::{MediaEntity, MediaType}, + tweet::{user_timeline, Tweet}, user::UserID, - tweet::{ - Tweet, - user_timeline, - }, + KeyPair, Token, }; +use std::error::Error; /// Gets Twitter oauth2 token pub fn get_oauth2_token(config: &TwitterConfig) -> Token { - let con_token = KeyPair::new(config.consumer_key.to_owned(),config.consumer_secret.to_owned()); - let access_token = KeyPair::new(config.access_key.to_owned(), config.access_secret.to_owned()); + let con_token = KeyPair::new( + config.consumer_key.to_owned(), + config.consumer_secret.to_owned(), + ); + let access_token = KeyPair::new( + config.access_key.to_owned(), + config.access_secret.to_owned(), + ); Token::Access { consumer: con_token, @@ -26,12 +28,21 @@ pub fn get_oauth2_token(config: &TwitterConfig) -> Token { } /// Gets Twitter user timeline -pub async fn get_user_timeline(config: &TwitterConfig, token: Token, lid: Option) -> Result, Box> { +pub async fn get_user_timeline( + config: &TwitterConfig, + token: Token, + lid: Option, +) -> Result, Box> { // fix the page size to 200 as it is the maximum Twitter authorizes - let (_, feed) = user_timeline(UserID::from(config.username.to_owned()), true, false, &token) - .with_page_size(200) - .older(lid) - .await?; + let (_, feed) = user_timeline( + UserID::from(config.username.to_owned()), + true, + false, + &token, + ) + .with_page_size(200) + .older(lid) + .await?; Ok(feed.to_vec()) } @@ -41,22 +52,27 @@ pub async fn get_tweet_media(m: &MediaEntity, t: &str) -> Result { return cache_media(&m.media_url_https, t).await; - }, - _ => { - match &m.video_info { - Some(v) => { - for variant in &v.variants { - if variant.content_type == "video/mp4" { - return cache_media(&variant.url, t).await; - } + } + _ => match &m.video_info { + Some(v) => { + for variant in &v.variants { + if variant.content_type == "video/mp4" { + return cache_media(&variant.url, t).await; } - return Err(ScootalooError::new(&format!("Media Type for {} is video but no mp4 file URL is available", &m.url)).into()); - }, - None => { - return Err(ScootalooError::new(&format!("Media Type for {} is video but does not contain any video_info", &m.url)).into()); - }, + } + return Err(ScootalooError::new(&format!( + "Media Type for {} is video but no mp4 file URL is available", + &m.url + )) + .into()); + } + None => { + return Err(ScootalooError::new(&format!( + "Media Type for {} is video but does not contain any video_info", + &m.url + )) + .into()); } }, }; } - diff --git a/src/util.rs b/src/util.rs index f665882..1517718 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,9 +1,9 @@ -use std::error::Error; use crate::ScootalooError; use reqwest::Url; +use std::error::Error; use tokio::{ + fs::{create_dir_all, File}, io::copy, - fs::{File, create_dir_all}, }; /// Gets and caches Twitter Media inside the determined temp dir @@ -16,8 +16,21 @@ pub async fn cache_media(u: &str, t: &str) -> Result> { // create local file let url = Url::parse(u)?; - let dest_filename = url.path_segments().ok_or_else(|| ScootalooError::new(&format!("Cannot determine the destination filename for {}", u)))? - .last().ok_or_else(|| ScootalooError::new(&format!("Cannot determine the destination filename for {}", u)))?; + let dest_filename = url + .path_segments() + .ok_or_else(|| { + ScootalooError::new(&format!( + "Cannot determine the destination filename for {}", + u + )) + })? + .last() + .ok_or_else(|| { + ScootalooError::new(&format!( + "Cannot determine the destination filename for {}", + u + )) + })?; let dest_filepath = format!("{}/{}", t, dest_filename); @@ -34,20 +47,21 @@ pub async fn cache_media(u: &str, t: &str) -> Result> { mod tests { use super::*; - use std::{ - path::Path, - fs::remove_dir_all, - }; + use std::{fs::remove_dir_all, path::Path}; const TMP_DIR: &'static str = "/tmp/scootaloo_test"; #[tokio::test] async fn test_cache_media() { - let dest = cache_media("https://forum.nintendojo.fr/styles/prosilver/theme/images/ndfr_casual.png", TMP_DIR).await.unwrap(); + let dest = cache_media( + "https://forum.nintendojo.fr/styles/prosilver/theme/images/ndfr_casual.png", + TMP_DIR, + ) + .await + .unwrap(); assert!(Path::new(&dest).exists()); remove_dir_all(TMP_DIR).unwrap(); } } - diff --git a/tests/config.rs b/tests/config.rs index ce1e1a7..a1df127 100644 --- a/tests/config.rs +++ b/tests/config.rs @@ -4,7 +4,10 @@ use scootaloo::parse_toml; fn test_parse_good_toml() { let parse_good_toml = parse_toml("tests/good_test.toml"); - assert_eq!(parse_good_toml.scootaloo.db_path, "/var/random/scootaloo.sqlite"); + assert_eq!( + parse_good_toml.scootaloo.db_path, + "/var/random/scootaloo.sqlite" + ); assert_eq!(parse_good_toml.scootaloo.cache_path, "/tmp/scootaloo"); assert_eq!(parse_good_toml.twitter.username, "tamerelol"); @@ -16,18 +19,25 @@ fn test_parse_good_toml() { assert_eq!(parse_good_toml.mastodon.base, "https://m.nintendojo.fr"); assert_eq!(parse_good_toml.mastodon.client_id, "rand client id"); assert_eq!(parse_good_toml.mastodon.client_secret, "secret"); - assert_eq!(parse_good_toml.mastodon.redirect, "urn:ietf:wg:oauth:2.0:oob"); + assert_eq!( + parse_good_toml.mastodon.redirect, + "urn:ietf:wg:oauth:2.0:oob" + ); assert_eq!(parse_good_toml.mastodon.token, "super secret"); } #[test] -#[should_panic(expected = "Cannot open config file tests/no_file.toml: No such file or directory (os error 2)")] +#[should_panic( + expected = "Cannot open config file tests/no_file.toml: No such file or directory (os error 2)" +)] fn test_parse_no_toml() { let _parse_no_toml = parse_toml("tests/no_file.toml"); } #[test] -#[should_panic(expected = "Cannot parse TOML file tests/bad_test.toml: expected an equals, found a newline at line 1 column 5")] +#[should_panic( + expected = "Cannot parse TOML file tests/bad_test.toml: expected an equals, found a newline at line 1 column 5" +)] fn test_parse_bad_toml() { let _parse_bad_toml = parse_toml("tests/bad_test.toml"); }