-use std::sync::Arc;
+use std::{fs, path::PathBuf, sync::Arc};
use anyhow::Result;
use futures_util::StreamExt;
},
OwnedRoomId, UserId,
},
+ store::RoomLoadSettings,
Client,
};
+use serde::{Deserialize, Serialize};
use tracing::{error, info, warn};
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>,
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?;
}
}
+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,
-use std::env;
+use std::{env, path::PathBuf};
use anyhow::{Context, Result};
use serde::Deserialize;
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,
})
}
}