mirror of
https://framagit.org/veretcle/scootaloo.git
synced 2025-07-21 09:31:19 +02:00
feature: make thread in Twitter thread in Mastodon
This commit is contained in:
@@ -1,7 +1,4 @@
|
|||||||
// std
|
|
||||||
use std::fs::read_to_string;
|
use std::fs::read_to_string;
|
||||||
|
|
||||||
// toml
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
/// General configuration Struct
|
/// General configuration Struct
|
||||||
|
32
src/lib.rs
32
src/lib.rs
@@ -1,4 +1,3 @@
|
|||||||
// auto-imports
|
|
||||||
mod error;
|
mod error;
|
||||||
use crate::error::ScootalooError;
|
use crate::error::ScootalooError;
|
||||||
|
|
||||||
@@ -19,22 +18,13 @@ mod state;
|
|||||||
use state::{read_state, write_state, TweetToToot};
|
use state::{read_state, write_state, TweetToToot};
|
||||||
pub use state::init_db;
|
pub use state::init_db;
|
||||||
|
|
||||||
// std
|
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
// tokio
|
|
||||||
use tokio::fs::remove_file;
|
use tokio::fs::remove_file;
|
||||||
|
|
||||||
// elefren
|
|
||||||
use elefren::{
|
use elefren::{
|
||||||
prelude::*,
|
prelude::*,
|
||||||
status_builder::StatusBuilder,
|
status_builder::StatusBuilder,
|
||||||
};
|
};
|
||||||
|
|
||||||
// log
|
|
||||||
use log::{info, warn, error, debug};
|
use log::{info, warn, error, debug};
|
||||||
|
|
||||||
// rusqlite
|
|
||||||
use rusqlite::Connection;
|
use rusqlite::Connection;
|
||||||
|
|
||||||
/// This is where the magic happens
|
/// This is where the magic happens
|
||||||
@@ -72,6 +62,8 @@ pub async fn run(config: Config) {
|
|||||||
|
|
||||||
for tweet in &feed {
|
for tweet in &feed {
|
||||||
debug!("Treating Tweet {} inside feed", tweet.id);
|
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
|
// 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 let Some(r) = &tweet.in_reply_to_screen_name {
|
||||||
if &r.to_lowercase() != &config.twitter.username.to_lowercase() {
|
if &r.to_lowercase() != &config.twitter.username.to_lowercase() {
|
||||||
@@ -79,6 +71,11 @@ pub async fn run(config: Config) {
|
|||||||
info!("Tweet is a direct response, skipping");
|
info!("Tweet is a direct response, skipping");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let searched_toot = read_state(&conn, tweet.in_reply_to_status_id).unwrap_or(None);
|
||||||
|
if let Some(i) = searched_toot {
|
||||||
|
toot_reply_id = Some(i.toot_id);
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// build basic status by just yielding text and dereferencing contained urls
|
// build basic status by just yielding text and dereferencing contained urls
|
||||||
@@ -125,10 +122,17 @@ pub async fn run(config: Config) {
|
|||||||
// finished reuploading attachments, now let’s do the toot baby!
|
// finished reuploading attachments, now let’s do the toot baby!
|
||||||
|
|
||||||
debug!("Building corresponding Mastodon status");
|
debug!("Building corresponding Mastodon status");
|
||||||
let status = StatusBuilder::new()
|
|
||||||
.status(&status_text)
|
let mut status_builder = StatusBuilder::new();
|
||||||
.media_ids(status_medias)
|
|
||||||
.build()
|
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()
|
||||||
.expect(&format!("Cannot build status with text {}", &status_text));
|
.expect(&format!("Cannot build status with text {}", &status_text));
|
||||||
|
|
||||||
// publish status
|
// publish status
|
||||||
|
11
src/main.rs
11
src/main.rs
@@ -1,14 +1,7 @@
|
|||||||
// self
|
|
||||||
use scootaloo::*;
|
use scootaloo::*;
|
||||||
|
|
||||||
// clap
|
|
||||||
use clap::{App, Arg, SubCommand};
|
use clap::{App, Arg, SubCommand};
|
||||||
|
|
||||||
// log
|
|
||||||
use log::{LevelFilter, error};
|
use log::{LevelFilter, error};
|
||||||
use simple_logger::SimpleLogger;
|
use simple_logger::SimpleLogger;
|
||||||
|
|
||||||
// std
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
const DEFAULT_CONFIG_PATH: &'static str = "/usr/local/etc/scootaloo.toml";
|
const DEFAULT_CONFIG_PATH: &'static str = "/usr/local/etc/scootaloo.toml";
|
||||||
@@ -21,7 +14,7 @@ fn main() {
|
|||||||
.short("c")
|
.short("c")
|
||||||
.long("config")
|
.long("config")
|
||||||
.value_name("CONFIG_FILE")
|
.value_name("CONFIG_FILE")
|
||||||
.help(&*format!("TOML config file for scootaloo (default {})", DEFAULT_CONFIG_PATH))
|
.help(&format!("TOML config file for scootaloo (default {})", DEFAULT_CONFIG_PATH))
|
||||||
.takes_value(true)
|
.takes_value(true)
|
||||||
.display_order(1))
|
.display_order(1))
|
||||||
.arg(Arg::with_name("log_level")
|
.arg(Arg::with_name("log_level")
|
||||||
@@ -49,7 +42,7 @@ fn main() {
|
|||||||
.short("c")
|
.short("c")
|
||||||
.long("config")
|
.long("config")
|
||||||
.value_name("CONFIG_FILE")
|
.value_name("CONFIG_FILE")
|
||||||
.help(&*format!("TOML config file for scootaloo (default {})", DEFAULT_CONFIG_PATH))
|
.help(&format!("TOML config file for scootaloo (default {})", DEFAULT_CONFIG_PATH))
|
||||||
.takes_value(true)
|
.takes_value(true)
|
||||||
.display_order(1)))
|
.display_order(1)))
|
||||||
.get_matches();
|
.get_matches();
|
||||||
|
@@ -1,24 +1,16 @@
|
|||||||
// auto imports
|
|
||||||
use crate::config::MastodonConfig;
|
use crate::config::MastodonConfig;
|
||||||
|
|
||||||
// std
|
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
error::Error,
|
error::Error,
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
io::stdin,
|
io::stdin,
|
||||||
};
|
};
|
||||||
|
|
||||||
// htmlescape
|
|
||||||
use htmlescape::decode_html;
|
use htmlescape::decode_html;
|
||||||
|
|
||||||
// egg-mode
|
|
||||||
use egg_mode::{
|
use egg_mode::{
|
||||||
tweet::Tweet,
|
tweet::Tweet,
|
||||||
entities::{UrlEntity, MentionEntity},
|
entities::{UrlEntity, MentionEntity},
|
||||||
};
|
};
|
||||||
|
|
||||||
// elefren
|
|
||||||
use elefren::{
|
use elefren::{
|
||||||
prelude::*,
|
prelude::*,
|
||||||
apps::App,
|
apps::App,
|
||||||
|
174
src/state.rs
174
src/state.rs
@@ -1,13 +1,7 @@
|
|||||||
// auto-imports
|
|
||||||
use crate::config::ScootalooConfig;
|
use crate::config::ScootalooConfig;
|
||||||
|
|
||||||
// std
|
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
|
||||||
// log
|
|
||||||
use log::debug;
|
use log::debug;
|
||||||
|
|
||||||
// rusqlite
|
|
||||||
use rusqlite::{Connection, params, OptionalExtension};
|
use rusqlite::{Connection, params, OptionalExtension};
|
||||||
|
|
||||||
/// Struct for each query line
|
/// Struct for each query line
|
||||||
@@ -50,9 +44,6 @@ pub fn write_state(conn: &Connection, t: TweetToToot) -> Result<(), Box<dyn Erro
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/*********
|
|
||||||
* Main functions
|
|
||||||
*********/
|
|
||||||
/// Initiates the DB from path
|
/// Initiates the DB from path
|
||||||
pub fn init_db(config: &ScootalooConfig) -> Result<(), Box<dyn Error>> {
|
pub fn init_db(config: &ScootalooConfig) -> Result<(), Box<dyn Error>> {
|
||||||
debug!("Initializing DB for Scootaloo");
|
debug!("Initializing DB for Scootaloo");
|
||||||
@@ -78,7 +69,7 @@ mod tests {
|
|||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_db() {
|
fn test_init_db() {
|
||||||
let scootaloo_config = ScootalooConfig {
|
let scootaloo_config = ScootalooConfig {
|
||||||
db_path: String::from("/tmp/test_init_db.sqlite"),
|
db_path: String::from("/tmp/test_init_db.sqlite"),
|
||||||
cache_path: String::from("/tmp/scootaloo"),
|
cache_path: String::from("/tmp/scootaloo"),
|
||||||
@@ -96,57 +87,136 @@ mod tests {
|
|||||||
[],
|
[],
|
||||||
).unwrap();
|
).unwrap();
|
||||||
|
|
||||||
// write a state to DB
|
remove_file(scootaloo_config.db_path).unwrap();
|
||||||
let t = TweetToToot {
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_write_state() {
|
||||||
|
let scootaloo_config = ScootalooConfig {
|
||||||
|
db_path: String::from("/tmp/test_write_state.sqlite"),
|
||||||
|
cache_path: String::from("/tmp/scootaloo"),
|
||||||
|
};
|
||||||
|
|
||||||
|
init_db(&scootaloo_config).unwrap();
|
||||||
|
|
||||||
|
let conn = Connection::open(&scootaloo_config.db_path).unwrap();
|
||||||
|
|
||||||
|
let t_in = TweetToToot {
|
||||||
tweet_id: 123456789,
|
tweet_id: 123456789,
|
||||||
toot_id: String::from("987654321"),
|
toot_id: String::from("987654321"),
|
||||||
};
|
};
|
||||||
write_state(&conn, t).unwrap();
|
|
||||||
|
|
||||||
let mut stmt = conn.prepare("SELECT * FROM tweet_to_toot limit 1;").unwrap();
|
write_state(&conn, t_in).unwrap();
|
||||||
let mut rows = stmt.query([]).unwrap();
|
|
||||||
|
|
||||||
while let Some(row) = rows.next().unwrap() {
|
let mut stmt = conn.prepare("SELECT * FROM tweet_to_toot;").unwrap();
|
||||||
assert_eq!(123456789 as u64, row.get::<_, u64>(0).unwrap());
|
|
||||||
assert_eq!("987654321", row.get::<_, String>(1).unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
// write several other states
|
let t_out = stmt.query_row([], |row| {
|
||||||
let (t1, t2) = (
|
Ok(TweetToToot {
|
||||||
TweetToToot {
|
tweet_id: row.get(0).unwrap(),
|
||||||
tweet_id: 11111111,
|
toot_id: row.get(1).unwrap(),
|
||||||
toot_id: String::from("tamerelol"),
|
})
|
||||||
},
|
}).unwrap();
|
||||||
TweetToToot {
|
|
||||||
tweet_id: 1123456789,
|
|
||||||
toot_id: String::from("tonperemdr"),
|
|
||||||
});
|
|
||||||
|
|
||||||
write_state(&conn, t1).unwrap();
|
assert_eq!(t_out.tweet_id, 123456789);
|
||||||
write_state(&conn, t2).unwrap();
|
assert_eq!(t_out.toot_id, String::from("987654321"));
|
||||||
|
|
||||||
match read_state(&conn, None).unwrap() {
|
|
||||||
Some(i) => {
|
|
||||||
assert_eq!(1123456789, i.tweet_id);
|
|
||||||
assert_eq!("tonperemdr", &i.toot_id);
|
|
||||||
},
|
|
||||||
None => panic!("This should not happen!"),
|
|
||||||
}
|
|
||||||
|
|
||||||
match read_state(&conn, Some(11111111)).unwrap() {
|
|
||||||
Some(i) => {
|
|
||||||
assert_eq!(11111111, i.tweet_id);
|
|
||||||
assert_eq!("tamerelol", &i.toot_id);
|
|
||||||
},
|
|
||||||
None => panic!("This should not happen!"),
|
|
||||||
}
|
|
||||||
|
|
||||||
match read_state(&conn, Some(0000000)).unwrap() {
|
|
||||||
Some(_) => panic!("This should not happen"),
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
|
|
||||||
remove_file(&scootaloo_config.db_path).unwrap();
|
remove_file(&scootaloo_config.db_path).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_none_to_tweet_id_read_state() {
|
||||||
|
let scootaloo_config = ScootalooConfig {
|
||||||
|
db_path: String::from("/tmp/test_none_to_tweet_id_read_state.sqlite"),
|
||||||
|
cache_path: String::from("/tmp/scootaloo"),
|
||||||
|
};
|
||||||
|
|
||||||
|
init_db(&scootaloo_config).unwrap();
|
||||||
|
|
||||||
|
let conn = Connection::open(&scootaloo_config.db_path).unwrap();
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO tweet_to_toot (tweet_id, toot_id)
|
||||||
|
VALUES
|
||||||
|
(101, 'A'),
|
||||||
|
(102, 'B');",
|
||||||
|
[],
|
||||||
|
).unwrap();
|
||||||
|
|
||||||
|
let t_out = read_state(&conn, None).unwrap().unwrap();
|
||||||
|
|
||||||
|
remove_file(&scootaloo_config.db_path).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(t_out.tweet_id, 102);
|
||||||
|
assert_eq!(t_out.toot_id, "B");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_none_to_none_read_state() {
|
||||||
|
let scootaloo_config = ScootalooConfig {
|
||||||
|
db_path: String::from("/tmp/test_none_to_none_read_state.sqlite"),
|
||||||
|
cache_path: String::from("/tmp/scootaloo"),
|
||||||
|
};
|
||||||
|
|
||||||
|
init_db(&scootaloo_config).unwrap();
|
||||||
|
|
||||||
|
let conn = Connection::open(&scootaloo_config.db_path).unwrap();
|
||||||
|
|
||||||
|
let t_out = read_state(&conn, None).unwrap();
|
||||||
|
|
||||||
|
remove_file(&scootaloo_config.db_path).unwrap();
|
||||||
|
|
||||||
|
assert!(t_out.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tweet_id_to_none_read_state() {
|
||||||
|
let scootaloo_config = ScootalooConfig {
|
||||||
|
db_path: String::from("/tmp/test_tweet_id_to_none_read_state.sqlite"),
|
||||||
|
cache_path: String::from("/tmp/scootaloo"),
|
||||||
|
};
|
||||||
|
|
||||||
|
init_db(&scootaloo_config).unwrap();
|
||||||
|
|
||||||
|
let conn = Connection::open(&scootaloo_config.db_path).unwrap();
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO tweet_to_toot (tweet_id, toot_id)
|
||||||
|
VALUES
|
||||||
|
(100, 'A');",
|
||||||
|
[],
|
||||||
|
).unwrap();
|
||||||
|
|
||||||
|
let t_out = read_state(&conn, Some(101)).unwrap();
|
||||||
|
|
||||||
|
remove_file(&scootaloo_config.db_path).unwrap();
|
||||||
|
|
||||||
|
assert!(t_out.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tweet_id_to_tweet_id_read_state() {
|
||||||
|
let scootaloo_config = ScootalooConfig {
|
||||||
|
db_path: String::from("/tmp/test_tweet_id_to_tweet_id_read_state.sqlite"),
|
||||||
|
cache_path: String::from("/tmp/scootaloo"),
|
||||||
|
};
|
||||||
|
|
||||||
|
init_db(&scootaloo_config).unwrap();
|
||||||
|
|
||||||
|
let conn = Connection::open(&scootaloo_config.db_path).unwrap();
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO tweet_to_toot (tweet_id, toot_id)
|
||||||
|
VALUES
|
||||||
|
(100, 'A');",
|
||||||
|
[],
|
||||||
|
).unwrap();
|
||||||
|
|
||||||
|
let t_out = read_state(&conn, Some(100)).unwrap().unwrap();
|
||||||
|
|
||||||
|
remove_file(&scootaloo_config.db_path).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(t_out.tweet_id, 100);
|
||||||
|
assert_eq!(t_out.toot_id, "A");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user