mirror of
https://framagit.org/veretcle/scootaloo.git
synced 2025-07-20 17:11:19 +02:00
Version with functionning media upload
This commit is contained in:
154
src/lib.rs
154
src/lib.rs
@@ -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))
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Generic private functions
|
||||||
*/
|
*/
|
||||||
|
fn cache_media(u: &str, t: &str) -> Result<String, Box<dyn Error>> {
|
||||||
|
// create dir
|
||||||
|
if !Path::new(t).is_dir() {
|
||||||
|
create_dir_all(t)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// decode urls from UrlEntities
|
// get file
|
||||||
fn decode_urls(urls: &Vec<UrlEntity>) -> HashMap<String, String> {
|
let client = Client::new();
|
||||||
let mut decoded_urls = HashMap::new();
|
let mut response = client.get(u).send()?;
|
||||||
|
|
||||||
for url in urls {
|
// create local file
|
||||||
if url.expanded_url.is_some() {
|
let dest_filename = match response.url()
|
||||||
// unwrap is safe here as we just verified that there is something inside expanded_url
|
.path_segments()
|
||||||
decoded_urls.insert(String::from(&url.url), String::from(url.expanded_url.as_deref().unwrap()));
|
.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())));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
decoded_urls
|
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 I’m 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 toot’s 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!("Can’t write the last tweet retrieved: {}", e)
|
panic!("Can’t write the last tweet retrieved: {}", e)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user