Files
oolatoocs/src/lib.rs

261 lines
9.9 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use log::debug;
mod error;
pub use error::OolatoocsError;
mod config;
pub use config::{parse_toml, Config};
mod state;
use state::{delete_state, read_all_state, read_state, write_state, TootRecord};
pub use state::{init_db, migrate_db};
mod mastodon;
pub use mastodon::register;
use mastodon::{get_mastodon_instance, get_mastodon_timeline_since, get_status_edited_at};
mod utils;
use utils::{generate_multi_tweets, strip_everything};
mod bsky;
use bsky::{
build_post_record, generate_media_records, generate_quote_records, generate_webcard_records,
get_session, BskyReply,
};
use rusqlite::Connection;
#[tokio::main]
pub async fn run(config: &Config) {
let conn = Connection::open(&config.oolatoocs.db_path)
.unwrap_or_else(|e| panic!("Cannot open DB: {}", e));
let mastodon = get_mastodon_instance(&config.mastodon)
.unwrap_or_else(|e| panic!("Cannot instantiate Mastodon: {}", e));
let bluesky = get_session(&config.bluesky)
.await
.unwrap_or_else(|e| panic!("Cannot get Bsky session: {}", e));
let last_entry =
read_state(&conn, None).unwrap_or_else(|e| panic!("Cannot get last toot id: {}", e));
let last_toot_id: Option<u64> = match last_entry {
None => None, // Does not exist, this is the same as previously
Some(t) => {
match get_status_edited_at(&mastodon, t.toot_id).await {
None => Some(t.toot_id),
Some(d) => {
// a date has been found
if d > t.datetime.unwrap() {
debug!("Last toot date is posterior to the previously written tweet, deleting…");
let local_record_uris =
read_all_state(&conn, t.toot_id).unwrap_or_else(|e| {
panic!(
"Cannot fetch all records associated with Toot ID {}: {}",
t.toot_id, e
)
});
for local_record_uri in local_record_uris.into_iter() {
bluesky
.delete_record(&local_record_uri)
.await
.unwrap_or_else(|e| {
panic!("Cannot delete record ID ({}): {}", &t.record_uri, e)
});
}
delete_state(&conn, t.toot_id).unwrap_or_else(|e| {
panic!("Cannot delete Toot ID ({}): {}", t.toot_id, e)
});
read_state(&conn, None)
.unwrap_or_else(|e| panic!("Cannot get last toot id: {}", e))
.map(|a| a.toot_id)
} else {
Some(t.toot_id)
}
}
}
}
};
let timeline = get_mastodon_timeline_since(&mastodon, last_toot_id)
.await
.unwrap_or_else(|e| panic!("Cannot get instance: {}", e));
for toot in timeline {
// detecting tag #NoTweet and skipping the toot
if toot.tags.iter().any(|f| &f.name == "notweet") {
continue;
}
// form tweet_content and strip everything useless in it
let toot_tags: Vec<megalodon::entities::status::Tag> =
match &config.oolatoocs.remove_hashtags {
true => toot.tags.clone(),
false => vec![],
};
let Ok(mut tweet_content) =
strip_everything(&toot.content, &toot_tags, &config.mastodon.base)
else {
continue; // skip in case we cant strip something
};
// threads if necessary
let mut record_reply_to = toot.in_reply_to_id.and_then(|t| {
read_state(&conn, Some(t.parse::<u64>().unwrap()))
.ok()
.flatten()
.map(|s| BskyReply {
record_uri: s.record_uri.to_owned(),
root_record_uri: s.root_record_uri.to_owned(),
})
});
// 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;
// post the first half
let record = build_post_record(
&config.bluesky,
&first_half,
&toot.language,
None,
&record_reply_to,
)
.await
.unwrap_or_else(|e| panic!("Cannot create valid record for {}: {}", &toot.id, e));
let record_reply_id = bluesky.create_record(record).await.unwrap_or_else(|e| {
panic!(
"Cannot post the first half of {} for Bluesky: {}",
&toot.id, e
)
});
// write it to db
write_state(
&conn,
TootRecord {
toot_id: toot.id.parse::<u64>().unwrap(),
record_uri: record_reply_id.data.uri.to_owned(),
root_record_uri: record_reply_to
.as_ref()
.map_or(record_reply_id.data.uri.to_owned(), |v| {
v.root_record_uri.to_owned()
}),
datetime: None,
},
)
.unwrap_or_else(|e| {
panic!(
"Cannot store Toot/Tweet/Record ({}/{}): {}",
&toot.id, &record_reply_id.data.uri, e
)
});
record_reply_to = Some(BskyReply {
record_uri: record_reply_id.data.uri.to_owned(),
root_record_uri: record_reply_to
.as_ref()
.map_or(record_reply_id.data.uri.clone(), |v| {
v.root_record_uri.clone()
}),
});
};
// handle quote if any
let quote_embed = match toot.reblog {
Some(r) => {
let quote_record = read_state(&conn, Some(r.id.parse::<u64>().unwrap()));
match quote_record {
Ok(Some(q)) => generate_quote_records(&config.bluesky, &q.record_uri)
.await
.ok(),
_ => None,
}
}
None => None,
};
// handle medias if any
let media_embed = if toot.media_attachments.len() > usize::from(0u8) {
generate_media_records(&bluesky, &toot.media_attachments)
.await
.ok()
} else {
None
};
// handle webcard if any
let webcard_embed = match toot.card {
Some(t) => generate_webcard_records(&bluesky, &t).await.ok(),
None => None,
};
let record_embed = if quote_embed.is_some() {
if media_embed.is_some() {
let medias_mapped = match media_embed.unwrap() {
atrium_api::app::bsky::feed::post::RecordEmbedRefs::AppBskyEmbedImagesMain(a) => atrium_api::app::bsky::embed::record_with_media::MainMediaRefs::AppBskyEmbedImagesMain(a),
atrium_api::app::bsky::feed::post::RecordEmbedRefs::AppBskyEmbedVideoMain(a) => atrium_api::app::bsky::embed::record_with_media::MainMediaRefs::AppBskyEmbedVideoMain(a),
_ => continue, // this should NEVER happen as Media are either Video or
// Images at this point
};
let quote_mapped = match quote_embed.unwrap() {
atrium_api::app::bsky::feed::post::RecordEmbedRefs::AppBskyEmbedRecordMain(
a,
) => a,
_ => continue, // again, this should NEVER happen
};
Some(atrium_api::types::Union::Refs(
atrium_api::app::bsky::feed::post::RecordEmbedRefs::AppBskyEmbedRecordWithMediaMain(
Box::new(
atrium_api::app::bsky::embed::record_with_media::MainData {
media: atrium_api::types::Union::Refs(medias_mapped),
record: (*quote_mapped),
}.into()
)
)
))
} else {
quote_embed.map(atrium_api::types::Union::Refs)
}
} else if media_embed.is_some() {
media_embed.map(atrium_api::types::Union::Refs)
} else if webcard_embed.is_some() {
webcard_embed.map(atrium_api::types::Union::Refs)
} else {
None
};
// posts corresponding tweet
let record = build_post_record(
&config.bluesky,
&tweet_content,
&toot.language,
record_embed,
&record_reply_to,
)
.await
.unwrap_or_else(|e| panic!("Cannot build record for {}: {}", &toot.id, e));
let created_record = bluesky
.create_record(record)
.await
.unwrap_or_else(|e| panic!("Cannot put record {}: {}", &toot.id, e));
// writes the current state of the tweet
write_state(
&conn,
TootRecord {
toot_id: toot.id.parse::<u64>().unwrap(),
record_uri: created_record.data.uri.clone(),
root_record_uri: record_reply_to
.as_ref()
.map_or(created_record.data.uri.clone(), |v| {
v.root_record_uri.clone()
}),
datetime: None,
},
)
.unwrap_or_else(|e| panic!("Cannot store Toot/Tweet ({}): {}", &toot.id, e));
}
}