Skip to main content

vector_core/db/
bots.rs

1//! Last-known bot manifests (kind 10304) — the `/` command picker's persistent
2//! layer. One row per bot pubkey, replaced only by a NEWER manifest edition, so
3//! the picker serves instantly from boot while a background refetch converges.
4//! Manifests are PUBLIC replaceable events: rows are plaintext by design.
5
6use 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
15/// Store a validated manifest for `pubkey_hex`, keeping whichever edition is
16/// newest (`event_created_at` = the manifest event's timestamp). An equal-time
17/// re-fetch refreshes `fetched_at` only via the replace (idempotent).
18pub 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
34/// Last-known manifest JSON for one bot, with its edition timestamp.
35pub 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
46/// Last-known manifests for a set of bots: `(pubkey_hex, manifest_json)`.
47/// Bots with no stored manifest are simply absent.
48pub 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        // A newer edition replaces…
92        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        // …while an OLDER one is refused (a lagging relay can't roll us back).
98        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}