Version with functionning media upload

This commit is contained in:
VC
2020-03-01 20:42:06 +01:00
parent d7cdd9f8bf
commit c39784fca7

View File

@@ -1,10 +1,11 @@
// std // std
use std::{ use std::{
path::Path,
borrow::Cow, borrow::Cow,
collections::HashMap, collections::HashMap,
io, io::{stdin, copy},
fmt, fmt,
fs::{read_to_string, write}, fs::{read_to_string, write, create_dir_all, File, remove_file},
error::Error, error::Error,
}; };
@@ -15,7 +16,7 @@ use serde::Deserialize;
use egg_mode::{ use egg_mode::{
Token, Token,
KeyPair, KeyPair,
entities::UrlEntity, entities::{UrlEntity, MediaEntity, MediaType},
tweet::{ tweet::{
Tweet, Tweet,
user_timeline, user_timeline,
@@ -24,9 +25,17 @@ use egg_mode::{
use tokio::runtime::current_thread::block_on_all; use tokio::runtime::current_thread::block_on_all;
// mammut // mammut
use mammut::{Mastodon, Data, Registration}; use mammut::{
use mammut::apps::{AppBuilder, Scopes}; Mastodon,
use mammut::status_builder::StatusBuilder; Data,
Registration,
apps::{AppBuilder, Scopes},
status_builder::StatusBuilder,
media_builder::MediaBuilder,
};
// reqwest
use reqwest::blocking::Client;
/********** /**********
* Generic usage functions * Generic usage functions
@@ -71,6 +80,44 @@ fn get_user_timeline(config: &Config, token: Token, lid: Option<u64>) -> Result<
Ok(feed.to_vec()) Ok(feed.to_vec())
} }
/// decode urls from UrlEntities
fn decode_urls(urls: &Vec<UrlEntity>) -> HashMap<String, String> {
let mut decoded_urls = HashMap::new();
for url in urls {
if url.expanded_url.is_some() {
// unwrap is safe here as we just verified that there is something inside expanded_url
decoded_urls.insert(String::from(&url.url), String::from(url.expanded_url.as_deref().unwrap()));
}
}
decoded_urls
}
/// Retrieve a single media from a tweet and store it in a temporary file
fn get_tweet_media(m: &MediaEntity, t: &str) -> Result<String, Box<dyn Error>> {
match m.media_type {
MediaType::Photo => {
return cache_media(&m.media_url_https, t);
},
_ => {
match &m.video_info {
Some(v) => {
for variant in &v.variants {
if variant.content_type == "video/mp4" {
return cache_media(&variant.url, t);
}
}
return Err(Box::new(ScootalooError::new(format!("Media Type for {} is video but no mp4 file URL is available", &m.url).as_str())));
},
None => {
return Err(Box::new(ScootalooError::new(format!("Media Type for {} is video but does not contain any video_info", &m.url).as_str())));
},
}
},
};
}
/* /*
* Those functions are related to the Mastodon side of things * Those functions are related to the Mastodon side of things
*/ */
@@ -88,7 +135,7 @@ fn get_mastodon_token(masto: &MastodonConfig) -> Mastodon {
} }
/// build toot from tweet /// build toot from tweet
fn build_status(tweet: &Tweet) -> Result<StatusBuilder, Box<dyn Error>> { fn build_basic_status(tweet: &Tweet) -> Result<StatusBuilder, Box<dyn Error>> {
let mut toot = String::from(&tweet.text); let mut toot = String::from(&tweet.text);
let decoded_urls = decode_urls(&tweet.entities.urls); let decoded_urls = decode_urls(&tweet.entities.urls);
@@ -97,26 +144,39 @@ fn build_status(tweet: &Tweet) -> Result<StatusBuilder, Box<dyn Error>> {
toot = toot.replace(&decoded_url.0, &decoded_url.1); toot = toot.replace(&decoded_url.0, &decoded_url.1);
} }
println!("{:#?}", tweet);
Err(Box::new(ScootalooError::new("Error")))
/*
Ok(StatusBuilder::new(toot)) Ok(StatusBuilder::new(toot))
*/
} }
/// decode urls from UrlEntities /*
fn decode_urls(urls: &Vec<UrlEntity>) -> HashMap<String, String> { * Generic private functions
let mut decoded_urls = HashMap::new(); */
fn cache_media(u: &str, t: &str) -> Result<String, Box<dyn Error>> {
for url in urls { // create dir
if url.expanded_url.is_some() { if !Path::new(t).is_dir() {
// unwrap is safe here as we just verified that there is something inside expanded_url create_dir_all(t)?;
decoded_urls.insert(String::from(&url.url), String::from(url.expanded_url.as_deref().unwrap()));
}
} }
decoded_urls // get file
let client = Client::new();
let mut response = client.get(u).send()?;
// create local file
let dest_filename = match response.url()
.path_segments()
.and_then(|segments| segments.last()) {
Some(r) => r,
None => {
return Err(Box::new(ScootalooError::new(format!("Cannot determine the destination filename for {}", u).as_str())));
},
};
let dest_filepath = format!("{}/{}", t, dest_filename);
let mut dest_file = File::create(&dest_filepath)?;
copy(&mut response, &mut dest_file)?;
Ok(dest_filepath)
} }
/********** /**********
@@ -155,6 +215,7 @@ impl std::error::Error for ScootalooError {
pub struct Config { pub struct Config {
twitter: TwitterConfig, twitter: TwitterConfig,
mastodon: MastodonConfig, mastodon: MastodonConfig,
scootaloo: ScootalooConfig,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@@ -164,7 +225,6 @@ struct TwitterConfig {
consumer_secret: String, consumer_secret: String,
access_key: String, access_key: String,
access_secret: String, access_secret: String,
last_tweet_path: String,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@@ -176,6 +236,12 @@ struct MastodonConfig {
token: String, token: String,
} }
#[derive(Debug, Deserialize)]
struct ScootalooConfig {
last_tweet_path: String,
cache_path: String,
}
/********* /*********
* Main functions * Main functions
*********/ *********/
@@ -211,7 +277,7 @@ pub fn register(host: &str) {
println!("Paste the returned authorization code: "); println!("Paste the returned authorization code: ");
let mut input = String::new(); let mut input = String::new();
io::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 code = input.trim();
let mastodon = registration.create_access_token(code.to_string()).expect("Unable to create access token!"); let mastodon = registration.create_access_token(code.to_string()).expect("Unable to create access token!");
@@ -224,7 +290,7 @@ pub fn register(host: &str) {
/// This is where the magic happens /// This is where the magic happens
pub fn run(config: Config) { pub fn run(config: Config) {
// retrieve the last tweet ID for the username // retrieve the last tweet ID for the username
let last_tweet_id = read_state(&config.twitter.last_tweet_path); let last_tweet_id = read_state(&config.scootaloo.last_tweet_path);
// get OAuth2 token // get OAuth2 token
let token = get_oauth2_token(&config); let token = get_oauth2_token(&config);
@@ -247,7 +313,8 @@ pub fn run(config: Config) {
feed.reverse(); feed.reverse();
for tweet in &feed { for tweet in &feed {
let status = match build_status(tweet) { // build basic status by just yielding text and dereferencing contained urls
let mut status = match build_basic_status(tweet) {
Ok(t) => t, Ok(t) => t,
Err(e) => { Err(e) => {
println!("Could not create status from tweet {}: {}", tweet.id ,e); println!("Could not create status from tweet {}: {}", tweet.id ,e);
@@ -255,11 +322,46 @@ pub fn run(config: Config) {
}, },
}; };
// 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) {
Ok(m) => m,
Err(e) => {
println!("Cannot get tweet media for {}: {}", &media.url, e);
continue;
},
};
let mastodon_media_ids = match mastodon.media(MediaBuilder::new(Cow::from(String::from(&local_tweet_media_path)))) {
Ok(m) => {
remove_file(&local_tweet_media_path).unwrap_or_else(|e|
println!("Attachment for {} has been upload, but Im unable to remove the existing file: {}", &local_tweet_media_path, e)
);
m.id
},
Err(e) => {
println!("Cannot attach media {} to Mastodon Instance: {}", &local_tweet_media_path, e);
continue;
}
};
// media has been successfully uploaded, adding it to the toot
match status.media_ids {
Some(ref mut i) => i.push(mastodon_media_ids),
None => status.media_ids = Some(vec![mastodon_media_ids]),
};
// last step, removing the reference to the media from with the toots text
status.status = status.status.replace(&media.url, "");
}
}
// publish status // publish status
mastodon.new_status(status).unwrap(); mastodon.new_status(status).unwrap();
// write the current state (tweet ID) to avoid copying it another time // write the current state (tweet ID) to avoid copying it another time
write_state(&config.twitter.last_tweet_path, tweet.id).unwrap_or_else(|e| write_state(&config.scootaloo.last_tweet_path, tweet.id).unwrap_or_else(|e|
panic!("Cant write the last tweet retrieved: {}", e) panic!("Cant write the last tweet retrieved: {}", e)
); );
} }