feature: make thread in Twitter thread in Mastodon

This commit is contained in:
VC
2022-04-23 13:39:41 +02:00
parent abfb2ff50a
commit 13bb6d6f37
5 changed files with 142 additions and 86 deletions

View File

@@ -1,7 +1,4 @@
// std
use std::fs::read_to_string;
// toml
use serde::Deserialize;
/// General configuration Struct

View File

@@ -1,4 +1,3 @@
// auto-imports
mod error;
use crate::error::ScootalooError;
@@ -19,22 +18,13 @@ mod state;
use state::{read_state, write_state, TweetToToot};
pub use state::init_db;
// std
use std::borrow::Cow;
// tokio
use tokio::fs::remove_file;
// elefren
use elefren::{
prelude::*,
status_builder::StatusBuilder,
};
// log
use log::{info, warn, error, debug};
// rusqlite
use rusqlite::Connection;
/// This is where the magic happens
@@ -72,6 +62,8 @@ pub async fn run(config: Config) {
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() {
@@ -79,6 +71,11 @@ pub async fn run(config: Config) {
info!("Tweet is a direct response, skipping");
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
@@ -125,10 +122,17 @@ pub async fn run(config: Config) {
// finished reuploading attachments, now lets do the toot baby!
debug!("Building corresponding Mastodon status");
let status = StatusBuilder::new()
.status(&status_text)
.media_ids(status_medias)
.build()
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()
.expect(&format!("Cannot build status with text {}", &status_text));
// publish status

View File

@@ -1,14 +1,7 @@
// self
use scootaloo::*;
// clap
use clap::{App, Arg, SubCommand};
// log
use log::{LevelFilter, error};
use simple_logger::SimpleLogger;
// std
use std::str::FromStr;
const DEFAULT_CONFIG_PATH: &'static str = "/usr/local/etc/scootaloo.toml";
@@ -21,7 +14,7 @@ fn main() {
.short("c")
.long("config")
.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)
.display_order(1))
.arg(Arg::with_name("log_level")
@@ -49,7 +42,7 @@ fn main() {
.short("c")
.long("config")
.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)
.display_order(1)))
.get_matches();

View File

@@ -1,24 +1,16 @@
// auto imports
use crate::config::MastodonConfig;
// std
use std::{
borrow::Cow,
error::Error,
collections::HashMap,
io::stdin,
};
// htmlescape
use htmlescape::decode_html;
// egg-mode
use egg_mode::{
tweet::Tweet,
entities::{UrlEntity, MentionEntity},
};
// elefren
use elefren::{
prelude::*,
apps::App,

View File

@@ -1,13 +1,7 @@
// auto-imports
use crate::config::ScootalooConfig;
// std
use std::error::Error;
// log
use log::debug;
// rusqlite
use rusqlite::{Connection, params, OptionalExtension};
/// Struct for each query line
@@ -50,9 +44,6 @@ pub fn write_state(conn: &Connection, t: TweetToToot) -> Result<(), Box<dyn Erro
Ok(())
}
/*********
* Main functions
*********/
/// Initiates the DB from path
pub fn init_db(config: &ScootalooConfig) -> Result<(), Box<dyn Error>> {
debug!("Initializing DB for Scootaloo");
@@ -78,7 +69,7 @@ mod tests {
};
#[test]
fn test_db() {
fn test_init_db() {
let scootaloo_config = ScootalooConfig {
db_path: String::from("/tmp/test_init_db.sqlite"),
cache_path: String::from("/tmp/scootaloo"),
@@ -96,57 +87,136 @@ mod tests {
[],
).unwrap();
// write a state to DB
let t = TweetToToot {
remove_file(scootaloo_config.db_path).unwrap();
}
#[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,
toot_id: String::from("987654321"),
};
write_state(&conn, t).unwrap();
let mut stmt = conn.prepare("SELECT * FROM tweet_to_toot limit 1;").unwrap();
let mut rows = stmt.query([]).unwrap();
write_state(&conn, t_in).unwrap();
while let Some(row) = rows.next().unwrap() {
assert_eq!(123456789 as u64, row.get::<_, u64>(0).unwrap());
assert_eq!("987654321", row.get::<_, String>(1).unwrap());
}
let mut stmt = conn.prepare("SELECT * FROM tweet_to_toot;").unwrap();
// write several other states
let (t1, t2) = (
TweetToToot {
tweet_id: 11111111,
toot_id: String::from("tamerelol"),
},
TweetToToot {
tweet_id: 1123456789,
toot_id: String::from("tonperemdr"),
});
let t_out = stmt.query_row([], |row| {
Ok(TweetToToot {
tweet_id: row.get(0).unwrap(),
toot_id: row.get(1).unwrap(),
})
}).unwrap();
write_state(&conn, t1).unwrap();
write_state(&conn, t2).unwrap();
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"),
_ => (),
}
assert_eq!(t_out.tweet_id, 123456789);
assert_eq!(t_out.toot_id, String::from("987654321"));
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");
}
}