Skip to main content

layer_client/
session_backend.rs

1//! Pluggable session storage backend.
2//!
3//! Two built-in backends:
4//! * [`BinaryFileBackend`] — compact binary file (default).
5//! * [`SqliteBackend`]     — SQLite (`sqlite-session` feature).
6//! * [`InMemoryBackend`]   — ephemeral, for tests / fresh-start bots.
7
8use std::io;
9use std::path::PathBuf;
10use crate::session::{CachedPeer, DcEntry, PersistedSession, UpdatesStateSnap};
11
12// ─── Trait ────────────────────────────────────────────────────────────────────
13
14pub trait SessionBackend: Send + Sync {
15    fn save(&self, session: &PersistedSession) -> io::Result<()>;
16    fn load(&self) -> io::Result<Option<PersistedSession>>;
17    fn delete(&self) -> io::Result<()>;
18    fn name(&self) -> &str;
19}
20
21// ─── BinaryFileBackend ────────────────────────────────────────────────────────
22
23/// Stores the session in a compact binary file (v2 format with update state + peer cache).
24pub struct BinaryFileBackend {
25    path: PathBuf,
26}
27
28impl BinaryFileBackend {
29    pub fn new(path: impl Into<PathBuf>) -> Self {
30        Self { path: path.into() }
31    }
32
33    /// Returns the path this backend writes to.
34    pub fn path(&self) -> &std::path::Path { &self.path }
35}
36
37impl SessionBackend for BinaryFileBackend {
38    fn save(&self, session: &PersistedSession) -> io::Result<()> {
39        session.save(&self.path)
40    }
41    fn load(&self) -> io::Result<Option<PersistedSession>> {
42        if !self.path.exists() { return Ok(None); }
43        PersistedSession::load(&self.path).map(Some)
44    }
45    fn delete(&self) -> io::Result<()> {
46        if self.path.exists() { std::fs::remove_file(&self.path)?; }
47        Ok(())
48    }
49    fn name(&self) -> &str { "binary-file" }
50}
51
52// ─── InMemoryBackend ─────────────────────────────────────────────────────────
53
54/// Ephemeral session — nothing persisted to disk.
55///
56/// Useful for tests or bots that always start fresh. Note that access-hash
57/// caches and update state are preserved across `save`/`load` calls *within
58/// the same process*, which is what the reconnect path needs.
59#[derive(Default)]
60pub struct InMemoryBackend {
61    data: std::sync::Mutex<Option<MemData>>,
62}
63
64#[derive(Clone)]
65struct MemData {
66    home_dc_id:    i32,
67    dcs:           Vec<DcEntry>,
68    updates_state: UpdatesStateSnap,
69    peers:         Vec<CachedPeer>,
70}
71
72impl InMemoryBackend {
73    pub fn new() -> Self { Self::default() }
74}
75
76impl SessionBackend for InMemoryBackend {
77    fn save(&self, s: &PersistedSession) -> io::Result<()> {
78        *self.data.lock().unwrap() = Some(MemData {
79            home_dc_id:    s.home_dc_id,
80            dcs:           s.dcs.clone(),
81            updates_state: s.updates_state.clone(),
82            peers:         s.peers.clone(),
83        });
84        Ok(())
85    }
86    fn load(&self) -> io::Result<Option<PersistedSession>> {
87        Ok(self.data.lock().unwrap().as_ref().map(|d| PersistedSession {
88            home_dc_id:    d.home_dc_id,
89            dcs:           d.dcs.clone(),
90            updates_state: d.updates_state.clone(),
91            peers:         d.peers.clone(),
92        }))
93    }
94    fn delete(&self) -> io::Result<()> {
95        *self.data.lock().unwrap() = None;
96        Ok(())
97    }
98    fn name(&self) -> &str { "in-memory" }
99}
100
101// ─── SqliteBackend ────────────────────────────────────────────────────────────
102
103#[cfg(feature = "sqlite-session")]
104pub use sqlite_backend::SqliteBackend;
105
106#[cfg(feature = "sqlite-session")]
107mod sqlite_backend {
108    use super::*;
109    use rusqlite::{Connection, params};
110
111    /// SQLite-backed session store.
112    ///
113    /// Schema (auto-created on first open):
114    /// - `meta`           — key/value string pairs (home_dc_id, pts, qts, date, seq)
115    /// - `dc_entries`     — one row per DC
116    /// - `channel_pts`    — per-channel pts counters
117    /// - `peers`          — cached access hashes
118    ///
119    /// Enable with the `sqlite-session` Cargo feature:
120    /// ```toml
121    /// layer-client = { version = "*", features = ["sqlite-session"] }
122    /// ```
123    pub struct SqliteBackend { path: PathBuf }
124
125    impl SqliteBackend {
126        pub fn new(path: impl Into<PathBuf>) -> io::Result<Self> {
127            let path = path.into();
128            let conn = Connection::open(&path)
129                .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
130            conn.execute_batch(
131                "CREATE TABLE IF NOT EXISTS meta (
132                    key   TEXT PRIMARY KEY,
133                    value TEXT NOT NULL
134                );
135                CREATE TABLE IF NOT EXISTS dc_entries (
136                    dc_id       INTEGER PRIMARY KEY,
137                    addr        TEXT    NOT NULL,
138                    auth_key    BLOB,
139                    first_salt  INTEGER NOT NULL DEFAULT 0,
140                    time_offset INTEGER NOT NULL DEFAULT 0
141                );
142                CREATE TABLE IF NOT EXISTS channel_pts (
143                    channel_id  INTEGER PRIMARY KEY,
144                    pts         INTEGER NOT NULL DEFAULT 0
145                );
146                CREATE TABLE IF NOT EXISTS peers (
147                    id          INTEGER PRIMARY KEY,
148                    access_hash INTEGER NOT NULL,
149                    is_channel  INTEGER NOT NULL DEFAULT 0
150                );",
151            ).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
152            Ok(Self { path })
153        }
154
155        fn conn(&self) -> io::Result<Connection> {
156            Connection::open(&self.path)
157                .map_err(|e| io::Error::new(io::ErrorKind::Other, e))
158        }
159    }
160
161    impl SessionBackend for SqliteBackend {
162        fn save(&self, s: &PersistedSession) -> io::Result<()> {
163            let conn = self.conn()?;
164            let e = |e: rusqlite::Error| io::Error::new(io::ErrorKind::Other, e);
165
166            // meta
167            for (k, v) in [
168                ("home_dc_id", s.home_dc_id.to_string()),
169                ("pts",  s.updates_state.pts.to_string()),
170                ("qts",  s.updates_state.qts.to_string()),
171                ("date", s.updates_state.date.to_string()),
172                ("seq",  s.updates_state.seq.to_string()),
173            ] {
174                conn.execute(
175                    "INSERT OR REPLACE INTO meta (key, value) VALUES (?1, ?2)",
176                    params![k, v],
177                ).map_err(e)?;
178            }
179
180            // dc_entries
181            for dc in &s.dcs {
182                conn.execute(
183                    "INSERT OR REPLACE INTO dc_entries
184                        (dc_id, addr, auth_key, first_salt, time_offset)
185                     VALUES (?1, ?2, ?3, ?4, ?5)",
186                    params![
187                        dc.dc_id,
188                        dc.addr,
189                        dc.auth_key.map(|k| k.to_vec()),
190                        dc.first_salt,
191                        dc.time_offset,
192                    ],
193                ).map_err(e)?;
194            }
195
196            // channel_pts
197            conn.execute_batch("DELETE FROM channel_pts").map_err(e)?;
198            for &(cid, cpts) in &s.updates_state.channels {
199                conn.execute(
200                    "INSERT INTO channel_pts (channel_id, pts) VALUES (?1, ?2)",
201                    params![cid, cpts],
202                ).map_err(e)?;
203            }
204
205            // peers
206            conn.execute_batch("DELETE FROM peers").map_err(e)?;
207            for p in &s.peers {
208                conn.execute(
209                    "INSERT INTO peers (id, access_hash, is_channel) VALUES (?1, ?2, ?3)",
210                    params![p.id, p.access_hash, p.is_channel as i32],
211                ).map_err(e)?;
212            }
213
214            Ok(())
215        }
216
217        fn load(&self) -> io::Result<Option<PersistedSession>> {
218            if !self.path.exists() { return Ok(None); }
219            let conn = self.conn()?;
220            let e = |err: rusqlite::Error| io::Error::new(io::ErrorKind::Other, err);
221
222            macro_rules! meta_i32 {
223                ($key:expr, $default:expr) => {
224                    conn.query_row(
225                        "SELECT value FROM meta WHERE key = ?1",
226                        params![$key],
227                        |row| row.get::<_, String>(0),
228                    ).ok()
229                    .and_then(|v| v.parse::<i32>().ok())
230                    .unwrap_or($default)
231                };
232            }
233
234            let home_dc_id = meta_i32!("home_dc_id", 0);
235            if home_dc_id == 0 { return Ok(None); }
236
237            // dc_entries
238            let mut stmt = conn.prepare(
239                "SELECT dc_id, addr, auth_key, first_salt, time_offset FROM dc_entries"
240            ).map_err(e)?;
241            let dcs: Vec<DcEntry> = stmt.query_map([], |row| {
242                let key_blob: Option<Vec<u8>> = row.get(2)?;
243                let auth_key = key_blob.and_then(|k| {
244                    if k.len() == 256 {
245                        let mut a = [0u8; 256]; a.copy_from_slice(&k); Some(a)
246                    } else { None }
247                });
248                Ok(DcEntry {
249                    dc_id:       row.get(0)?,
250                    addr:        row.get(1)?,
251                    auth_key,
252                    first_salt:  row.get(3)?,
253                    time_offset: row.get(4)?,
254                })
255            }).map_err(e)?.filter_map(|r| r.ok()).collect();
256
257            // update state
258            let pts  = meta_i32!("pts",  0);
259            let qts  = meta_i32!("qts",  0);
260            let date = meta_i32!("date", 0);
261            let seq  = meta_i32!("seq",  0);
262
263            let mut ch_stmt = conn.prepare(
264                "SELECT channel_id, pts FROM channel_pts"
265            ).map_err(e)?;
266            let channels: Vec<(i64, i32)> = ch_stmt
267                .query_map([], |row| Ok((row.get::<_, i64>(0)?, row.get::<_, i32>(1)?)))
268                .map_err(e)?.filter_map(|r| r.ok()).collect();
269
270            // peers
271            let mut peer_stmt = conn.prepare(
272                "SELECT id, access_hash, is_channel FROM peers"
273            ).map_err(e)?;
274            let peers: Vec<CachedPeer> = peer_stmt
275                .query_map([], |row| Ok(CachedPeer {
276                    id:          row.get(0)?,
277                    access_hash: row.get(1)?,
278                    is_channel:  row.get::<_, i32>(2)? != 0,
279                }))
280                .map_err(e)?.filter_map(|r| r.ok()).collect();
281
282            Ok(Some(PersistedSession {
283                home_dc_id,
284                dcs,
285                updates_state: UpdatesStateSnap { pts, qts, date, seq, channels },
286                peers,
287            }))
288        }
289
290        fn delete(&self) -> io::Result<()> {
291            if self.path.exists() { std::fs::remove_file(&self.path)?; }
292            Ok(())
293        }
294
295        fn name(&self) -> &str { "sqlite" }
296    }
297}