From 901226a7d173449d26d030443fd90b7f16f8c0f4 Mon Sep 17 00:00:00 2001 From: Fmstrat Date: Wed, 10 Dec 2025 09:28:48 -0500 Subject: [PATCH 1/2] Add support for custom avatars --- etc/app/config.yml.dist | 4 ++++ src/bot/implementation.rs | 25 ++++++++++++++++++++----- src/bot/load_config.rs | 3 +++ src/entity/cfg/config.rs | 3 +++ src/entity/cfg/env.rs | 1 + 5 files changed, 31 insertions(+), 5 deletions(-) diff --git a/etc/app/config.yml.dist b/etc/app/config.yml.dist index 2785301..37f07d8 100644 --- a/etc/app/config.yml.dist +++ b/etc/app/config.yml.dist @@ -11,6 +11,10 @@ user: # Leave empty to use the default (baibot). name: baibot + # An optional path to a 768x768 PNG to be used as a custom avatar image. + # Leave empty to use the default. + avatar: + encryption: # An optional passphrase to use for backing up and recovering the bot's encryption keys. # You can use any string here. diff --git a/src/bot/implementation.rs b/src/bot/implementation.rs index c55a3a3..91b5d75 100644 --- a/src/bot/implementation.rs +++ b/src/bot/implementation.rs @@ -1,4 +1,5 @@ use std::sync::Arc; +use std::fs; use std::{future::Future, pin::Pin}; use mxlink::matrix_sdk::Room; @@ -316,6 +317,18 @@ impl Bot { } } + let logo_bytes: Option> = if self.inner.config.user.avatar.is_none() { + Some(LOGO_BYTES.to_vec()) + } else { + let avatar_path = self.inner.config.user.avatar.as_ref().unwrap(); + match fs::read(avatar_path) { + Ok(bytes) => Some(bytes), + Err(_e) => { + Some(LOGO_BYTES.to_vec()) + } + } + }; + let should_update_avatar = match ¤t_avatar_url { Some(avatar_url) => { let request = MediaRequestParameters { @@ -328,7 +341,7 @@ impl Bot { .await .map_err(|e| anyhow::anyhow!("Failed fetching existing avatar: {:?}", e))?; - content.as_slice() != LOGO_BYTES + Some(content.as_slice()) != logo_bytes.as_deref() } None => true, }; @@ -340,10 +353,12 @@ impl Bot { .parse() .expect("Failed parsing mime type for logo"); - account - .upload_avatar(&mime_type, LOGO_BYTES.to_vec()) - .await - .map_err(|e| anyhow::anyhow!("Failed uploading avatar: {:?}", e))?; + if let Some(bytes) = logo_bytes { + account + .upload_avatar(&mime_type, bytes) + .await + .map_err(|e| anyhow::anyhow!("Failed uploading avatar: {:?}", e))?; + } } Ok(()) diff --git a/src/bot/load_config.rs b/src/bot/load_config.rs index d56f6aa..a0768f2 100644 --- a/src/bot/load_config.rs +++ b/src/bot/load_config.rs @@ -37,6 +37,9 @@ pub fn load() -> anyhow::Result { config.user.encryption.recovery_reset_allowed = value.parse::()?; } cfg_env::BAIBOT_USER_NAME => config.user.name = value, + cfg_env::BAIBOT_USER_AVATAR => { + config.user.avatar = Some(value); + } cfg_env::BAIBOT_COMMAND_PREFIX => config.command_prefix = value, cfg_env::BAIBOT_ROOM_POST_JOIN_SELF_INTRODUCTION_ENABLED => { config.room.post_join_self_introduction_enabled = value.parse::()?; diff --git a/src/entity/cfg/config.rs b/src/entity/cfg/config.rs index 875e3ac..145d76c 100644 --- a/src/entity/cfg/config.rs +++ b/src/entity/cfg/config.rs @@ -93,6 +93,9 @@ pub struct ConfigUser { #[serde(default)] pub encryption: ConfigUserEncryption, + + #[serde(default)] + pub avatar: Option, } impl ConfigUser { diff --git a/src/entity/cfg/env.rs b/src/entity/cfg/env.rs index cfba7b6..101fd0e 100644 --- a/src/entity/cfg/env.rs +++ b/src/entity/cfg/env.rs @@ -6,6 +6,7 @@ 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_NAME: &str = "BAIBOT_USER_NAME"; +pub const BAIBOT_USER_AVATAR: &str = "BAIBOT_USER_AVATAR"; pub const BAIBOT_USER_ENCRYPTION_RECOVERY_PASSPHRASE: &str = "BAIBOT_USER_ENCRYPTION_RECOVERY_PASSPHRASE"; pub const BAIBOT_USER_ENCRYPTION_RECOVERY_RESET_ALLOWED: &str = From ac0576f62484e7d51783d17e5027919cee81997a Mon Sep 17 00:00:00 2001 From: Fmstrat Date: Sat, 13 Dec 2025 18:48:57 -0500 Subject: [PATCH 2/2] add mime-type support and switch to user.avatar.source --- Cargo.lock | 2 ++ Cargo.toml | 2 ++ etc/app/config.yml.dist | 5 +++-- src/bot/implementation.rs | 36 ++++++++++++++++++++---------------- src/bot/load_config.rs | 9 +++++++-- src/entity/cfg/config.rs | 8 +++++++- src/entity/cfg/env.rs | 2 +- src/entity/cfg/mod.rs | 2 +- 8 files changed, 43 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f0b671a..72289fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -297,6 +297,8 @@ dependencies = [ "chrono", "etke_openai_api_rust", "matrix-sdk", + "mime", + "mime_guess", "mxidwc", "mxlink", "quick_cache", diff --git a/Cargo.toml b/Cargo.toml index 1e9b35e..bbdf54d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,8 @@ tokio = { version = "1.48.*", features = ["rt", "rt-multi-thread", "macros"] } tracing = "0.1.*" tracing-subscriber = { version = "0.3.*", features = ["env-filter"] } url = "2.5.*" +mime_guess = "2.0.5" +mime = "0.3.17" [profile.release] strip = true diff --git a/etc/app/config.yml.dist b/etc/app/config.yml.dist index 37f07d8..177bfc9 100644 --- a/etc/app/config.yml.dist +++ b/etc/app/config.yml.dist @@ -11,9 +11,10 @@ user: # Leave empty to use the default (baibot). name: baibot - # An optional path to a 768x768 PNG to be used as a custom avatar image. - # Leave empty to use the default. avatar: + # An optional path to an image file to be used as a custom avatar image. + # Leave empty to use the default. + source: encryption: # An optional passphrase to use for backing up and recovering the bot's encryption keys. diff --git a/src/bot/implementation.rs b/src/bot/implementation.rs index 91b5d75..0dff359 100644 --- a/src/bot/implementation.rs +++ b/src/bot/implementation.rs @@ -1,6 +1,7 @@ use std::sync::Arc; use std::fs; use std::{future::Future, pin::Pin}; +use mime::Mime; use mxlink::matrix_sdk::Room; use mxlink::matrix_sdk::media::{MediaFormat, MediaRequestParameters}; @@ -317,14 +318,15 @@ impl Bot { } } - let logo_bytes: Option> = if self.inner.config.user.avatar.is_none() { - Some(LOGO_BYTES.to_vec()) + let logo_bytes: Vec = if self.inner.config.user.avatar.is_none() { + LOGO_BYTES.to_vec() } else { - let avatar_path = self.inner.config.user.avatar.as_ref().unwrap(); + let avatar = self.inner.config.user.avatar.as_ref().unwrap(); + let avatar_path = &avatar.source; match fs::read(avatar_path) { - Ok(bytes) => Some(bytes), + Ok(bytes) => bytes, Err(_e) => { - Some(LOGO_BYTES.to_vec()) + LOGO_BYTES.to_vec() } } }; @@ -341,24 +343,26 @@ impl Bot { .await .map_err(|e| anyhow::anyhow!("Failed fetching existing avatar: {:?}", e))?; - Some(content.as_slice()) != logo_bytes.as_deref() + content.as_slice() != logo_bytes } None => true, }; if should_update_avatar { - tracing::info!("Updating avatar.."); + let mime_type: Mime = if self.inner.config.user.avatar.is_none() { + LOGO_MIME_TYPE.parse::().expect("Failed parsing mime type for logo") + } else { + let avatar = self.inner.config.user.avatar.as_ref().unwrap(); + let avatar_path = &avatar.source; + mime_guess::guess_mime_type(avatar_path) + }; - let mime_type = LOGO_MIME_TYPE - .parse() - .expect("Failed parsing mime type for logo"); + tracing::info!("Updating avatar.."); - if let Some(bytes) = logo_bytes { - account - .upload_avatar(&mime_type, bytes) - .await - .map_err(|e| anyhow::anyhow!("Failed uploading avatar: {:?}", e))?; - } + account + .upload_avatar(&mime_type, logo_bytes) + .await + .map_err(|e| anyhow::anyhow!("Failed uploading avatar: {:?}", e))?; } Ok(()) diff --git a/src/bot/load_config.rs b/src/bot/load_config.rs index a0768f2..6d9931d 100644 --- a/src/bot/load_config.rs +++ b/src/bot/load_config.rs @@ -4,6 +4,7 @@ use std::path::PathBuf; use anyhow::anyhow; use crate::agent::AgentPurpose; +use crate::entity::cfg::config::ConfigAvatar; pub use crate::entity::cfg::{Config, defaults as cfg_defaults, env as cfg_env}; @@ -37,8 +38,12 @@ pub fn load() -> anyhow::Result { config.user.encryption.recovery_reset_allowed = value.parse::()?; } cfg_env::BAIBOT_USER_NAME => config.user.name = value, - cfg_env::BAIBOT_USER_AVATAR => { - config.user.avatar = Some(value); + cfg_env::BAIBOT_USER_AVATAR_SOURCE => { + if let Some(ref mut avatar) = config.user.avatar { + avatar.source = value; + } else { + config.user.avatar = Some(ConfigAvatar { source: value }); + } } cfg_env::BAIBOT_COMMAND_PREFIX => config.command_prefix = value, cfg_env::BAIBOT_ROOM_POST_JOIN_SELF_INTRODUCTION_ENABLED => { diff --git a/src/entity/cfg/config.rs b/src/entity/cfg/config.rs index 145d76c..eb8674b 100644 --- a/src/entity/cfg/config.rs +++ b/src/entity/cfg/config.rs @@ -83,6 +83,12 @@ impl ConfigHomeserver { } } +#[derive(Debug, Serialize, Deserialize)] +pub struct ConfigAvatar { + #[serde(default)] + pub source: String, +} + #[derive(Debug, Serialize, Deserialize)] pub struct ConfigUser { pub mxid_localpart: String, @@ -95,7 +101,7 @@ pub struct ConfigUser { pub encryption: ConfigUserEncryption, #[serde(default)] - pub avatar: Option, + pub avatar: Option, } impl ConfigUser { diff --git a/src/entity/cfg/env.rs b/src/entity/cfg/env.rs index 101fd0e..5fcb809 100644 --- a/src/entity/cfg/env.rs +++ b/src/entity/cfg/env.rs @@ -6,7 +6,7 @@ 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_NAME: &str = "BAIBOT_USER_NAME"; -pub const BAIBOT_USER_AVATAR: &str = "BAIBOT_USER_AVATAR"; +pub const BAIBOT_USER_AVATAR_SOURCE: &str = "BAIBOT_USER_AVATAR_SOURCE"; pub const BAIBOT_USER_ENCRYPTION_RECOVERY_PASSPHRASE: &str = "BAIBOT_USER_ENCRYPTION_RECOVERY_PASSPHRASE"; pub const BAIBOT_USER_ENCRYPTION_RECOVERY_RESET_ALLOWED: &str = diff --git a/src/entity/cfg/mod.rs b/src/entity/cfg/mod.rs index c4917b6..bb9e857 100644 --- a/src/entity/cfg/mod.rs +++ b/src/entity/cfg/mod.rs @@ -1,4 +1,4 @@ -mod config; +pub mod config; pub mod defaults; pub mod env;