1use rusqlite::{params, OptionalExtension};
7
8fn now_secs() -> u64 {
9 std::time::SystemTime::now()
10 .duration_since(std::time::UNIX_EPOCH)
11 .map(|d| d.as_secs())
12 .unwrap_or(0)
13}
14
15pub fn upsert_bot_manifest(pubkey_hex: &str, manifest_json: &str, event_created_at: u64) -> Result<(), String> {
19 let conn = super::get_write_connection_guard_static()?;
20 conn.execute(
21 "INSERT INTO bot_manifests (pubkey, manifest, event_created_at, fetched_at)
22 VALUES (?1, ?2, ?3, ?4)
23 ON CONFLICT(pubkey) DO UPDATE SET
24 manifest = excluded.manifest,
25 event_created_at = excluded.event_created_at,
26 fetched_at = excluded.fetched_at
27 WHERE excluded.event_created_at >= bot_manifests.event_created_at",
28 params![pubkey_hex, manifest_json, event_created_at as i64, now_secs() as i64],
29 )
30 .map_err(|e| format!("upsert bot manifest: {e}"))?;
31 Ok(())
32}
33
34pub fn get_bot_manifest(pubkey_hex: &str) -> Result<Option<(String, u64)>, String> {
36 let conn = super::get_db_connection_guard_static()?;
37 conn.query_row(
38 "SELECT manifest, event_created_at FROM bot_manifests WHERE pubkey = ?1",
39 params![pubkey_hex],
40 |r| Ok((r.get::<_, String>(0)?, r.get::<_, i64>(1)? as u64)),
41 )
42 .optional()
43 .map_err(|e| format!("get bot manifest: {e}"))
44}
45
46pub fn get_bot_manifests(pubkeys: &[String]) -> Result<Vec<(String, String)>, String> {
49 if pubkeys.is_empty() {
50 return Ok(Vec::new());
51 }
52 let conn = super::get_db_connection_guard_static()?;
53 let placeholders = vec!["?"; pubkeys.len()].join(",");
54 let sql = format!("SELECT pubkey, manifest FROM bot_manifests WHERE pubkey IN ({placeholders})");
55 let mut stmt = conn.prepare(&sql).map_err(|e| format!("get bot manifests: {e}"))?;
56 let rows = stmt
57 .query_map(rusqlite::params_from_iter(pubkeys.iter()), |r| {
58 Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?))
59 })
60 .map_err(|e| format!("get bot manifests: {e}"))?
61 .filter_map(|r| r.ok())
62 .collect();
63 Ok(rows)
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69
70 fn init_test_db() -> (tempfile::TempDir, std::sync::MutexGuard<'static, ()>) {
71 let guard = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
72 crate::db::close_database();
73 crate::db::clear_id_caches();
74 use nostr_sdk::prelude::ToBech32;
75 let tmp = tempfile::tempdir().unwrap();
76 let account = nostr_sdk::prelude::Keys::generate().public_key().to_bech32().unwrap();
77 std::fs::create_dir_all(tmp.path().join(&account)).unwrap();
78 crate::db::set_app_data_dir(tmp.path().to_path_buf());
79 crate::db::set_current_account(account.clone()).unwrap();
80 crate::db::init_database(&account).unwrap();
81 (tmp, guard)
82 }
83
84 #[test]
85 fn manifest_store_keeps_the_newest_edition() {
86 let (_tmp, _guard) = init_test_db();
87
88 upsert_bot_manifest("aa", r#"{"v":1,"commands":[]}"#, 100).unwrap();
89 assert_eq!(get_bot_manifest("aa").unwrap().unwrap().1, 100);
90
91 upsert_bot_manifest("aa", r#"{"v":1,"commands":[{"name":"x","description":"d"}]}"#, 200).unwrap();
93 let (json, at) = get_bot_manifest("aa").unwrap().unwrap();
94 assert_eq!(at, 200);
95 assert!(json.contains("\"x\""));
96
97 upsert_bot_manifest("aa", r#"{"v":1,"commands":[]}"#, 150).unwrap();
99 let (json, at) = get_bot_manifest("aa").unwrap().unwrap();
100 assert_eq!(at, 200);
101 assert!(json.contains("\"x\""));
102
103 upsert_bot_manifest("bb", r#"{"v":1,"commands":[]}"#, 50).unwrap();
104 let batch = get_bot_manifests(&["aa".into(), "bb".into(), "cc".into()]).unwrap();
105 assert_eq!(batch.len(), 2, "absent bots are simply missing");
106 assert!(get_bot_manifests(&[]).unwrap().is_empty());
107 }
108}