mirror of
https://framagit.org/veretcle/scootaloo.git
synced 2025-07-21 17:34:37 +02:00
Compare commits
16 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
9dd6ab8370 | ||
![]() |
4679578101 | ||
![]() |
2501d5990f | ||
![]() |
cb36730151 | ||
![]() |
a9942fad5c | ||
![]() |
522d4e3ea5 | ||
![]() |
91e3cd04a0 | ||
![]() |
87a7574d42 | ||
![]() |
18e8b9d306 | ||
![]() |
1e9c768a74 | ||
![]() |
83a133bb86 | ||
![]() |
92d5fdffad | ||
![]() |
331adec60f | ||
![]() |
9a341310da | ||
![]() |
2c77a0e5fc | ||
![]() |
032e3cf8dd |
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -2103,7 +2103,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scootaloo"
|
name = "scootaloo"
|
||||||
version = "0.9.3"
|
version = "0.10.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "scootaloo"
|
name = "scootaloo"
|
||||||
version = "0.9.3"
|
version = "0.10.1"
|
||||||
authors = ["VC <veretcle+framagit@mateu.be>"]
|
authors = ["VC <veretcle+framagit@mateu.be>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
|
@@ -48,6 +48,7 @@ This will give you the end of the TOML file. It will look like this:
|
|||||||
```toml
|
```toml
|
||||||
[mastodon.nintendojofr] ## account
|
[mastodon.nintendojofr] ## account
|
||||||
twitter_screen_name = "NintendojoFR" ## User Timeline to copy
|
twitter_screen_name = "NintendojoFR" ## User Timeline to copy
|
||||||
|
mastodon_screen_name = "nintendojofr" ## optional, Mastodon account name used for smart mentions
|
||||||
base = "https://m.nintendojo.fr"
|
base = "https://m.nintendojo.fr"
|
||||||
client_id = "MYCLIENTID"
|
client_id = "MYCLIENTID"
|
||||||
client_secret = "MYCLIENTSECRET"
|
client_secret = "MYCLIENTSECRET"
|
||||||
|
@@ -22,6 +22,7 @@ pub struct TwitterConfig {
|
|||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct MastodonConfig {
|
pub struct MastodonConfig {
|
||||||
pub twitter_screen_name: String,
|
pub twitter_screen_name: String,
|
||||||
|
pub mastodon_screen_name: Option<String>,
|
||||||
pub twitter_page_size: Option<i32>,
|
pub twitter_page_size: Option<i32>,
|
||||||
pub base: String,
|
pub base: String,
|
||||||
pub client_id: String,
|
pub client_id: String,
|
||||||
|
36
src/lib.rs
36
src/lib.rs
@@ -19,14 +19,13 @@ mod state;
|
|||||||
pub use state::{init_db, migrate_db};
|
pub use state::{init_db, migrate_db};
|
||||||
use state::{read_state, write_state, TweetToToot};
|
use state::{read_state, write_state, TweetToToot};
|
||||||
|
|
||||||
use elefren::{prelude::*, status_builder::StatusBuilder};
|
use elefren::{prelude::*, status_builder::StatusBuilder, Language};
|
||||||
|
use futures::StreamExt;
|
||||||
use log::info;
|
use log::info;
|
||||||
use rusqlite::Connection;
|
use rusqlite::Connection;
|
||||||
use std::sync::Arc;
|
use std::{collections::HashMap, sync::Arc};
|
||||||
use tokio::{spawn, sync::Mutex};
|
use tokio::{spawn, sync::Mutex};
|
||||||
|
|
||||||
use futures::StreamExt;
|
|
||||||
|
|
||||||
const DEFAULT_RATE_LIMIT: usize = 4;
|
const DEFAULT_RATE_LIMIT: usize = 4;
|
||||||
const DEFAULT_PAGE_SIZE: i32 = 200;
|
const DEFAULT_PAGE_SIZE: i32 = 200;
|
||||||
|
|
||||||
@@ -43,6 +42,22 @@ pub async fn run(config: Config) {
|
|||||||
}),
|
}),
|
||||||
));
|
));
|
||||||
|
|
||||||
|
let scootaloo_mentions: HashMap<String, String> = config
|
||||||
|
.mastodon
|
||||||
|
.values()
|
||||||
|
.filter(|s| s.mastodon_screen_name.is_some())
|
||||||
|
.map(|s| {
|
||||||
|
(
|
||||||
|
format!("@{}", s.twitter_screen_name),
|
||||||
|
format!(
|
||||||
|
"@{}@{}",
|
||||||
|
s.mastodon_screen_name.as_ref().unwrap(),
|
||||||
|
s.base.split('/').last().unwrap()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
let mut stream = futures::stream::iter(config.mastodon.into_values())
|
let mut stream = futures::stream::iter(config.mastodon.into_values())
|
||||||
.map(|mastodon_config| {
|
.map(|mastodon_config| {
|
||||||
// calculate Twitter page size
|
// calculate Twitter page size
|
||||||
@@ -52,12 +67,12 @@ pub async fn run(config: Config) {
|
|||||||
|
|
||||||
// create temporary value for each task
|
// create temporary value for each task
|
||||||
let scootaloo_cache_path = config.scootaloo.cache_path.clone();
|
let scootaloo_cache_path = config.scootaloo.cache_path.clone();
|
||||||
|
let scootaloo_mentions = scootaloo_mentions.clone();
|
||||||
let token = get_oauth2_token(&config.twitter);
|
let token = get_oauth2_token(&config.twitter);
|
||||||
let task_conn = conn.clone();
|
let task_conn = conn.clone();
|
||||||
|
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
info!("Starting treating {}", &mastodon_config.twitter_screen_name);
|
info!("Starting treating {}", &mastodon_config.twitter_screen_name);
|
||||||
|
|
||||||
// retrieve the last tweet ID for the username
|
// retrieve the last tweet ID for the username
|
||||||
let lconn = task_conn.lock().await;
|
let lconn = task_conn.lock().await;
|
||||||
let last_tweet_id = read_state(&lconn, &mastodon_config.twitter_screen_name, None)?
|
let last_tweet_id = read_state(&lconn, &mastodon_config.twitter_screen_name, None)?
|
||||||
@@ -78,6 +93,7 @@ pub async fn run(config: Config) {
|
|||||||
|
|
||||||
for tweet in &feed {
|
for tweet in &feed {
|
||||||
info!("Treating Tweet {} inside feed", tweet.id);
|
info!("Treating Tweet {} inside feed", tweet.id);
|
||||||
|
|
||||||
let lconn = task_conn.lock().await;
|
let lconn = task_conn.lock().await;
|
||||||
// initiate the toot_reply_id var and retrieve the corresponding toot_id
|
// initiate the toot_reply_id var and retrieve the corresponding toot_id
|
||||||
let toot_reply_id: Option<String> = tweet.in_reply_to_user_id.and_then(|_| {
|
let toot_reply_id: Option<String> = tweet.in_reply_to_user_id.and_then(|_| {
|
||||||
@@ -92,7 +108,7 @@ pub async fn run(config: Config) {
|
|||||||
drop(lconn);
|
drop(lconn);
|
||||||
|
|
||||||
// build basic status by just yielding text and dereferencing contained urls
|
// build basic status by just yielding text and dereferencing contained urls
|
||||||
let mut status_text = build_basic_status(tweet);
|
let mut status_text = build_basic_status(tweet, &scootaloo_mentions);
|
||||||
|
|
||||||
// building associative media list
|
// building associative media list
|
||||||
let (media_url, status_medias) =
|
let (media_url, status_medias) =
|
||||||
@@ -106,10 +122,18 @@ pub async fn run(config: Config) {
|
|||||||
|
|
||||||
status_builder.status(&status_text).media_ids(status_medias);
|
status_builder.status(&status_text).media_ids(status_medias);
|
||||||
|
|
||||||
|
// theard if necessary
|
||||||
if let Some(i) = toot_reply_id {
|
if let Some(i) = toot_reply_id {
|
||||||
status_builder.in_reply_to(&i);
|
status_builder.in_reply_to(&i);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// language if any
|
||||||
|
if let Some(l) = &tweet.lang {
|
||||||
|
if let Some(r) = Language::from_639_1(l) {
|
||||||
|
status_builder.language(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// can be activated for test purposes
|
// can be activated for test purposes
|
||||||
// status_builder.visibility(elefren::status_builder::Visibility::Private);
|
// status_builder.visibility(elefren::status_builder::Visibility::Private);
|
||||||
|
|
||||||
|
@@ -4,7 +4,7 @@ use egg_mode::{
|
|||||||
entities::{MentionEntity, UrlEntity},
|
entities::{MentionEntity, UrlEntity},
|
||||||
tweet::Tweet,
|
tweet::Tweet,
|
||||||
};
|
};
|
||||||
use elefren::{apps::App, prelude::*, scopes::Scopes};
|
use elefren::{apps::App, prelude::*, scopes::Read, scopes::Scopes, scopes::Write};
|
||||||
use html_escape::decode_html_entities;
|
use html_escape::decode_html_entities;
|
||||||
use std::{borrow::Cow, collections::HashMap, io::stdin};
|
use std::{borrow::Cow, collections::HashMap, io::stdin};
|
||||||
|
|
||||||
@@ -48,14 +48,18 @@ pub fn get_mastodon_token(masto: &MastodonConfig) -> Mastodon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Builds toot text from tweet
|
/// Builds toot text from tweet
|
||||||
pub fn build_basic_status(tweet: &Tweet) -> String {
|
pub fn build_basic_status(tweet: &Tweet, mentions: &HashMap<String, String>) -> String {
|
||||||
let mut toot = tweet.text.to_owned();
|
let mut toot = tweet.text.to_owned();
|
||||||
|
|
||||||
for decoded_url in decode_urls(&tweet.entities.urls) {
|
for decoded_url in decode_urls(&tweet.entities.urls) {
|
||||||
toot = toot.replace(&decoded_url.0, &decoded_url.1);
|
toot = toot.replace(&decoded_url.0, &decoded_url.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
for decoded_mention in twitter_mentions(&tweet.entities.user_mentions) {
|
for decoded_mention in twitter_mentions(&tweet.entities.user_mentions)
|
||||||
|
.into_iter()
|
||||||
|
.chain(mentions.to_owned())
|
||||||
|
.collect::<HashMap<String, String>>()
|
||||||
|
{
|
||||||
toot = toot.replace(&decoded_mention.0, &decoded_mention.1);
|
toot = toot.replace(&decoded_mention.0, &decoded_mention.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +74,12 @@ pub fn register(host: &str, screen_name: &str) {
|
|||||||
builder
|
builder
|
||||||
.client_name(Cow::from(env!("CARGO_PKG_NAME").to_string()))
|
.client_name(Cow::from(env!("CARGO_PKG_NAME").to_string()))
|
||||||
.redirect_uris(Cow::from("urn:ietf:wg:oauth:2.0:oob".to_string()))
|
.redirect_uris(Cow::from("urn:ietf:wg:oauth:2.0:oob".to_string()))
|
||||||
.scopes(Scopes::write_all())
|
.scopes(
|
||||||
|
Scopes::write(Write::Accounts)
|
||||||
|
.and(Scopes::write(Write::Media))
|
||||||
|
.and(Scopes::write(Write::Statuses))
|
||||||
|
.and(Scopes::read(Read::Accounts)),
|
||||||
|
)
|
||||||
.website(Cow::from(
|
.website(Cow::from(
|
||||||
"https://framagit.org/veretcle/scootaloo".to_string(),
|
"https://framagit.org/veretcle/scootaloo".to_string(),
|
||||||
));
|
));
|
||||||
@@ -99,13 +108,19 @@ pub fn register(host: &str, screen_name: &str) {
|
|||||||
|
|
||||||
let toml = toml::to_string(&*mastodon).unwrap();
|
let toml = toml::to_string(&*mastodon).unwrap();
|
||||||
|
|
||||||
|
let current_account = mastodon
|
||||||
|
.verify_credentials()
|
||||||
|
.expect("Unable to access account information!");
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
"Please insert the following block at the end of your configuration file:
|
"Please insert the following block at the end of your configuration file:
|
||||||
[mastodon.{}]
|
[mastodon.{}]
|
||||||
twitter_screen_name = \"{}\"
|
twitter_screen_name = \"{}\"
|
||||||
|
mastodon_screen_name = \"{}\"
|
||||||
{}",
|
{}",
|
||||||
screen_name.to_lowercase(),
|
screen_name.to_lowercase(),
|
||||||
screen_name,
|
screen_name,
|
||||||
|
current_account.username,
|
||||||
toml
|
toml
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -192,7 +207,13 @@ mod tests {
|
|||||||
range: (80, 95),
|
range: (80, 95),
|
||||||
name: "Nintendo France".to_string(),
|
name: "Nintendo France".to_string(),
|
||||||
screen_name: "NintendoFrance".to_string(),
|
screen_name: "NintendoFrance".to_string(),
|
||||||
}
|
},
|
||||||
|
MentionEntity {
|
||||||
|
id: 999999999,
|
||||||
|
range: (80, 95),
|
||||||
|
name: "Willy Wonka".to_string(),
|
||||||
|
screen_name: "WillyWonka".to_string(),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
media: None,
|
media: None,
|
||||||
},
|
},
|
||||||
@@ -213,7 +234,7 @@ mod tests {
|
|||||||
retweeted: None,
|
retweeted: None,
|
||||||
retweeted_status: None,
|
retweeted_status: None,
|
||||||
source: None,
|
source: None,
|
||||||
text: "Mother 1 & 2 sur le NES/SNES online !\nDispo maintenant. cc @NintendoFrance https://t.co/zXw0FfX2Nt".to_string(),
|
text: "Mother 1 & 2 sur le NES/SNES online !\nDispo maintenant. cc @NintendoFrance @WillyWonka https://t.co/zXw0FfX2Nt".to_string(),
|
||||||
truncated: false,
|
truncated: false,
|
||||||
user: None,
|
user: None,
|
||||||
withheld_copyright: false,
|
withheld_copyright: false,
|
||||||
@@ -221,8 +242,13 @@ mod tests {
|
|||||||
withheld_scope: None,
|
withheld_scope: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let t_out = build_basic_status(&t);
|
let s: HashMap<String, String> = HashMap::from([(
|
||||||
|
"@WillyWonka".to_string(),
|
||||||
|
"@WillyWonka@chocolatefactory.org".to_string(),
|
||||||
|
)]);
|
||||||
|
|
||||||
assert_eq!(&t_out, "Mother 1 & 2 sur le NES/SNES online !\nDispo maintenant. cc @NintendoFrance@twitter.com https://www.youtube.com/watch?v=w5TrSaoYmZ8");
|
let t_out = build_basic_status(&t, &s);
|
||||||
|
|
||||||
|
assert_eq!(&t_out, "Mother 1 & 2 sur le NES/SNES online !\nDispo maintenant. cc @NintendoFrance@twitter.com @WillyWonka@chocolatefactory.org https://www.youtube.com/watch?v=w5TrSaoYmZ8");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -52,6 +52,29 @@ fn test_parse_good_toml_rate_limit() {
|
|||||||
assert_eq!(parse_good_toml.scootaloo.rate_limit, Some(69 as usize));
|
assert_eq!(parse_good_toml.scootaloo.rate_limit, Some(69 as usize));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_good_toml_mastodon_screen_name() {
|
||||||
|
let parse_good_toml = parse_toml("tests/good_test_mastodon_screen_name.toml");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
parse_good_toml
|
||||||
|
.mastodon
|
||||||
|
.get("0")
|
||||||
|
.unwrap()
|
||||||
|
.mastodon_screen_name,
|
||||||
|
Some("tarace".to_string())
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
parse_good_toml
|
||||||
|
.mastodon
|
||||||
|
.get("1")
|
||||||
|
.unwrap()
|
||||||
|
.mastodon_screen_name,
|
||||||
|
None
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_good_toml() {
|
fn test_parse_good_toml() {
|
||||||
let parse_good_toml = parse_toml("tests/good_test.toml");
|
let parse_good_toml = parse_toml("tests/good_test.toml");
|
||||||
|
28
tests/good_test_mastodon_screen_name.toml
Normal file
28
tests/good_test_mastodon_screen_name.toml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
[scootaloo]
|
||||||
|
|
||||||
|
db_path="/var/random/scootaloo.sqlite"
|
||||||
|
cache_path="/tmp/scootaloo"
|
||||||
|
|
||||||
|
[twitter]
|
||||||
|
consumer_key="rand consumer key"
|
||||||
|
consumer_secret="secret"
|
||||||
|
access_key="rand access key"
|
||||||
|
access_secret="super secret"
|
||||||
|
|
||||||
|
[mastodon]
|
||||||
|
[mastodon.0]
|
||||||
|
twitter_screen_name="tamerelol"
|
||||||
|
mastodon_screen_name="tarace"
|
||||||
|
base = "https://m.nintendojo.fr"
|
||||||
|
client_id = "rand client id"
|
||||||
|
client_secret = "secret"
|
||||||
|
redirect = "urn:ietf:wg:oauth:2.0:oob"
|
||||||
|
token = "super secret"
|
||||||
|
|
||||||
|
[mastodon.1]
|
||||||
|
twitter_screen_name="tamerelol"
|
||||||
|
base = "https://m.nintendojo.fr"
|
||||||
|
client_id = "rand client id"
|
||||||
|
client_secret = "secret"
|
||||||
|
redirect = "urn:ietf:wg:oauth:2.0:oob"
|
||||||
|
token = "super secret"
|
Reference in New Issue
Block a user