mirror of
https://framagit.org/veretcle/scootaloo.git
synced 2025-07-20 17:11:19 +02:00
Compare commits
16 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
3413f49d08 | ||
![]() |
d4fbccc69b | ||
![]() |
058a07865d | ||
![]() |
c6a29d1d7d | ||
![]() |
3692d6e51f | ||
![]() |
5fe57f189a | ||
![]() |
83c398cebf | ||
![]() |
8f567ed6b4 | ||
![]() |
d7431862ba | ||
![]() |
f3b13eb62f | ||
![]() |
6b68c8e299 | ||
![]() |
0bb5eabdac | ||
![]() |
3d44bbfb86 | ||
![]() |
9a03c7681b | ||
![]() |
a8a8f8c13f | ||
![]() |
90a9df220a |
40
Cargo.lock
generated
40
Cargo.lock
generated
@@ -776,15 +776,6 @@ dependencies = [
|
||||
"windows-sys 0.42.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "isolang"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b64fd6448ee8a45ce6e4365c58e4fa7d8740cba2ed70db3e9ab4879ebd93eaaa"
|
||||
dependencies = [
|
||||
"phf",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.5"
|
||||
@@ -858,9 +849,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "megalodon"
|
||||
version = "0.3.3"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e7bc4427c44dc9c4b439da7bcd15a87b51e21340a12ef5a4f7eac90fc247886"
|
||||
checksum = "3b39bd378603a46216457c1c95c54f8dec37959a02c06a59cc33d00fc71a0ba0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
@@ -1082,24 +1073,6 @@ version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259"
|
||||
dependencies = [
|
||||
"phf_shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_shared"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096"
|
||||
dependencies = [
|
||||
"siphasher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.9"
|
||||
@@ -1343,14 +1316,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "scootaloo"
|
||||
version = "1.1.3"
|
||||
version = "1.1.6"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"clap",
|
||||
"egg-mode",
|
||||
"futures",
|
||||
"html-escape",
|
||||
"isolang",
|
||||
"log",
|
||||
"megalodon",
|
||||
"mime",
|
||||
@@ -1523,12 +1495,6 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "0.3.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.7"
|
||||
|
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "scootaloo"
|
||||
version = "1.1.3"
|
||||
version = "1.1.6"
|
||||
authors = ["VC <veretcle+framagit@mateu.be>"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -14,10 +14,9 @@ toml = "^0.5"
|
||||
clap = "^4"
|
||||
egg-mode = "^0.16"
|
||||
rusqlite = "^0.27"
|
||||
isolang = "^2"
|
||||
tokio = { version = "^1", features = ["rt"]}
|
||||
futures = "^0.3"
|
||||
megalodon = "^0.3.3"
|
||||
megalodon = "^0.3.6"
|
||||
html-escape = "^0.2"
|
||||
reqwest = "^0.11"
|
||||
log = "^0.4"
|
||||
|
@@ -1,3 +1,9 @@
|
||||
**Due to the new Twitter policy about API v1.1 and API v2, this project will no longer be updated as it no longer can work for free any way, shape or form.**
|
||||
|
||||
First of all, I’m deeply sorry about that. It worked pretty great for what it did with a level of quality I’m very proud to have achieved.
|
||||
|
||||
Secondly, fuck you Musk, fuck you.
|
||||
|
||||
A Twitter to Mastodon copy bot written in Rust
|
||||
|
||||
It:
|
||||
@@ -25,7 +31,7 @@ rate_limiting = 4 ## optional, default 4, number of accounts handled simultaneou
|
||||
## this parameter allows you to catch such URLs and apply the `display_url` (i.e. `tout.es`) instead of the `expanded_url` (i.e. `http://tout.es`)
|
||||
## in those particular cases
|
||||
## (!) use with caution, it might have some undesired effects
|
||||
show_url_as_display_url_for = "^http(s)://(.+)\\.es$"
|
||||
show_url_as_display_url_for = "^https?://(.+)\\.es$"
|
||||
## optional, this allows you to replace the host for popular services such as YouTube of Twitter, or any other
|
||||
## with their more freely accessible equivalent
|
||||
[scootaloo.alternative_services_for]
|
||||
|
@@ -43,10 +43,10 @@ pub struct ScootalooConfig {
|
||||
/// Parses the TOML file into a Config Struct
|
||||
pub fn parse_toml(toml_file: &str) -> Config {
|
||||
let toml_config = read_to_string(toml_file)
|
||||
.unwrap_or_else(|e| panic!("Cannot open config file {}: {}", toml_file, e));
|
||||
.unwrap_or_else(|e| panic!("Cannot open config file {toml_file}: {e}"));
|
||||
|
||||
let config: Config = toml::from_str(&toml_config)
|
||||
.unwrap_or_else(|e| panic!("Cannot parse TOML file {}: {}", toml_file, e));
|
||||
.unwrap_or_else(|e| panic!("Cannot parse TOML file {toml_file}: {e}"));
|
||||
|
||||
config
|
||||
}
|
||||
|
@@ -30,12 +30,12 @@ impl Display for ScootalooError {
|
||||
|
||||
impl From<Box<dyn Error>> for ScootalooError {
|
||||
fn from(error: Box<dyn Error>) -> Self {
|
||||
ScootalooError::new(&format!("Error in a subset crate: {}", error))
|
||||
ScootalooError::new(&format!("Error in a subset crate: {error}"))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<megalodonError> for ScootalooError {
|
||||
fn from(error: megalodonError) -> Self {
|
||||
ScootalooError::new(&format!("Error in megalodon crate: {}", error))
|
||||
ScootalooError::new(&format!("Error in megalodon crate: {error}"))
|
||||
}
|
||||
}
|
||||
|
13
src/lib.rs
13
src/lib.rs
@@ -21,7 +21,6 @@ use state::{read_state, write_state, TweetToToot};
|
||||
|
||||
use futures::StreamExt;
|
||||
use html_escape::decode_html_entities;
|
||||
use isolang::Language;
|
||||
use log::info;
|
||||
use megalodon::{
|
||||
megalodon::PostStatusInputOptions, megalodon::UpdateCredentialsInputOptions, Megalodon,
|
||||
@@ -204,9 +203,7 @@ pub async fn run(config: Config) {
|
||||
|
||||
// language if any
|
||||
if let Some(l) = &tweet.lang {
|
||||
if let Some(r) = Language::from_639_1(l) {
|
||||
post_status.language = Some(r.to_string());
|
||||
}
|
||||
post_status.language = Some(l.to_string());
|
||||
}
|
||||
|
||||
// can be activated for test purposes
|
||||
@@ -238,8 +235,8 @@ pub async fn run(config: Config) {
|
||||
// launch and wait for every handle
|
||||
while let Some(result) = stream.next().await {
|
||||
match result {
|
||||
Ok(Err(e)) => eprintln!("Error within thread: {}", e),
|
||||
Err(e) => eprintln!("Error with thread: {}", e),
|
||||
Ok(Err(e)) => eprintln!("Error within thread: {e}"),
|
||||
Err(e) => eprintln!("Error with thread: {e}"),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
@@ -307,8 +304,8 @@ pub async fn profile(config: Config, bot: Option<bool>) {
|
||||
|
||||
while let Some(result) = stream.next().await {
|
||||
match result {
|
||||
Ok(Err(e)) => eprintln!("Error within thread: {}", e),
|
||||
Err(e) => eprintln!("Error with thread: {}", e),
|
||||
Ok(Err(e)) => eprintln!("Error within thread: {e}"),
|
||||
Err(e) => eprintln!("Error with thread: {e}"),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
@@ -63,10 +63,7 @@ fn main() {
|
||||
.short('c')
|
||||
.long("config")
|
||||
.value_name("CONFIG_FILE")
|
||||
.help(format!(
|
||||
"TOML config file for scootaloo (default {})",
|
||||
DEFAULT_CONFIG_PATH
|
||||
))
|
||||
.help("TOML config file for scootaloo")
|
||||
.default_value(DEFAULT_CONFIG_PATH)
|
||||
.num_args(1)
|
||||
.display_order(1),
|
||||
@@ -81,7 +78,7 @@ fn main() {
|
||||
.short('c')
|
||||
.long("config")
|
||||
.value_name("CONFIG_FILE")
|
||||
.help(format!("TOML config file for scootaloo (default {})", DEFAULT_CONFIG_PATH))
|
||||
.help("TOML config file for scootaloo")
|
||||
.default_value(DEFAULT_CONFIG_PATH)
|
||||
.num_args(1)
|
||||
.display_order(1),
|
||||
@@ -104,7 +101,7 @@ fn main() {
|
||||
.short('c')
|
||||
.long("config")
|
||||
.value_name("CONFIG_FILE")
|
||||
.help(format!("TOML config file for scootaloo (default {})", DEFAULT_CONFIG_PATH))
|
||||
.help("TOML config file for scootaloo")
|
||||
.default_value(DEFAULT_CONFIG_PATH)
|
||||
.num_args(1)
|
||||
.display_order(1),
|
||||
|
@@ -83,7 +83,7 @@ pub fn associate_urls(urls: &[UrlEntity], re: &Option<Regex>) -> HashMap<String,
|
||||
pub fn replace_alt_services(urls: &mut HashMap<String, String>, alts: &HashMap<String, String>) {
|
||||
for val in urls.values_mut() {
|
||||
for (k, v) in alts {
|
||||
*val = val.replace(&format!("/{}/", k), &format!("/{}/", v));
|
||||
*val = val.replace(&format!("/{k}/"), &format!("/{v}/"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,7 +120,7 @@ pub fn replace_tweet_by_toot(
|
||||
twitter_screen_name.to_lowercase(),
|
||||
tweet_id
|
||||
)) {
|
||||
*val = format!("{}/@{}/{}", base_url, mastodon_screen_name, toot_id);
|
||||
*val = format!("{base_url}/@{mastodon_screen_name}/{toot_id}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -184,7 +184,7 @@ pub async fn register(host: &str, screen_name: &str) {
|
||||
|
||||
let url = app_data.url.expect("Cannot generate registration URI!");
|
||||
|
||||
println!("Click this link to authorize on Mastodon: {}", url);
|
||||
println!("Click this link to authorize on Mastodon: {url}");
|
||||
println!("Paste the returned authorization code: ");
|
||||
|
||||
let mut input = String::new();
|
||||
|
@@ -21,8 +21,8 @@ pub fn read_state(
|
||||
) -> Result<Option<TweetToToot>, Box<dyn Error>> {
|
||||
debug!("Reading tweet_id {:?}", s);
|
||||
let query: String = match s {
|
||||
Some(i) => format!("SELECT * FROM tweet_to_toot WHERE tweet_id = {} and twitter_screen_name = \"{}\"", i, n),
|
||||
None => format!("SELECT * FROM tweet_to_toot WHERE twitter_screen_name = \"{}\" ORDER BY tweet_id DESC LIMIT 1", n),
|
||||
Some(i) => format!("SELECT * FROM tweet_to_toot WHERE tweet_id = {i} and twitter_screen_name = \"{n}\""),
|
||||
None => format!("SELECT * FROM tweet_to_toot WHERE twitter_screen_name = \"{n}\" ORDER BY tweet_id DESC LIMIT 1"),
|
||||
};
|
||||
|
||||
let mut stmt = conn.prepare(&query)?;
|
||||
@@ -78,8 +78,7 @@ pub fn migrate_db(d: &str, s: &str) -> Result<(), Box<dyn Error>> {
|
||||
&format!(
|
||||
"ALTER TABLE tweet_to_toot
|
||||
ADD COLUMN twitter_screen_name TEXT NOT NULL
|
||||
DEFAULT \"{}\"",
|
||||
s
|
||||
DEFAULT \"{s}\""
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
34
src/util.rs
34
src/util.rs
@@ -6,6 +6,7 @@ use futures::{stream, stream::StreamExt};
|
||||
use log::{error, info, warn};
|
||||
use megalodon::{
|
||||
entities::UploadMedia::{AsyncAttachment, Attachment},
|
||||
error,
|
||||
mastodon::Mastodon,
|
||||
megalodon::Megalodon,
|
||||
};
|
||||
@@ -14,6 +15,7 @@ use std::error::Error;
|
||||
use tokio::{
|
||||
fs::{create_dir_all, remove_file, File},
|
||||
io::copy,
|
||||
time::{sleep, Duration},
|
||||
};
|
||||
|
||||
/// Generate associative table between media ids and tweet extended entities
|
||||
@@ -55,7 +57,7 @@ pub async fn generate_media_ids(
|
||||
|
||||
let id = match mastodon_media {
|
||||
Attachment(m) => m.id,
|
||||
AsyncAttachment(m) => m.id,
|
||||
AsyncAttachment(m) => wait_until_uploaded(&mastodon, &m.id).await?,
|
||||
};
|
||||
|
||||
Ok::<String, ScootalooError>(id)
|
||||
@@ -81,6 +83,24 @@ pub async fn generate_media_ids(
|
||||
(media_url, media_ids)
|
||||
}
|
||||
|
||||
/// Wait on uploaded medias when necessary
|
||||
async fn wait_until_uploaded(client: &Mastodon, id: &str) -> Result<String, error::Error> {
|
||||
loop {
|
||||
sleep(Duration::from_secs(1)).await;
|
||||
let res = client.get_media(id.to_string()).await;
|
||||
return match res {
|
||||
Ok(res) => Ok(res.json.id),
|
||||
Err(err) => match err {
|
||||
error::Error::OwnError(ref own_err) => match own_err.kind {
|
||||
error::Kind::HTTPPartialContentError => continue,
|
||||
_ => Err(err),
|
||||
},
|
||||
_ => Err(err),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Transforms the media into a base64 equivalent
|
||||
pub async fn base64_media(u: &str) -> Result<String, Box<dyn Error>> {
|
||||
let mut response = reqwest::get(u).await?;
|
||||
@@ -94,12 +114,12 @@ pub async fn base64_media(u: &str) -> Result<String, Box<dyn Error>> {
|
||||
let content_type = response
|
||||
.headers()
|
||||
.get("content-type")
|
||||
.ok_or_else(|| ScootalooError::new(&format!("Cannot get media content type for {}", u)))?
|
||||
.ok_or_else(|| ScootalooError::new(&format!("Cannot get media content type for {u}")))?
|
||||
.to_str()?;
|
||||
|
||||
let encoded_f = encode(buffer);
|
||||
|
||||
Ok(format!("data:{};base64,{}", content_type, encoded_f))
|
||||
Ok(format!("data:{content_type};base64,{encoded_f}"))
|
||||
}
|
||||
|
||||
/// Gets and caches Twitter Media inside the determined temp dir
|
||||
@@ -116,19 +136,17 @@ pub async fn cache_media(u: &str, t: &str) -> Result<String, Box<dyn Error>> {
|
||||
.path_segments()
|
||||
.ok_or_else(|| {
|
||||
ScootalooError::new(&format!(
|
||||
"Cannot determine the destination filename for {}",
|
||||
u
|
||||
"Cannot determine the destination filename for {u}"
|
||||
))
|
||||
})?
|
||||
.last()
|
||||
.ok_or_else(|| {
|
||||
ScootalooError::new(&format!(
|
||||
"Cannot determine the destination filename for {}",
|
||||
u
|
||||
"Cannot determine the destination filename for {u}"
|
||||
))
|
||||
})?;
|
||||
|
||||
let dest_filepath = format!("{}/{}", t, dest_filename);
|
||||
let dest_filepath = format!("{t}/{dest_filename}");
|
||||
|
||||
let mut dest_file = File::create(&dest_filepath).await?;
|
||||
|
||||
|
Reference in New Issue
Block a user