diff --git a/Cargo.lock b/Cargo.lock index e6decbc..141f238 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1058,7 +1058,7 @@ checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "oolatoocs" -version = "0.1.0" +version = "0.2.0" dependencies = [ "clap", "dissolve", diff --git a/Cargo.toml b/Cargo.toml index 71e8782..a8073ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oolatoocs" -version = "0.1.0" +version = "0.2.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -13,7 +13,7 @@ log = "^0.4" megalodon = "^0.11" oauth1-request = "^0.6" regex = "1.10.2" -reqwest = { version = "0.11.22", features = ["json"] } +reqwest = { version = "0.11.22", features = ["json", "stream", "multipart"] } rusqlite = "^0.27" serde = { version = "^1.0", features = ["derive"] } tokio = { version = "^1.33", features = ["rt-multi-thread", "macros"] } diff --git a/src/lib.rs b/src/lib.rs index efc4c63..c864dc0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,9 +36,19 @@ pub async fn run(config: &Config) { let Ok(tweet_content) = strip_everything(&toot.content, &toot.tags) else { continue; // skip in case we can’t strip something }; + let mut medias: Vec = vec![]; + // if we wanted to cut toot in half, now would be the right time to do so - // treating medias (nothing for now) - let tweet_id = post_tweet(&config.twitter, &tweet_content, &[]) + + for media in toot.media_attachments { + let Ok(id) = upload_media(&config.twitter, &media.url, &media.description).await else { + continue; + }; + + medias.push(id); + } + + let tweet_id = post_tweet(&config.twitter, &tweet_content, &medias) .await .unwrap_or_else(|e| panic!("Cannot Tweet {}: {}", toot.id, e)); diff --git a/src/twitter.rs b/src/twitter.rs index 283d2d7..332f4cc 100644 --- a/src/twitter.rs +++ b/src/twitter.rs @@ -1,6 +1,9 @@ use crate::config::TwitterConfig; use oauth1_request::Token; -use reqwest::Client; +use reqwest::{ + multipart::{Form, Part}, + Body, Client, +}; use serde::{Deserialize, Serialize}; use std::error::Error; @@ -11,6 +14,12 @@ struct EmptyRequest {} #[derive(Serialize, Debug)] pub struct Tweet { pub text: String, + pub media: TweetMediasIds, +} + +#[derive(Serialize, Debug)] +pub struct TweetMediasIds { + pub media_ids: Vec, } #[derive(Deserialize, Debug)] @@ -23,6 +32,22 @@ pub struct TweetResponseData { pub id: String, } +#[derive(Deserialize, Debug)] +struct UploadMediaResponse { + media_id: u64, +} + +#[derive(Serialize, Debug)] +struct MediaMetadata { + media_id: u64, + alt_text: MediaMetadataAltText, +} + +#[derive(Serialize, Debug)] +struct MediaMetadataAltText { + text: String, +} + /// This function returns the OAuth1 Token object from TwitterConfig fn get_token(config: &TwitterConfig) -> Token { oauth1_request::Token::from_parts( @@ -35,15 +60,68 @@ fn get_token(config: &TwitterConfig) -> Token { /// This function uploads media from Mastodon to Twitter and returns the media id from Twitter #[allow(dead_code)] -pub async fn upload_media(_u: &str) -> Result> { - Ok(0) +pub async fn upload_media( + config: &TwitterConfig, + u: &str, + d: &Option, +) -> Result> { + // initiate request parameters + let uri = "https://upload.twitter.com/1.1/media/upload.json"; + let empty_request = EmptyRequest {}; // Why? Because fuck you, that’s why! + let token = get_token(config); + + // retrieve the length, type and bytes stream from the given URL + let dl = reqwest::get(u).await?; + let content_length = dl + .content_length() + .ok_or(format!("Cannot get content length for {}", u))?; + let stream = dl.bytes_stream(); + + // upload the media + let client = Client::new(); + let res: UploadMediaResponse = client + .post(uri) + .header( + "Authorization", + oauth1_request::post(uri, &empty_request, &token, oauth1_request::HMAC_SHA1), + ) + .multipart(Form::new().part( + "media", + Part::stream_with_length(Body::wrap_stream(stream), content_length), + )) + .send() + .await? + .json() + .await?; + + // update the metadata + if let Some(metadata) = d { + let uri = "https://upload.twitter.com/1.1/media/metadata/create.json"; + let media_metadata = MediaMetadata { + media_id: res.media_id, + alt_text: MediaMetadataAltText { + text: metadata.to_string(), + }, + }; + let _metadata = client + .post(uri) + .header( + "Authorization", + oauth1_request::post(uri, &empty_request, &token, oauth1_request::HMAC_SHA1), + ) + .json(&media_metadata) + .send() + .await?; + } + + Ok(res.media_id) } /// This posts Tweets with all the associated medias pub async fn post_tweet( config: &TwitterConfig, content: &str, - _medias: &[u64], + medias: &[u64], ) -> Result> { let uri = "https://api.twitter.com/2/tweets"; let empty_request = EmptyRequest {}; // Why? Because fuck you, that’s why! @@ -51,6 +129,9 @@ pub async fn post_tweet( let tweet = Tweet { text: content.to_string(), + media: TweetMediasIds { + media_ids: medias.iter().map(|m| m.to_string()).collect(), + }, }; let client = Client::new();