use crate::config::MastodonConfig; use egg_mode::{ entities::{MentionEntity, UrlEntity}, user::UserEntityDetail, }; use megalodon::{ generator, mastodon::Mastodon, megalodon::{AppInputOptions, CredentialsFieldAttribute}, }; use regex::Regex; use std::{collections::HashMap, io::stdin}; /// Decodes the Twitter mention to something that will make sense once Twitter has joined the /// Fediverse. Users in the global user list of Scootaloo are rewritten, as they are Mastodon users /// as well pub fn twitter_mentions( toot: &mut String, ums: &[MentionEntity], masto: &HashMap, ) { let tm: HashMap = ums .iter() .map(|s| { ( format!("@{}", s.screen_name), format!("@{}@twitter.com", s.screen_name), ) }) .chain( masto .values() .filter(|s| s.mastodon_screen_name.is_some()) .map(|s| { ( format!("@{}", s.twitter_screen_name), format!( "@{}@{}", s.mastodon_screen_name.as_ref().unwrap(), s.base.split('/').last().unwrap() ), ) }) .collect::>(), ) .collect(); for (k, v) in tm { *toot = toot.replace(&k, &v); } } /// Decodes urls in toot pub fn decode_urls(toot: &mut String, urls: &HashMap) { for (k, v) in urls { *toot = toot.replace(k, v); } } /// Reassociates source url with destination url for rewritting /// this takes a Teet UrlEntity and an optional Regex pub fn associate_urls(urls: &[UrlEntity], re: &Option) -> HashMap { urls.iter() .filter(|s| s.expanded_url.is_some()) .map(|s| { (s.url.to_owned(), { let mut def = s.expanded_url.as_deref().unwrap().to_owned(); if let Some(r) = re { if r.is_match(s.expanded_url.as_deref().unwrap()) { def = s.display_url.to_owned(); } } def }) }) .collect::>() } /// Replaces the commonly used services by mirrors, if asked to pub fn replace_alt_services(urls: &mut HashMap, alts: &HashMap) { for val in urls.values_mut() { for (k, v) in alts { *val = val.replace(&format!("/{k}/"), &format!("/{v}/")); } } } /// Finds a Mastodon screen_name/base_url from a MastodonConfig pub fn find_mastodon_screen_name_by_twitter_screen_name( twitter_screen_name: &str, masto: &HashMap, ) -> Option<(String, String)> { masto.iter().find_map(|(_, v)| { if twitter_screen_name == v.twitter_screen_name && v.mastodon_screen_name.is_some() { Some(( v.mastodon_screen_name.as_ref().unwrap().to_owned(), v.base.to_owned(), )) } else { None } }) } /// Replaces the original quoted tweet by the corresponding toot pub fn replace_tweet_by_toot( urls: &mut HashMap, twitter_screen_name: &str, tweet_id: u64, mastodon_screen_name: &str, base_url: &str, toot_id: &str, ) { for val in urls.values_mut() { if val.to_lowercase().starts_with(&format!( "https://twitter.com/{}/status/{}", twitter_screen_name.to_lowercase(), tweet_id )) { *val = format!("{base_url}/@{mastodon_screen_name}/{toot_id}"); } } } /// Gets Mastodon Data pub fn get_mastodon_token(masto: &MastodonConfig) -> Mastodon { Mastodon::new(masto.base.to_string(), Some(masto.token.to_string()), None) } /// Gets note from twitter_user description pub fn get_note_from_description(t: &Option, urls: &[UrlEntity]) -> Option { t.as_ref().map(|d| { let mut n = d.to_owned(); let a_urls = associate_urls(urls, &None); decode_urls(&mut n, &a_urls); n }) } /// Gets fields_attribute from UserEntityDetail pub fn get_attribute_from_url( user_entity_detail: &Option, ) -> Option> { user_entity_detail.as_ref().and_then(|u| { u.urls.first().and_then(|v| { v.expanded_url.as_ref().map(|e| { vec![CredentialsFieldAttribute { name: v.display_url.to_string(), value: e.to_string(), }] }) }) }) } /// Generic register function /// As this function is supposed to be run only once, it will panic for every error it encounters /// Most of this function is a direct copy/paste of the official `elefren` crate #[tokio::main] pub async fn register(host: &str, screen_name: &str) { let mastodon = generator(megalodon::SNS::Mastodon, host.to_string(), None, None); let options = AppInputOptions { redirect_uris: None, scopes: Some( [ "read:accounts".to_string(), "write:accounts".to_string(), "write:media".to_string(), "write:statuses".to_string(), ] .to_vec(), ), website: Some("https://framagit.org/veretcle/scootaloo".to_string()), }; let app_data = mastodon .register_app(env!("CARGO_PKG_NAME").to_string(), &options) .await .expect("Cannot build registration object!"); let url = app_data.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!"); let token_data = mastodon .fetch_access_token( app_data.client_id.to_owned(), app_data.client_secret.to_owned(), input.trim().to_string(), megalodon::default::NO_REDIRECT.to_string(), ) .await .expect("Unable to create access token!"); let mastodon = generator( megalodon::SNS::Mastodon, host.to_string(), Some(token_data.access_token.to_owned()), None, ); let current_account = mastodon .verify_account_credentials() .await .expect("Unable to access account information!") .json(); println!( r#"Please insert the following block at the end of your configuration file: [mastodon.{}] twitter_screen_name = "{}" mastodon_screen_name = "{}" base = "{}" client_id = "{}" client_secret = "{}" redirect = "{}" token = "{}""#, screen_name.to_lowercase(), screen_name, current_account.username, host, app_data.client_id, app_data.client_secret, app_data.redirect_uri, token_data.access_token, ); } #[cfg(test)] mod tests { use super::*; #[test] fn test_get_attribute_from_url() { let expected_credentials_field_attribute = CredentialsFieldAttribute { name: "Nintendojo.fr".to_string(), value: "https://www.nintendojo.fr".to_string(), }; let true_urls = vec![UrlEntity { display_url: "Nintendojo.fr".to_string(), expanded_url: Some("https://www.nintendojo.fr".to_string()), range: (1, 3), url: "https://t.me/balek".to_string(), }]; let false_urls = vec![UrlEntity { display_url: "Nintendojo.fr".to_string(), expanded_url: None, range: (1, 3), url: "https://t.me/balek".to_string(), }]; assert!(get_attribute_from_url(&None).is_none()); assert!(get_attribute_from_url(&Some(UserEntityDetail { urls: false_urls })).is_none()); let binding = get_attribute_from_url(&Some(UserEntityDetail { urls: true_urls })).unwrap(); let result_credentials_field_attribute = binding.first().unwrap(); assert_eq!( result_credentials_field_attribute.name, expected_credentials_field_attribute.name ); assert_eq!( result_credentials_field_attribute.value, expected_credentials_field_attribute.value ); } #[test] fn test_get_note_from_description() { let urls = vec![UrlEntity { display_url: "tamerelol".to_string(), expanded_url: Some("https://www.nintendojo.fr/dojobar".to_string()), range: (1, 3), url: "https://t.me/tamerelol".to_string(), }]; let some_description = Some("Youpi | https://t.me/tamerelol".to_string()); let none_description = None; assert_eq!( get_note_from_description(&some_description, &urls), Some("Youpi | https://www.nintendojo.fr/dojobar".to_string()) ); assert_eq!(get_note_from_description(&none_description, &urls), None); } #[test] fn test_replace_tweet_by_toot() { let mut associated_urls = HashMap::from([ ( "https://t.co/perdudeouf".to_string(), "https://www.perdu.com".to_string(), ), ( "https://t.co/realquoteshere".to_string(), "https://twitter.com/nintendojofr/status/1590047921633755136".to_string(), ), ( "https://t.co/almostthere".to_string(), "https://twitter.com/NintendojoFR/status/nope".to_string(), ), ( "http://t.co/yetanotherone".to_string(), "https://twitter.com/NINTENDOJOFR/status/1590047921633755136".to_string(), ), ]); let expected_urls = HashMap::from([ ( "https://t.co/perdudeouf".to_string(), "https://www.perdu.com".to_string(), ), ( "https://t.co/realquoteshere".to_string(), "https://m.nintendojo.fr/@nintendojofr/109309605486908797".to_string(), ), ( "https://t.co/almostthere".to_string(), "https://twitter.com/NintendojoFR/status/nope".to_string(), ), ( "http://t.co/yetanotherone".to_string(), "https://m.nintendojo.fr/@nintendojofr/109309605486908797".to_string(), ), ]); replace_tweet_by_toot( &mut associated_urls, "NintendojoFR", 1590047921633755136, "nintendojofr", "https://m.nintendojo.fr", "109309605486908797", ); assert_eq!(associated_urls, expected_urls); } #[test] fn test_find_mastodon_screen_name_by_twitter_screen_name() { let masto_config = HashMap::from([ ( "test".to_string(), MastodonConfig { twitter_screen_name: "tonpere".to_string(), mastodon_screen_name: Some("lalali".to_string()), twitter_page_size: None, base: "https://mstdn.net".to_string(), client_id: "".to_string(), client_secret: "".to_string(), redirect: "".to_string(), token: "".to_string(), }, ), ( "test2".to_string(), MastodonConfig { twitter_screen_name: "tamerelol".to_string(), mastodon_screen_name: None, twitter_page_size: None, base: "https://mastoot.fr".to_string(), client_id: "".to_string(), client_secret: "".to_string(), redirect: "".to_string(), token: "".to_string(), }, ), ( "test3".to_string(), MastodonConfig { twitter_screen_name: "NintendojoFR".to_string(), mastodon_screen_name: Some("nintendojofr".to_string()), twitter_page_size: None, base: "https://m.nintendojo.fr".to_string(), client_id: "".to_string(), client_secret: "".to_string(), redirect: "".to_string(), token: "".to_string(), }, ), ]); // case sensitiveness, to avoid any mistake assert_eq!( None, find_mastodon_screen_name_by_twitter_screen_name("nintendojofr", &masto_config) ); assert_eq!( Some(( "nintendojofr".to_string(), "https://m.nintendojo.fr".to_string() )), find_mastodon_screen_name_by_twitter_screen_name("NintendojoFR", &masto_config) ); // should return None if twitter_screen_name is undefined assert_eq!( None, find_mastodon_screen_name_by_twitter_screen_name("tamerelol", &masto_config) ); assert_eq!( Some(("lalali".to_string(), "https://mstdn.net".to_string())), find_mastodon_screen_name_by_twitter_screen_name("tonpere", &masto_config) ); } #[test] fn test_twitter_mentions() { let mention_entities = vec![ MentionEntity { id: 12345, range: (1, 3), name: "Ta Mere l0l".to_string(), screen_name: "tamerelol".to_string(), }, MentionEntity { id: 6789, range: (1, 3), name: "TONPERE".to_string(), screen_name: "tonpere".to_string(), }, ]; let mut toot = ":kikoo: @tamerelol @tonpere !".to_string(); let masto_config = HashMap::from([( "test".to_string(), (MastodonConfig { twitter_screen_name: "tonpere".to_string(), mastodon_screen_name: Some("lalali".to_string()), twitter_page_size: None, base: "https://mstdn.net".to_string(), client_id: "".to_string(), client_secret: "".to_string(), redirect: "".to_string(), token: "".to_string(), }), )]); twitter_mentions(&mut toot, &mention_entities, &masto_config); assert_eq!(&toot, ":kikoo: @tamerelol@twitter.com @lalali@mstdn.net !"); } #[test] fn test_decode_urls() { let urls = HashMap::from([ ( "https://t.co/thisisatest".to_string(), "https://www.nintendojo.fr/dojobar".to_string(), ), ( "https://t.co/nopenotinclusive".to_string(), "invité.es".to_string(), ), ]); let mut toot = "Rendez-vous sur https://t.co/thisisatest avec nos https://t.co/nopenotinclusive !" .to_string(); decode_urls(&mut toot, &urls); assert_eq!( &toot, "Rendez-vous sur https://www.nintendojo.fr/dojobar avec nos invité.es !" ); } #[test] fn test_associate_urls() { let urls = vec![ UrlEntity { display_url: "tamerelol".to_string(), expanded_url: Some("https://www.nintendojo.fr/dojobar".to_string()), range: (1, 3), url: "https://t.me/tamerelol".to_string(), }, UrlEntity { display_url: "sadcat".to_string(), expanded_url: None, range: (1, 3), url: "https://t.me/sadcat".to_string(), }, UrlEntity { display_url: "invité.es".to_string(), expanded_url: Some("http://xn--invit-fsa.es".to_string()), range: (85, 108), url: "https://t.co/WAUgnpHLmo".to_string(), }, ]; let expected_urls = HashMap::from([ ( "https://t.me/tamerelol".to_string(), "https://www.nintendojo.fr/dojobar".to_string(), ), ( "https://t.co/WAUgnpHLmo".to_string(), "invité.es".to_string(), ), ]); let re = Regex::new("(.+)\\.es$").ok(); let associated_urls = associate_urls(&urls, &re); assert_eq!(associated_urls, expected_urls); } #[test] fn test_replace_alt_services() { let mut associated_urls = HashMap::from([ ( "https://t.co/youplaboom".to_string(), "https://www.youtube.com/watch?v=dQw4w9WgXcQ".to_string(), ), ( "https://t.co/thisisfine".to_string(), "https://twitter.com/Nintendo/status/1594590628771688448".to_string(), ), ( "https://t.co/nopenope".to_string(), "https://www.nintendojo.fr/dojobar".to_string(), ), ( "https://t.co/broken".to_string(), "http://youtu.be".to_string(), ), ( "https://t.co/alsobroken".to_string(), "https://youtube.com".to_string(), ), ]); let alt_services = HashMap::from([ ("twitter.com".to_string(), "nitter.net".to_string()), ("youtu.be".to_string(), "invidio.us".to_string()), ("www.youtube.com".to_string(), "invidio.us".to_string()), ("youtube.com".to_string(), "invidio.us".to_string()), ]); let expected_urls = HashMap::from([ ( "https://t.co/youplaboom".to_string(), "https://invidio.us/watch?v=dQw4w9WgXcQ".to_string(), ), ( "https://t.co/thisisfine".to_string(), "https://nitter.net/Nintendo/status/1594590628771688448".to_string(), ), ( "https://t.co/nopenope".to_string(), "https://www.nintendojo.fr/dojobar".to_string(), ), ( "https://t.co/broken".to_string(), "http://youtu.be".to_string(), ), ( "https://t.co/alsobroken".to_string(), "https://youtube.com".to_string(), ), ]); replace_alt_services(&mut associated_urls, &alt_services); assert_eq!(associated_urls, expected_urls); } }