]> Humopery - private/memberbot.git/commitdiff
Persist device ID so a new one isn't created every time
authorErik Mackdanz <erikmack@gmail.com>
Sun, 22 Feb 2026 20:38:29 +0000 (14:38 -0600)
committerErik Mackdanz <erikmack@gmail.com>
Sun, 22 Feb 2026 20:38:29 +0000 (14:38 -0600)
.gitignore
src/bot.rs
src/config.rs

index 3b05ad8a9c2a32f848e27885fada85f7f4e0bcea..630a395538c5526f467c1b71be58295d69533cb2 100644 (file)
@@ -1,4 +1,6 @@
 /target
 .env
 memberbot.db
-matrix_store.db/*
\ No newline at end of file
+matrix_store.db/*
+*.session.json
+matrix_store_*
index 6af3de3a52df6da8ca4b1d7c75e0673c58a8937e..8e4cc15445e8cc66903e6559edc29386daf5833d 100644 (file)
@@ -1,4 +1,4 @@
-use std::sync::Arc;
+use std::{fs, path::PathBuf, sync::Arc};
 
 use anyhow::Result;
 use futures_util::StreamExt;
@@ -13,8 +13,10 @@ use matrix_sdk::{
         },
         OwnedRoomId, UserId,
     },
+    store::RoomLoadSettings,
     Client,
 };
+use serde::{Deserialize, Serialize};
 use tracing::{error, info, warn};
 
 use crate::{
@@ -23,6 +25,14 @@ use crate::{
     models::MemberUpsert,
 };
 
+#[derive(Debug, Serialize, Deserialize)]
+struct SessionData {
+    user_id: String,
+    device_id: String,
+    access_token: String,
+    refresh_token: Option<String>,
+}
+
 pub struct Bot {
     client: Client,
     db: Arc<Database>,
@@ -33,23 +43,28 @@ impl Bot {
     pub async fn new(config: Config, db: Database) -> Result<Self> {
         info!("Initializing Matrix bot...");
 
-        // Build the Matrix client
-        let client = Client::builder()
-            .homeserver_url(&config.matrix_homeserver_url)
-            .sqlite_store("matrix_store.db", None)
-            .build()
-            .await?;
-
-        // Login
-        info!("Logging in as {}...", config.matrix_username);
-        let user_id = UserId::parse(&config.matrix_username)?;
-        client.matrix_auth()
-            .login_username(&user_id, &config.matrix_password)
-            .initial_device_display_name("MemberBot")
-            .send()
-            .await?;
+        // Try to restore existing session
+        let (client, restored) = if let Some(session_data) = load_session(&config.session_file) {
+            info!("Found existing session, attempting to restore...");
+            match restore_session(&config, &session_data).await {
+                Ok(client) => {
+                    info!("Successfully restored session");
+                    (client, true)
+                }
+                Err(e) => {
+                    warn!("Failed to restore session: {}. Creating new session...", e);
+                    create_new_session(&config).await?
+                }
+            }
+        } else {
+            info!("No existing session found, creating new session...");
+            create_new_session(&config).await?
+        };
 
-        info!("Successfully logged in");
+        // If we created a new session, save it
+        if !restored {
+            save_session(&client, &config.session_file).await?;
+        }
 
         // Sync to get initial state
         client.sync_once(SyncSettings::default()).await?;
@@ -147,6 +162,136 @@ impl Bot {
     }
 }
 
+async fn create_new_session(config: &Config) -> Result<(Client, bool)> {
+    info!("Creating new Matrix client...");
+
+    // Build the Matrix client with store path
+    let client = Client::builder()
+        .homeserver_url(&config.matrix_homeserver_url)
+        .sqlite_store(&config.store_path, None)
+        .build()
+        .await?;
+
+    // Login with username and password
+    info!("Logging in as {}...", config.matrix_username);
+    let user_id = UserId::parse(&config.matrix_username)?;
+    client.matrix_auth()
+        .login_username(&user_id, &config.matrix_password)
+        .initial_device_display_name("MemberBot")
+        .send()
+        .await?;
+
+    info!("Successfully logged in with new session");
+    Ok((client, false))
+}
+
+async fn restore_session(config: &Config, session_data: &SessionData) -> Result<Client> {
+    info!("Restoring session for {}...", session_data.user_id);
+
+    // Build the Matrix client with the same store path
+    let client = Client::builder()
+        .homeserver_url(&config.matrix_homeserver_url)
+        .sqlite_store(&config.store_path, None)
+        .build()
+        .await?;
+
+    // Create a MatrixSession from our saved data
+    use matrix_sdk::authentication::matrix::MatrixSession;
+    use matrix_sdk::{SessionMeta, SessionTokens};
+    
+    let session = MatrixSession {
+        meta: SessionMeta {
+            user_id: UserId::parse(&session_data.user_id)?.to_owned(),
+            device_id: session_data.device_id.clone().into(),
+        },
+        tokens: SessionTokens {
+            access_token: session_data.access_token.clone(),
+            refresh_token: session_data.refresh_token.clone(),
+        },
+    };
+
+    // Try to restore the session
+    match client.matrix_auth()
+        .restore_session(session, RoomLoadSettings::default())
+        .await
+    {
+        Ok(_) => {
+            info!("Successfully restored session");
+            Ok(client)
+        }
+        Err(e) => {
+            // If restore fails (e.g., token expired), try to login with saved device_id
+            warn!("Failed to restore session: {}. Attempting to login with saved device_id...", e);
+            login_with_device_id(config, &session_data.device_id).await
+        }
+    }
+}
+
+async fn login_with_device_id(config: &Config, device_id: &str) -> Result<Client> {
+    info!("Logging in with device_id {}...", device_id);
+
+    // Build the Matrix client with the same store path
+    let client = Client::builder()
+        .homeserver_url(&config.matrix_homeserver_url)
+        .sqlite_store(&config.store_path, None)
+        .build()
+        .await?;
+
+    // Login with username, password, and saved device_id
+    let user_id = UserId::parse(&config.matrix_username)?;
+    client.matrix_auth()
+        .login_username(&user_id, &config.matrix_password)
+        .device_id(device_id)
+        .initial_device_display_name("MemberBot")
+        .send()
+        .await?;
+
+    info!("Successfully logged in with saved device_id");
+    Ok(client)
+}
+
+async fn save_session(client: &Client, session_file: &PathBuf) -> Result<()> {
+    if let Some(session) = client.session() {
+        let meta = session.meta();
+        let session_data = SessionData {
+            user_id: meta.user_id.to_string(),
+            device_id: meta.device_id.to_string(),
+            access_token: session.access_token().to_string(),
+            refresh_token: session.get_refresh_token().map(|s| s.to_string()),
+        };
+
+        let json = serde_json::to_string_pretty(&session_data)?;
+        fs::write(session_file, json)?;
+        info!("Session saved to {:?}", session_file);
+    } else {
+        warn!("No session available to save");
+    }
+    Ok(())
+}
+
+fn load_session(session_file: &PathBuf) -> Option<SessionData> {
+    if !session_file.exists() {
+        return None;
+    }
+
+    match fs::read_to_string(session_file) {
+        Ok(json) => match serde_json::from_str(&json) {
+            Ok(session_data) => {
+                info!("Loaded session from {:?}", session_file);
+                Some(session_data)
+            }
+            Err(e) => {
+                warn!("Failed to parse session file {:?}: {}", session_file, e);
+                None
+            }
+        },
+        Err(e) => {
+            warn!("Failed to read session file {:?}: {}", session_file, e);
+            None
+        }
+    }
+}
+
 async fn handle_message(
     event: OriginalSyncRoomMessageEvent,
     room: Room,
index d04b89eb564bf6d0a642ee069ccc1e63fea955fc..af1d42e1dac6ccf1a54cf922ca5ddd6b46772ed9 100644 (file)
@@ -1,4 +1,4 @@
-use std::env;
+use std::{env, path::PathBuf};
 
 use anyhow::{Context, Result};
 use serde::Deserialize;
@@ -10,22 +10,32 @@ pub struct Config {
     pub matrix_password: String,
     pub database_url: String,
     pub bot_room_id: Option<String>,
+    pub session_file: PathBuf,
+    pub store_path: PathBuf,
 }
 
 impl Config {
     pub fn from_env() -> Result<Self> {
         dotenvy::dotenv().ok();
 
+        let username = env::var("MATRIX_USERNAME")
+            .context("MATRIX_USERNAME must be set")?;
+        
+        // Create session file path based on username
+        let session_file = PathBuf::from(format!("{}.session.json", username.replace(':', "_")));
+        let store_path = PathBuf::from(format!("matrix_store_{}.db", username.replace(':', "_")));
+
         Ok(Config {
             matrix_homeserver_url: env::var("MATRIX_HOMESERVER_URL")
                 .context("MATRIX_HOMESERVER_URL must be set")?,
-            matrix_username: env::var("MATRIX_USERNAME")
-                .context("MATRIX_USERNAME must be set")?,
+            matrix_username: username,
             matrix_password: env::var("MATRIX_PASSWORD")
                 .context("MATRIX_PASSWORD must be set")?,
             database_url: env::var("DATABASE_URL")
                 .unwrap_or_else(|_| "sqlite:memberbot.db".to_string()),
             bot_room_id: env::var("BOT_ROOM_ID").ok(),
+            session_file,
+            store_path,
         })
     }
 }