diff --git a/Cargo.lock b/Cargo.lock index b6e6237..181fbfd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -972,8 +972,9 @@ checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "oolatoocs" -version = "1.4.0" +version = "1.5.0" dependencies = [ + "chrono", "clap", "env_logger", "futures", diff --git a/Cargo.toml b/Cargo.toml index 70884ad..7e76e84 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,12 @@ [package] name = "oolatoocs" -version = "1.4.0" +version = "1.5.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +chrono = "0.4.31" clap = "^4" env_logger = "^0.10" futures = "^0.3" diff --git a/README.md b/README.md index 6e4265c..24123d6 100644 --- a/README.md +++ b/README.md @@ -13,11 +13,9 @@ What it can do: * Cuts (poorly) the Toot in half in it’s too long for Twitter and thread it (this is cut using a word count, not the best method, but it gets the job done); * Reuploads images/gifs/videos from Mastodon to Twitter * Can reproduce threads from Mastodon to Twitter +* Can reproduce poll from Mastodon to Twitter * Can prevent a Toot from being tweeted by using the #NoTweet (case-insensitive) hashtag in Mastodon -What it can’t do: -* Poll (no idea on how to do it) - # Configuration file The configuration is relatively easy to follow: diff --git a/src/lib.rs b/src/lib.rs index c493b2c..ca73d0c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,7 +18,7 @@ use utils::{generate_multi_tweets, strip_everything}; mod twitter; #[allow(unused_imports)] -use twitter::{generate_media_ids, post_tweet}; +use twitter::{generate_media_ids, post_tweet, transform_poll}; use rusqlite::Connection; @@ -57,17 +57,20 @@ pub async fn run(config: &Config) { // if the toot is too long, we cut it in half here if let Some((first_half, second_half)) = generate_multi_tweets(&tweet_content) { tweet_content = second_half; - let reply_id = post_tweet(&config.twitter, &first_half, &[], &reply_to) + let reply_id = post_tweet(&config.twitter, first_half, vec![], reply_to, None) .await .unwrap_or_else(|e| panic!("Cannot post the first half of {}: {}", &toot.id, e)); reply_to = Some(reply_id); }; + // treats poll if any + let in_poll = toot.poll.map(|p| transform_poll(&p)); + // treats medias let medias = generate_media_ids(&config.twitter, &toot.media_attachments).await; // posts corresponding tweet - let tweet_id = post_tweet(&config.twitter, &tweet_content, &medias, &reply_to) + let tweet_id = post_tweet(&config.twitter, tweet_content, medias, reply_to, in_poll) .await .unwrap_or_else(|e| panic!("Cannot Tweet {}: {}", toot.id, e)); diff --git a/src/twitter.rs b/src/twitter.rs index 264d51b..149bfe7 100644 --- a/src/twitter.rs +++ b/src/twitter.rs @@ -1,8 +1,12 @@ use crate::config::TwitterConfig; use crate::error::OolatoocsError; +use chrono::Utc; use futures::{stream, StreamExt}; use log::{debug, error, warn}; -use megalodon::entities::attachment::{Attachment, AttachmentType}; +use megalodon::entities::{ + attachment::{Attachment, AttachmentType}, + Poll, +}; use oauth1_request::Token; use reqwest::{ multipart::{Form, Part}, @@ -28,6 +32,8 @@ struct Tweet { media: Option, #[serde(skip_serializing_if = "Option::is_none")] reply: Option, + #[serde(skip_serializing_if = "Option::is_none")] + poll: Option, } #[derive(Serialize, Debug)] @@ -40,6 +46,12 @@ struct TweetReply { in_reply_to_tweet_id: String, } +#[derive(Serialize, Debug)] +pub struct TweetPoll { + pub options: Vec, + pub duration_minutes: i64, +} + #[derive(Deserialize, Debug)] struct TweetResponse { data: TweetResponseData, @@ -416,24 +428,37 @@ async fn upload_chunk_media( Ok(orig_media_id.media_id) } +pub fn transform_poll(p: &Poll) -> TweetPoll { + let poll_end_datetime = p.expires_at.unwrap(); // should be safe at this point + let now = Utc::now(); + let diff = poll_end_datetime.signed_duration_since(now); + + TweetPoll { + options: p.options.iter().map(|i| i.title.clone()).collect(), + duration_minutes: diff.num_minutes(), + } +} + /// This posts Tweets with all the associated medias pub async fn post_tweet( config: &TwitterConfig, - content: &str, - medias: &[u64], - reply_to: &Option, + content: String, + medias: Vec, + reply_to: Option, + poll: Option, ) -> Result> { let empty_request = EmptyRequest {}; // Why? Because fuck you, that’s why! let token = get_token(config); let tweet = Tweet { - text: content.to_string(), + text: content, media: medias.is_empty().not().then(|| TweetMediasIds { media_ids: medias.iter().map(|m| m.to_string()).collect(), }), reply: reply_to.map(|s| TweetReply { in_reply_to_tweet_id: s.to_string(), }), + poll, }; let client = Client::new();