From: Erik Mackdanz Date: Sun, 22 Feb 2026 20:38:29 +0000 (-0600) Subject: Persist device ID so a new one isn't created every time X-Git-Url: https://git.humopery.space/?a=commitdiff_plain;h=60297daecb63d662240adb2a6bb3800a2fb1dc66;p=private%2Fmemberbot.git Persist device ID so a new one isn't created every time --- diff --git a/.gitignore b/.gitignore index 3b05ad8..630a395 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ /target .env memberbot.db -matrix_store.db/* \ No newline at end of file +matrix_store.db/* +*.session.json +matrix_store_* diff --git a/src/bot.rs b/src/bot.rs index 6af3de3..8e4cc15 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -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, +} + pub struct Bot { client: Client, db: Arc, @@ -33,23 +43,28 @@ impl Bot { pub async fn new(config: Config, db: Database) -> Result { 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 { + 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 { + 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 { + 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, diff --git a/src/config.rs b/src/config.rs index d04b89e..af1d42e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -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, + pub session_file: PathBuf, + pub store_path: PathBuf, } impl Config { pub fn from_env() -> Result { 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, }) } }