Merge branch '5-feat-repeat-mastodon-poll-in-twitter' into 'main'

feat: add poll from Mastodon to Twitter + pass owned values in post_tweet

Closes #5

See merge request veretcle/oolatoocs!7
This commit is contained in:
VC
2023-11-21 22:20:49 +00:00
5 changed files with 41 additions and 13 deletions

3
Cargo.lock generated
View File

@@ -972,8 +972,9 @@ checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
[[package]] [[package]]
name = "oolatoocs" name = "oolatoocs"
version = "1.4.0" version = "1.5.0"
dependencies = [ dependencies = [
"chrono",
"clap", "clap",
"env_logger", "env_logger",
"futures", "futures",

View File

@@ -1,11 +1,12 @@
[package] [package]
name = "oolatoocs" name = "oolatoocs"
version = "1.4.0" version = "1.5.0"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
chrono = "0.4.31"
clap = "^4" clap = "^4"
env_logger = "^0.10" env_logger = "^0.10"
futures = "^0.3" futures = "^0.3"

View File

@@ -13,11 +13,9 @@ What it can do:
* Cuts (poorly) the Toot in half in its too long for Twitter and thread it (this is cut using a word count, not the best method, but it gets the job done); * Cuts (poorly) the Toot in half in its 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 * Reuploads images/gifs/videos from Mastodon to Twitter
* Can reproduce threads 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 * Can prevent a Toot from being tweeted by using the #NoTweet (case-insensitive) hashtag in Mastodon
What it cant do:
* Poll (no idea on how to do it)
# Configuration file # Configuration file
The configuration is relatively easy to follow: The configuration is relatively easy to follow:

View File

@@ -18,7 +18,7 @@ use utils::{generate_multi_tweets, strip_everything};
mod twitter; mod twitter;
#[allow(unused_imports)] #[allow(unused_imports)]
use twitter::{generate_media_ids, post_tweet}; use twitter::{generate_media_ids, post_tweet, transform_poll};
use rusqlite::Connection; 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 the toot is too long, we cut it in half here
if let Some((first_half, second_half)) = generate_multi_tweets(&tweet_content) { if let Some((first_half, second_half)) = generate_multi_tweets(&tweet_content) {
tweet_content = second_half; 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 .await
.unwrap_or_else(|e| panic!("Cannot post the first half of {}: {}", &toot.id, e)); .unwrap_or_else(|e| panic!("Cannot post the first half of {}: {}", &toot.id, e));
reply_to = Some(reply_id); reply_to = Some(reply_id);
}; };
// treats poll if any
let in_poll = toot.poll.map(|p| transform_poll(&p));
// treats medias // treats medias
let medias = generate_media_ids(&config.twitter, &toot.media_attachments).await; let medias = generate_media_ids(&config.twitter, &toot.media_attachments).await;
// posts corresponding tweet // 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 .await
.unwrap_or_else(|e| panic!("Cannot Tweet {}: {}", toot.id, e)); .unwrap_or_else(|e| panic!("Cannot Tweet {}: {}", toot.id, e));

View File

@@ -1,8 +1,12 @@
use crate::config::TwitterConfig; use crate::config::TwitterConfig;
use crate::error::OolatoocsError; use crate::error::OolatoocsError;
use chrono::Utc;
use futures::{stream, StreamExt}; use futures::{stream, StreamExt};
use log::{debug, error, warn}; use log::{debug, error, warn};
use megalodon::entities::attachment::{Attachment, AttachmentType}; use megalodon::entities::{
attachment::{Attachment, AttachmentType},
Poll,
};
use oauth1_request::Token; use oauth1_request::Token;
use reqwest::{ use reqwest::{
multipart::{Form, Part}, multipart::{Form, Part},
@@ -28,6 +32,8 @@ struct Tweet {
media: Option<TweetMediasIds>, media: Option<TweetMediasIds>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
reply: Option<TweetReply>, reply: Option<TweetReply>,
#[serde(skip_serializing_if = "Option::is_none")]
poll: Option<TweetPoll>,
} }
#[derive(Serialize, Debug)] #[derive(Serialize, Debug)]
@@ -40,6 +46,12 @@ struct TweetReply {
in_reply_to_tweet_id: String, in_reply_to_tweet_id: String,
} }
#[derive(Serialize, Debug)]
pub struct TweetPoll {
pub options: Vec<String>,
pub duration_minutes: i64,
}
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
struct TweetResponse { struct TweetResponse {
data: TweetResponseData, data: TweetResponseData,
@@ -416,24 +428,37 @@ async fn upload_chunk_media(
Ok(orig_media_id.media_id) 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 /// This posts Tweets with all the associated medias
pub async fn post_tweet( pub async fn post_tweet(
config: &TwitterConfig, config: &TwitterConfig,
content: &str, content: String,
medias: &[u64], medias: Vec<u64>,
reply_to: &Option<u64>, reply_to: Option<u64>,
poll: Option<TweetPoll>,
) -> Result<u64, Box<dyn Error>> { ) -> Result<u64, Box<dyn Error>> {
let empty_request = EmptyRequest {}; // Why? Because fuck you, thats why! let empty_request = EmptyRequest {}; // Why? Because fuck you, thats why!
let token = get_token(config); let token = get_token(config);
let tweet = Tweet { let tweet = Tweet {
text: content.to_string(), text: content,
media: medias.is_empty().not().then(|| TweetMediasIds { media: medias.is_empty().not().then(|| TweetMediasIds {
media_ids: medias.iter().map(|m| m.to_string()).collect(), media_ids: medias.iter().map(|m| m.to_string()).collect(),
}), }),
reply: reply_to.map(|s| TweetReply { reply: reply_to.map(|s| TweetReply {
in_reply_to_tweet_id: s.to_string(), in_reply_to_tweet_id: s.to_string(),
}), }),
poll,
}; };
let client = Client::new(); let client = Client::new();