mirror of
https://framagit.org/veretcle/oolatoocs.git
synced 2025-12-06 14:53:15 +01:00
feat: add the ability to rewrite an edited toot
This commit is contained in:
277
src/state.rs
277
src/state.rs
@@ -1,3 +1,4 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use log::debug;
|
||||
use rusqlite::{params, Connection, OptionalExtension};
|
||||
use std::error::Error;
|
||||
@@ -7,6 +8,34 @@ use std::error::Error;
|
||||
pub struct TweetToToot {
|
||||
pub tweet_id: u64,
|
||||
pub toot_id: u64,
|
||||
pub datetime: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
/// Deletes a given state
|
||||
pub fn delete_state(conn: &Connection, toot_id: u64) -> Result<(), Box<dyn Error>> {
|
||||
debug!("Deleting Toot ID {}", toot_id);
|
||||
conn.execute(
|
||||
&format!("DELETE FROM tweet_to_toot WHERE toot_id = {}", toot_id),
|
||||
[],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Retrieves all tweets associated to a toot in the form of a vector
|
||||
pub fn read_all_tweet_state(conn: &Connection, toot_id: u64) -> Result<Vec<u64>, Box<dyn Error>> {
|
||||
let query = format!(
|
||||
"SELECT tweet_id FROM tweet_to_toot WHERE toot_id = {};",
|
||||
toot_id
|
||||
);
|
||||
let mut stmt = conn.prepare(&query)?;
|
||||
let mut rows = stmt.query([])?;
|
||||
|
||||
let mut v = Vec::new();
|
||||
while let Some(row) = rows.next()? {
|
||||
v.push(row.get(0)?);
|
||||
}
|
||||
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
/// if None is passed, read the last tweet from DB
|
||||
@@ -17,8 +46,10 @@ pub fn read_state(
|
||||
) -> Result<Option<TweetToToot>, Box<dyn Error>> {
|
||||
debug!("Reading toot_id {:?}", s);
|
||||
let query: String = match s {
|
||||
Some(i) => format!("SELECT * FROM tweet_to_toot WHERE toot_id = {i}"),
|
||||
None => "SELECT * FROM tweet_to_toot ORDER BY toot_id DESC LIMIT 1".to_string(),
|
||||
Some(i) => format!(
|
||||
"SELECT tweet_id, toot_id, UNIXEPOCH(datetime) AS datetime FROM tweet_to_toot WHERE toot_id = {i} ORDER BY tweet_id DESC LIMIT 1"
|
||||
),
|
||||
None => "SELECT tweet_id, toot_id, UNIXEPOCH(datetime) AS datetime FROM tweet_to_toot ORDER BY toot_id DESC LIMIT 1".to_string(),
|
||||
};
|
||||
|
||||
let mut stmt = conn.prepare(&query)?;
|
||||
@@ -28,6 +59,7 @@ pub fn read_state(
|
||||
Ok(TweetToToot {
|
||||
tweet_id: row.get("tweet_id")?,
|
||||
toot_id: row.get("toot_id")?,
|
||||
datetime: Some(DateTime::from_timestamp(row.get("datetime").unwrap(), 0).unwrap()),
|
||||
})
|
||||
})
|
||||
.optional()?;
|
||||
@@ -56,8 +88,9 @@ pub fn init_db(d: &str) -> Result<(), Box<dyn Error>> {
|
||||
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS tweet_to_toot (
|
||||
tweet_id INTEGER,
|
||||
toot_id INTEGER PRIMARY KEY
|
||||
tweet_id INTEGER PRIMARY KEY,
|
||||
toot_id INTEGER,
|
||||
datetime INTEGER DEFAULT CURRENT_TIMESTAMP
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
@@ -65,6 +98,56 @@ pub fn init_db(d: &str) -> Result<(), Box<dyn Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Migrate DB from 1.5.x to 1.6.x
|
||||
pub fn migrate_db(d: &str) -> Result<(), Box<dyn Error>> {
|
||||
debug!("Migration DB for Oolatoocs");
|
||||
|
||||
let conn = Connection::open(d)?;
|
||||
|
||||
let res = conn.execute("SELECT datetime from tweet_to_toot;", []);
|
||||
|
||||
// If the column can be selected then, it’s OK
|
||||
// if not, see if the error is a missing column and add it
|
||||
match res {
|
||||
Err(e) => match e.to_string().as_str() {
|
||||
"no such column: datetime" => migrate_db_alter_table(&conn), //column does not exist
|
||||
"Execute returned results - did you mean to call query?" => Ok(()), // return results,
|
||||
// column does
|
||||
// exist
|
||||
_ => Err(e.into()),
|
||||
},
|
||||
Ok(_) => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new table, copy the data from the old table and rename it
|
||||
fn migrate_db_alter_table(c: &Connection) -> Result<(), Box<dyn Error>> {
|
||||
// create the new table
|
||||
c.execute(
|
||||
"CREATE TABLE IF NOT EXISTS tweet_to_toot_new (
|
||||
tweet_id INTEGER PRIMARY KEY,
|
||||
toot_id INTEGER,
|
||||
datetime INTEGER DEFAULT CURRENT_TIMESTAMP
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
// copy data from the old table
|
||||
c.execute(
|
||||
"INSERT INTO tweet_to_toot_new (tweet_id, toot_id)
|
||||
SELECT tweet_id, toot_id FROM tweet_to_toot;",
|
||||
[],
|
||||
)?;
|
||||
|
||||
// drop the old table
|
||||
c.execute("DROP TABLE tweet_to_toot;", [])?;
|
||||
|
||||
// rename the new table
|
||||
c.execute("ALTER TABLE tweet_to_toot_new RENAME TO tweet_to_toot;", [])?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -119,17 +202,25 @@ mod tests {
|
||||
let t_in = TweetToToot {
|
||||
tweet_id: 123456789,
|
||||
toot_id: 987654321,
|
||||
datetime: None,
|
||||
};
|
||||
|
||||
write_state(&conn, t_in).unwrap();
|
||||
|
||||
let mut stmt = conn.prepare("SELECT * FROM tweet_to_toot;").unwrap();
|
||||
let mut stmt = conn
|
||||
.prepare(
|
||||
"SELECT tweet_id, toot_id, UNIXEPOCH(datetime) AS datetime FROM tweet_to_toot;",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let t_out = stmt
|
||||
.query_row([], |row| {
|
||||
Ok(TweetToToot {
|
||||
tweet_id: row.get("tweet_id").unwrap(),
|
||||
toot_id: row.get("toot_id").unwrap(),
|
||||
datetime: Some(
|
||||
DateTime::from_timestamp(row.get("datetime").unwrap(), 0).unwrap(),
|
||||
),
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
@@ -226,4 +317,180 @@ mod tests {
|
||||
assert_eq!(t_out.tweet_id, 100);
|
||||
assert_eq!(t_out.toot_id, 1000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_last_toot_id_read_state() {
|
||||
let d = "/tmp/test_last_toot_id_read_state.sqlite";
|
||||
|
||||
init_db(d).unwrap();
|
||||
|
||||
let conn = Connection::open(d).unwrap();
|
||||
|
||||
conn.execute(
|
||||
"INSERT INTO tweet_to_toot(tweet_id, toot_id)
|
||||
VALUES (100, 1000), (101, 1000);",
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let t_out = read_state(&conn, Some(1000)).unwrap().unwrap();
|
||||
|
||||
remove_file(d).unwrap();
|
||||
|
||||
assert_eq!(t_out.tweet_id, 101);
|
||||
assert_eq!(t_out.toot_id, 1000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_migrate_db_alter_table() {
|
||||
let d = "/tmp/test_migrate_db_alter_table.sqlite";
|
||||
|
||||
let conn = Connection::open(d).unwrap();
|
||||
|
||||
init_db(d).unwrap();
|
||||
|
||||
write_state(
|
||||
&conn,
|
||||
TweetToToot {
|
||||
tweet_id: 0,
|
||||
toot_id: 0,
|
||||
datetime: None,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
write_state(
|
||||
&conn,
|
||||
TweetToToot {
|
||||
tweet_id: 1,
|
||||
toot_id: 1,
|
||||
datetime: None,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
migrate_db_alter_table(&conn).unwrap();
|
||||
|
||||
let mut stmt = conn.prepare("PRAGMA table_info(tweet_to_toot);").unwrap();
|
||||
let mut t = stmt.query([]).unwrap();
|
||||
|
||||
while let Some(row) = t.next().unwrap() {
|
||||
if row.get::<usize, u8>(0).unwrap() == 2 {
|
||||
assert_eq!(row.get::<usize, String>(1).unwrap(), "datetime".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
remove_file(d).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_migrate_db() {
|
||||
// this should be idempotent
|
||||
let d = "/tmp/test_migrate_db.sqlite";
|
||||
|
||||
let conn = Connection::open(d).unwrap();
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS tweet_to_toot (
|
||||
tweet_id INTEGER,
|
||||
toot_id INTEGER PRIMARY KEY
|
||||
)",
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
conn.execute("INSERT INTO tweet_to_toot VALUES (0, 0), (1, 1);", [])
|
||||
.unwrap();
|
||||
|
||||
migrate_db(d).unwrap();
|
||||
|
||||
let last_state = read_state(&conn, None).unwrap().unwrap();
|
||||
|
||||
assert_eq!(last_state.tweet_id, 1);
|
||||
assert_eq!(last_state.toot_id, 1);
|
||||
|
||||
migrate_db(d).unwrap(); // shouldn’t do anything
|
||||
|
||||
remove_file(d).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_state() {
|
||||
let d = "/tmp/test_delete_state.sqlite";
|
||||
|
||||
init_db(d).unwrap();
|
||||
|
||||
let conn = Connection::open(d).unwrap();
|
||||
|
||||
conn.execute(
|
||||
"INSERT INTO tweet_to_toot(tweet_id, toot_id) VALUES (0, 0);",
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
delete_state(&conn, 0).unwrap();
|
||||
|
||||
let mut stmt = conn
|
||||
.prepare(
|
||||
"SELECT tweet_id, toot_id, UNIXEPOCH(datetime) AS datetime FROM tweet_to_toot;",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let t_out = stmt.query_row([], |row| {
|
||||
Ok(TweetToToot {
|
||||
tweet_id: row.get("tweet_id").unwrap(),
|
||||
toot_id: row.get("toot_id").unwrap(),
|
||||
datetime: Some(DateTime::from_timestamp(row.get("datetime").unwrap(), 0).unwrap()),
|
||||
})
|
||||
});
|
||||
|
||||
assert!(t_out.is_err_and(|x| x == rusqlite::Error::QueryReturnedNoRows));
|
||||
|
||||
conn.execute(
|
||||
"INSERT INTO tweet_to_toot(tweet_id, toot_id) VALUES(102,42), (103,42);",
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
delete_state(&conn, 42).unwrap();
|
||||
|
||||
let mut stmt = conn
|
||||
.prepare(
|
||||
"SELECT tweet_id, toot_id, UNIXEPOCH(datetime) AS datetime FROM tweet_to_toot;",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let t_out = stmt.query_row([], |row| {
|
||||
Ok(TweetToToot {
|
||||
tweet_id: row.get("tweet_id").unwrap(),
|
||||
toot_id: row.get("toot_id").unwrap(),
|
||||
datetime: Some(DateTime::from_timestamp(row.get("datetime").unwrap(), 0).unwrap()),
|
||||
})
|
||||
});
|
||||
|
||||
assert!(t_out.is_err_and(|x| x == rusqlite::Error::QueryReturnedNoRows));
|
||||
|
||||
remove_file(d).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_all_tweet_state() {
|
||||
let d = "/tmp/read_all_tweet_state.sqlite";
|
||||
|
||||
init_db(d).unwrap();
|
||||
|
||||
let conn = Connection::open(d).unwrap();
|
||||
|
||||
conn.execute(
|
||||
"INSERT INTO tweet_to_toot(tweet_id, toot_id) VALUES (102, 42), (103, 42), (105, 43);",
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let v1 = read_all_tweet_state(&conn, 43).unwrap();
|
||||
let v2 = read_all_tweet_state(&conn, 42).unwrap();
|
||||
|
||||
assert_eq!(v1, vec![105]);
|
||||
assert_eq!(v2, vec![102, 103]);
|
||||
|
||||
remove_file(d).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user