mirror of
https://framagit.org/veretcle/scootaloo.git
synced 2025-07-21 09:31:19 +02:00
158 lines
5.4 KiB
Rust
158 lines
5.4 KiB
Rust
mod error;
|
||
use error::ScootalooError;
|
||
|
||
mod config;
|
||
pub use config::parse_toml;
|
||
use config::Config;
|
||
|
||
mod mastodon;
|
||
pub use mastodon::register;
|
||
use mastodon::{build_basic_status, get_mastodon_token};
|
||
|
||
mod twitter;
|
||
use twitter::*;
|
||
|
||
mod util;
|
||
|
||
mod state;
|
||
pub use state::init_db;
|
||
use state::{read_state, write_state, TweetToToot};
|
||
|
||
use elefren::{prelude::*, status_builder::StatusBuilder};
|
||
use log::{debug, error, info, warn};
|
||
use rusqlite::Connection;
|
||
use std::borrow::Cow;
|
||
use tokio::fs::remove_file;
|
||
|
||
/// This is where the magic happens
|
||
#[tokio::main]
|
||
pub async fn run(config: Config) {
|
||
// open the SQLite connection
|
||
let conn = Connection::open(&config.scootaloo.db_path).unwrap_or_else(|e| {
|
||
panic!(
|
||
"Something went wrong when opening the DB {}: {}",
|
||
&config.scootaloo.db_path, e
|
||
)
|
||
});
|
||
// retrieve the last tweet ID for the username
|
||
let last_tweet_id = read_state(&conn, None)
|
||
.unwrap_or_else(|e| panic!("Cannot retrieve last_tweet_id: {}", e))
|
||
.map(|s| s.tweet_id);
|
||
|
||
// get OAuth2 token
|
||
let token = get_oauth2_token(&config.twitter);
|
||
|
||
// get Mastodon instance
|
||
let mastodon = get_mastodon_token(&config.mastodon);
|
||
|
||
// get user timeline feed (Vec<tweet>)
|
||
let mut feed = get_user_timeline(&config.twitter, token, last_tweet_id)
|
||
.await
|
||
.unwrap_or_else(|e| {
|
||
panic!(
|
||
"Something went wrong when trying to retrieve {}’s timeline: {}",
|
||
&config.twitter.username, e
|
||
)
|
||
});
|
||
|
||
// empty feed -> exiting
|
||
if feed.is_empty() {
|
||
info!("Nothing to retrieve since last time, exiting…");
|
||
return;
|
||
}
|
||
|
||
// order needs to be chronological
|
||
feed.reverse();
|
||
|
||
for tweet in &feed {
|
||
debug!("Treating Tweet {} inside feed", tweet.id);
|
||
// initiate the toot_reply_id var
|
||
let mut toot_reply_id: Option<String> = None;
|
||
// determine if the tweet is part of a thread (response to self) or a standard response
|
||
if let Some(r) = &tweet.in_reply_to_screen_name {
|
||
if r.to_lowercase() != config.twitter.username.to_lowercase() {
|
||
// we are responding not threading
|
||
info!("Tweet is a direct response, skipping");
|
||
continue;
|
||
}
|
||
info!("Tweet is a thread");
|
||
toot_reply_id = read_state(&conn, tweet.in_reply_to_status_id)
|
||
.unwrap_or(None)
|
||
.map(|s| s.toot_id);
|
||
};
|
||
|
||
// build basic status by just yielding text and dereferencing contained urls
|
||
let mut status_text = build_basic_status(tweet);
|
||
|
||
let mut status_medias: Vec<String> = vec![];
|
||
// 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).await {
|
||
Ok(m) => m,
|
||
Err(e) => {
|
||
error!("Cannot get tweet media for {}: {}", &media.url, e);
|
||
continue;
|
||
}
|
||
};
|
||
|
||
let mastodon_media_ids = match mastodon
|
||
.media(Cow::from(local_tweet_media_path.to_owned()))
|
||
{
|
||
Ok(m) => {
|
||
remove_file(&local_tweet_media_path)
|
||
.await
|
||
.unwrap_or_else(|e|
|
||
warn!("Attachment for {} has been uploaded, but I’m unable to remove the existing file: {}", &local_tweet_media_path, e)
|
||
);
|
||
m.id
|
||
}
|
||
Err(e) => {
|
||
error!(
|
||
"Attachment {} cannot be uploaded to Mastodon Instance: {}",
|
||
&local_tweet_media_path, e
|
||
);
|
||
continue;
|
||
}
|
||
};
|
||
|
||
status_medias.push(mastodon_media_ids);
|
||
|
||
// last step, removing the reference to the media from with the toot’s text
|
||
status_text = status_text.replace(&media.url, "");
|
||
}
|
||
}
|
||
// finished reuploading attachments, now let’s do the toot baby!
|
||
|
||
debug!("Building corresponding Mastodon status");
|
||
|
||
let mut status_builder = StatusBuilder::new();
|
||
|
||
status_builder.status(&status_text).media_ids(status_medias);
|
||
|
||
if let Some(i) = toot_reply_id {
|
||
status_builder.in_reply_to(&i);
|
||
}
|
||
|
||
let status = status_builder
|
||
.build()
|
||
.unwrap_or_else(|_| panic!("Cannot build status with text {}", &status_text));
|
||
|
||
// publish status
|
||
// again unwrap is safe here as we are in the main thread
|
||
let published_status = mastodon.new_status(status).unwrap();
|
||
// this will panic if it cannot publish the status, which is a good thing, it allows the
|
||
// last_tweet gathered not to be written
|
||
|
||
let ttt_towrite = TweetToToot {
|
||
tweet_id: tweet.id,
|
||
toot_id: published_status.id,
|
||
};
|
||
|
||
// write the current state (tweet ID and toot ID) to avoid copying it another time
|
||
write_state(&conn, ttt_towrite)
|
||
.unwrap_or_else(|e| panic!("Can’t write the last tweet retrieved: {}", e));
|
||
}
|
||
}
|