feature: state is held into a sqlite db

This commit is contained in:
VC
2022-04-23 10:01:00 +02:00
parent 6363c12460
commit 48b8eaaa5b
4 changed files with 120 additions and 88 deletions

View File

@@ -30,7 +30,7 @@ access_secret="MYACCESSSECRET"
``` ```
Then run the command with the `init` subcommand to initiate the DB: Then run the command with the `init` subcommand to initiate the DB:
``` ```sh
scootaloo init scootaloo init
``` ```
@@ -52,7 +52,9 @@ token = "MYTOKEN"
You can then run the application via `cron` for example. Here is the generic usage: You can then run the application via `cron` for example. Here is the generic usage:
``` ```sh
A Twitter to Mastodon bot
USAGE: USAGE:
scootaloo [OPTIONS] [SUBCOMMAND] scootaloo [OPTIONS] [SUBCOMMAND]
@@ -62,20 +64,21 @@ FLAGS:
OPTIONS: OPTIONS:
-c, --config <CONFIG_FILE> TOML config file for scootaloo (default /usr/local/etc/scootaloo.toml) -c, --config <CONFIG_FILE> TOML config file for scootaloo (default /usr/local/etc/scootaloo.toml)
-l, --loglevel <LOGLEVEL> Log level.Valid values are: Off, Warn, Error, Info, Debug
SUBCOMMANDS: SUBCOMMANDS:
help Prints this message or the help of the given subcommand(s) help Prints this message or the help of the given subcommand(s)
init Command to init Scootaloo DB
register Command to register to a Mastodon Instance register Command to register to a Mastodon Instance
``` ```
# Quirks # Quirks
Scootaloo does not respect the spam limits imposed by Mastodon: it will make a 429 error if too much Tweets are converted to Toots in a short amount of time (and it will not recover from it). By default, it gets the last 200 tweets from the user timeline (which is a lot!). It is recommended to put a Tweet number into the `last_tweet` file before copying an old account. Scootaloo does not respect the spam limits imposed by Mastodon: it will make a 429 error if too much Tweets are converted to Toots in a short amount of time (and it will not recover from it). By default, it gets the last 200 tweets from the user timeline (which is a lot!). It is recommended to put a Tweet number into the DB file before copying an old account.
You can do that with a command like: You can can insert it like this:
```sh ```sh
echo -n '8189881949849' > last_tweet sqlite3 /var/lib/scootaloo/scootaloo.sqlite
INSERT INTO tweet_to_toot VALUES (1383782580412030982, "");
.quit
``` ```
**This file should only contain the last tweet ID without any other char (no EOL or new line).**

View File

@@ -1,35 +0,0 @@
use scootaloo::parse_toml;
#[test]
fn parse_good_toml() {
let tConfig = TwitterConfig {
username: "test",
consumer_key: "foo",
consumer_secret: "bar",
access_key: "secret",
access_secret: "super secret",
};
let mConfig = MastodonConfig {
base: "https://www.example.com",
client_id: "my_id",
client_secret: "this is secret",
redirect: "ooo:oooo:o",
token: "super secret",
};
let sConfig = ScootalooConfig {
db_path: "/tmp/scootaloo/scootaloo.db",
cache_path: "/tmp",
};
let test_config = Config {
twitter: tConfig,
mastodon: mConfig,
scootaloo: sConfig,
};
let parsed_config = parse_toml("tests/right_config.toml");
assert_eq!(parsed_config, test_config);
}

View File

@@ -16,7 +16,7 @@ use twitter::*;
mod util; mod util;
mod state; mod state;
use state::{read_state, write_state}; use state::{read_state, write_state, TweetToToot};
pub use state::init_db; pub use state::init_db;
// std // std
@@ -34,11 +34,19 @@ use elefren::{
// log // log
use log::{info, warn, error, debug}; use log::{info, warn, error, debug};
// rusqlite
use rusqlite::Connection;
/// This is where the magic happens /// This is where the magic happens
#[tokio::main] #[tokio::main]
pub async fn run(config: Config) { pub async fn run(config: Config) {
// open the SQLite connection
let conn = Connection::open(&config.scootaloo.db_path).unwrap();
// retrieve the last tweet ID for the username // retrieve the last tweet ID for the username
let last_tweet_id = read_state(&config.scootaloo.db_path); let last_tweet_id = match read_state(&conn, None).unwrap() {
Some(i) => Some(i.tweet_id),
None => None,
};
// get OAuth2 token // get OAuth2 token
let token = get_oauth2_token(&config.twitter); let token = get_oauth2_token(&config.twitter);
@@ -125,12 +133,17 @@ pub async fn run(config: Config) {
// publish status // publish status
// again unwrap is safe here as we are in the main thread // again unwrap is safe here as we are in the main thread
mastodon.new_status(status).unwrap(); let published_status = mastodon.new_status(status).unwrap();
// this will panic if it cannot publish the status, which is a good thing, it allows the // this will panic if it cannot publish the status, which is a good thing, it allows the
// last_tweet gathered not to be written // last_tweet gathered not to be written
let ttt_towrite = TweetToToot {
tweet_id: tweet.id,
toot_id: published_status.id,
};
// write the current state (tweet ID) to avoid copying it another time // write the current state (tweet ID) to avoid copying it another time
write_state(&config.scootaloo.db_path, tweet.id).unwrap_or_else(|e| write_state(&conn, ttt_towrite).unwrap_or_else(|e|
panic!("Cant write the last tweet retrieved: {}", e) panic!("Cant write the last tweet retrieved: {}", e)
); );
} }

View File

@@ -2,32 +2,52 @@
use crate::config::ScootalooConfig; use crate::config::ScootalooConfig;
// std // std
use std::{ use std::error::Error;
fs::{read_to_string, write},
error::Error,
};
// log // log
use log::debug; use log::debug;
// rusqlite // rusqlite
use rusqlite::{Connection, OpenFlags, params}; use rusqlite::{Connection, params, OptionalExtension};
/// Reads last tweet id from a file /// Struct for each query line
pub fn read_state(s: &str) -> Option<u64> { #[derive(Debug)]
let state = read_to_string(s); pub struct TweetToToot {
pub tweet_id: u64,
if let Ok(s) = state { pub toot_id: String,
debug!("Last Tweet ID (from file): {}", &s);
return s.parse::<u64>().ok();
}
None
} }
/// Writes last treated tweet id to a file /// if None is passed, read the last tweet from DB
pub fn write_state(f: &str, s: u64) -> Result<(), std::io::Error> { /// if a tweet_id is passed, read this particular tweet from DB
write(f, format!("{}", s)) pub fn read_state(conn: &Connection, s: Option<u64>) -> Result<Option<TweetToToot>, Box<dyn Error>> {
debug!("Reading tweet_id {:?}", s);
let query: String;
match s {
Some(i) => query = format!("SELECT * FROM tweet_to_toot WHERE tweet_id = {}", i),
None => query = String::from("SELECT * FROM tweet_to_toot ORDER BY tweet_id DESC LIMIT 1"),
};
let mut stmt = conn.prepare(&query)?;
let t = stmt.query_row([], |row| {
Ok(TweetToToot {
tweet_id: row.get(0)?,
toot_id: row.get(1)?,
})
}).optional()?;
Ok(t)
}
/// Writes last treated tweet id and toot id to the db
pub fn write_state(conn: &Connection, t: TweetToToot) -> Result<(), Box<dyn Error>> {
debug!("Write struct {:?}", t);
conn.execute(
"INSERT INTO tweet_to_toot (tweet_id, toot_id) VALUES (?1, ?2)",
params![t.tweet_id, t.toot_id],
)?;
Ok(())
} }
/********* /*********
@@ -35,6 +55,7 @@ pub fn write_state(f: &str, s: u64) -> Result<(), std::io::Error> {
*********/ *********/
/// 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");
let conn = Connection::open(&config.db_path)?; let conn = Connection::open(&config.db_path)?;
conn.execute( conn.execute(
@@ -57,7 +78,7 @@ mod tests {
}; };
#[test] #[test]
fn test_init_db() { fn test_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"),
@@ -69,33 +90,63 @@ mod tests {
assert!(Path::new(&scootaloo_config.db_path).exists()); assert!(Path::new(&scootaloo_config.db_path).exists());
// open said file // open said file
let conn = Connection::open_with_flags(&scootaloo_config.db_path, OpenFlags::SQLITE_OPEN_READ_ONLY).unwrap(); let conn = Connection::open(&scootaloo_config.db_path).unwrap();
conn.execute( conn.execute(
"SELECT * from tweet_to_toot;", "SELECT * from tweet_to_toot;",
[], [],
).unwrap(); ).unwrap();
conn.close().unwrap(); // write a state to DB
let t = 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();
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());
}
// 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"),
});
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"),
_ => (),
}
remove_file(&scootaloo_config.db_path).unwrap(); remove_file(&scootaloo_config.db_path).unwrap();
} }
#[test]
fn test_read_state() {
let scootaloo_config = ScootalooConfig {
db_path: String::from("/tmp/test_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 (?1, ?2)",
params![123456789 as u64, String::from("987654321")],
).unwrap();
}
} }