From 1d8f2b68909fc44ffca3524184a08af546828788 Mon Sep 17 00:00:00 2001 From: Taylor Southwick Date: Thu, 5 Mar 2026 18:57:00 +0000 Subject: [PATCH] Add support for access tokens using MAS --- etc/app/config.yml.dist | 9 +++++++++ src/bot/implementation.rs | 25 +++++++++++++++++++----- src/bot/load_config.rs | 4 +++- src/entity/cfg/config.rs | 40 ++++++++++++++++++++++++++++++++------- src/entity/cfg/env.rs | 2 ++ 5 files changed, 67 insertions(+), 13 deletions(-) diff --git a/etc/app/config.yml.dist b/etc/app/config.yml.dist index 326dcb1..1742d7b 100644 --- a/etc/app/config.yml.dist +++ b/etc/app/config.yml.dist @@ -5,8 +5,17 @@ homeserver: user: mxid_localpart: baibot + + # Authentication: set EITHER password OR access_token + device_id. + # + # Password-based login (traditional homeservers): password: baibot + # Access token login (for MAS/OIDC-enabled homeservers): + # Generate a token via: mas-cli manage issue-compatibility-token [device_id] + # access_token: null + # device_id: null + # The name the bot uses as a display name and when it refers to itself. # Leave empty to use the default (baibot). name: baibot diff --git a/src/bot/implementation.rs b/src/bot/implementation.rs index e85f7b9..665880d 100644 --- a/src/bot/implementation.rs +++ b/src/bot/implementation.rs @@ -6,7 +6,7 @@ use mxlink::matrix_sdk::Room; use mxlink::matrix_sdk::media::{MediaFormat, MediaRequestParameters}; use mxlink::matrix_sdk::ruma::api::client::profile::{AvatarUrl, DisplayName}; use mxlink::matrix_sdk::ruma::{ - MilliSecondsSinceUnixEpoch, OwnedUserId, events::room::MediaSource, + MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedUserId, events::room::MediaSource, }; use mxlink::{ @@ -395,10 +395,25 @@ async fn create_matrix_link(config: &Config) -> anyhow::Result { let session_encryption_key = config.persistence.session_encryption_key()?; let db_dir_path: std::path::PathBuf = config.persistence.db_dir_path()?; - let login_creds = LoginCredentials::UserPassword( - config.user.mxid_localpart.to_owned(), - config.user.password.to_owned(), - ); + let login_creds = if let Some(access_token) = &config.user.access_token { + let server_name = &config.homeserver.server_name; + let localpart = &config.user.mxid_localpart; + let user_id = OwnedUserId::try_from(format!("@{localpart}:{server_name}")) + .map_err(|e| anyhow::anyhow!("Invalid user ID: {e}"))?; + let device_id = OwnedDeviceId::from( + config.user.device_id.as_deref().expect("device_id must be set for access token auth"), + ); + LoginCredentials::AccessToken { + user_id, + device_id, + access_token: access_token.to_owned(), + } + } else { + LoginCredentials::UserPassword( + config.user.mxid_localpart.to_owned(), + config.user.password.as_deref().expect("password must be set if access_token is not").to_owned(), + ) + }; let login_encryption = LoginEncryption::new( config.user.encryption.recovery_passphrase.clone(), diff --git a/src/bot/load_config.rs b/src/bot/load_config.rs index c43362e..740138e 100644 --- a/src/bot/load_config.rs +++ b/src/bot/load_config.rs @@ -29,7 +29,9 @@ pub fn load() -> anyhow::Result { cfg_env::BAIBOT_HOMESERVER_SERVER_NAME => config.homeserver.server_name = value, cfg_env::BAIBOT_HOMESERVER_URL => config.homeserver.url = value, cfg_env::BAIBOT_USER_MXID_LOCALPART => config.user.mxid_localpart = value, - cfg_env::BAIBOT_USER_PASSWORD => config.user.password = value, + cfg_env::BAIBOT_USER_PASSWORD => config.user.password = Some(value), + cfg_env::BAIBOT_USER_ACCESS_TOKEN => config.user.access_token = Some(value), + cfg_env::BAIBOT_USER_DEVICE_ID => config.user.device_id = Some(value), cfg_env::BAIBOT_USER_ENCRYPTION_RECOVERY_PASSPHRASE => { config.user.encryption.recovery_passphrase = Some(value); } diff --git a/src/entity/cfg/config.rs b/src/entity/cfg/config.rs index 448c72d..c17d344 100644 --- a/src/entity/cfg/config.rs +++ b/src/entity/cfg/config.rs @@ -127,7 +127,15 @@ impl Avatar { #[derive(Debug, Serialize, Deserialize)] pub struct ConfigUser { pub mxid_localpart: String, - pub password: String, + + #[serde(default)] + pub password: Option, + + #[serde(default)] + pub access_token: Option, + + #[serde(default)] + pub device_id: Option, #[serde(default = "super::defaults::name")] pub name: String, @@ -148,12 +156,7 @@ impl ConfigUser { )); } - if self.password.is_empty() { - return Err(anyhow::anyhow!( - "The user.password ({}) configuration must be set", - super::env::BAIBOT_USER_PASSWORD - )); - } + self.validate_auth()?; if self.name.is_empty() { return Err(anyhow::anyhow!( @@ -166,6 +169,29 @@ impl ConfigUser { Ok(()) } + + fn validate_auth(&self) -> anyhow::Result<()> { + let has_password = self.password.as_deref().is_some_and(|p| !p.is_empty()); + let has_access_token = self.access_token.as_deref().is_some_and(|t| !t.is_empty()); + let has_device_id = self.device_id.as_deref().is_some_and(|d| !d.is_empty()); + + if !has_password && !has_access_token { + return Err(anyhow::anyhow!( + "Either user.password ({}) or user.access_token ({}) must be set", + super::env::BAIBOT_USER_PASSWORD, + super::env::BAIBOT_USER_ACCESS_TOKEN + )); + } + + if has_access_token && !has_device_id { + return Err(anyhow::anyhow!( + "user.device_id ({}) must be set when using access token authentication", + super::env::BAIBOT_USER_DEVICE_ID + )); + } + + Ok(()) + } } #[derive(Debug, Default, Serialize, Deserialize)] diff --git a/src/entity/cfg/env.rs b/src/entity/cfg/env.rs index 101fd0e..36cf9cc 100644 --- a/src/entity/cfg/env.rs +++ b/src/entity/cfg/env.rs @@ -5,6 +5,8 @@ pub const BAIBOT_HOMESERVER_URL: &str = "BAIBOT_HOMESERVER_URL"; pub const BAIBOT_USER_MXID_LOCALPART: &str = "BAIBOT_USER_MXID_LOCALPART"; pub const BAIBOT_USER_PASSWORD: &str = "BAIBOT_USER_PASSWORD"; +pub const BAIBOT_USER_ACCESS_TOKEN: &str = "BAIBOT_USER_ACCESS_TOKEN"; +pub const BAIBOT_USER_DEVICE_ID: &str = "BAIBOT_USER_DEVICE_ID"; pub const BAIBOT_USER_NAME: &str = "BAIBOT_USER_NAME"; pub const BAIBOT_USER_AVATAR: &str = "BAIBOT_USER_AVATAR"; pub const BAIBOT_USER_ENCRYPTION_RECOVERY_PASSPHRASE: &str =