Allow setting an avatar image for the bot's account

This commit is contained in:
2025-08-10 00:06:22 -07:00
parent 602d562c66
commit ed0aa2a2bf
6 changed files with 81 additions and 5 deletions

27
Cargo.lock generated
View File

@@ -371,6 +371,9 @@ dependencies = [
"http", "http",
"log", "log",
"matrix-sdk", "matrix-sdk",
"mime",
"mime-sniffer",
"mime_guess2",
"quick-xml", "quick-xml",
"regex", "regex",
"reqwest", "reqwest",
@@ -1826,12 +1829,34 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "mime-sniffer"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b8b2a64cd735f1d5f17ff6701ced3cc3c54851f9448caf454cd9c923d812408"
dependencies = [
"mime",
"url",
]
[[package]] [[package]]
name = "mime2ext" name = "mime2ext"
version = "0.1.54" version = "0.1.54"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbf6f36070878c42c5233846cd3de24cf9016828fd47bc22957a687298bb21fc" checksum = "cbf6f36070878c42c5233846cd3de24cf9016828fd47bc22957a687298bb21fc"
[[package]]
name = "mime_guess2"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1706dc14a2e140dec0a7a07109d9a3d5890b81e85bd6c60b906b249a77adf0ca"
dependencies = [
"mime",
"phf",
"phf_shared",
"unicase",
]
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.8.9" version = "0.8.9"
@@ -2034,6 +2059,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn",
"unicase",
] ]
[[package]] [[package]]
@@ -2043,6 +2069,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
dependencies = [ dependencies = [
"siphasher", "siphasher",
"unicase",
] ]
[[package]] [[package]]

View File

@@ -34,6 +34,9 @@ futures = "0.3"
http = "1.3" http = "1.3"
log = { version = "0.4", features = ["std"] } log = { version = "0.4", features = ["std"] }
matrix-sdk = { version = "0.13", features = ["anyhow", "markdown", "rustls-tls"], default-features = false } matrix-sdk = { version = "0.13", features = ["anyhow", "markdown", "rustls-tls"], default-features = false }
mime = "0.3"
mime-sniffer = "0.1"
mime_guess2 = "2"
quick-xml = { version = "0.38", features = ["serialize"] } quick-xml = { version = "0.38", features = ["serialize"] }
regex = "1" regex = "1"
reqwest = { version = "0.12", default-features = false, features = ["charset", "http2", "gzip", "rustls-tls-native-roots", "system-proxy"] } reqwest = { version = "0.12", default-features = false, features = ["charset", "http2", "gzip", "rustls-tls-native-roots", "system-proxy"] }

View File

@@ -6,6 +6,8 @@ bind_port: 3000
user_id: "@mybebot:example.com" user_id: "@mybebot:example.com"
# Password for Matrix user. # Password for Matrix user.
password: "secret-matrix-account-password" password: "secret-matrix-account-password"
# Optional path to an image file to use as the bot account's avatar.
avatar_image_path: "/path/to/avatar.jpg"
# All Gitlab-specific settings are under here. # All Gitlab-specific settings are under here.
gitlab_webhook: gitlab_webhook:
# Optional prefix to serve the webhook path under (default is empty string). # Optional prefix to serve the webhook path under (default is empty string).

View File

@@ -87,6 +87,7 @@ pub struct Config {
#[serde(deserialize_with = "crate::matrix::deser_user_id")] #[serde(deserialize_with = "crate::matrix::deser_user_id")]
pub user_id: OwnedUserId, pub user_id: OwnedUserId,
pub password: String, pub password: String,
pub avatar_image_path: Option<PathBuf>,
pub gitlab_webhook: Option<GitlabWebhookConfig>, pub gitlab_webhook: Option<GitlabWebhookConfig>,
pub mail_archive: Option<MailArchiveConfig>, pub mail_archive: Option<MailArchiveConfig>,
} }

View File

@@ -41,13 +41,25 @@ async fn run() -> anyhow::Result<()> {
.ok_or_else(|| anyhow!("Config file should be passed as only parameter"))?; .ok_or_else(|| anyhow!("Config file should be passed as only parameter"))?;
let mut config = config::load(config_path).await?; let mut config = config::load(config_path).await?;
let matrix_client = matrix::connect(&config).await.context("Failed to connect to Matrix")?; let mut join_handles = Vec::default();
let handles = if let Some(mail_archive) = config.mail_archive.take() { let matrix_client = matrix::connect(&config).await.context("Failed to connect to Matrix")?;
if let Some(avatar_path) = config.avatar_image_path {
let matrix_client = matrix_client.clone();
let handle = tokio::spawn(async move {
if let Err(err) = matrix::set_avatar_if_needed(&matrix_client, avatar_path).await {
warn!("Failed to set matrix avatar: {err}");
}
});
join_handles.push(handle);
}
let mail_join_handles = if let Some(mail_archive) = config.mail_archive.take() {
mail_archive::start_polling(mail_archive, matrix_client.clone())? mail_archive::start_polling(mail_archive, matrix_client.clone())?
} else { } else {
vec![] vec![]
}; };
join_handles.extend(mail_join_handles);
if let Some(gitlab_webhook) = config.gitlab_webhook.take() { if let Some(gitlab_webhook) = config.gitlab_webhook.take() {
let gitlab = gitlab_webhook::build_route(gitlab_webhook, matrix_client.clone()); let gitlab = gitlab_webhook::build_route(gitlab_webhook, matrix_client.clone());
@@ -60,7 +72,7 @@ async fn run() -> anyhow::Result<()> {
axum::serve(listener, gitlab).await?; axum::serve(listener, gitlab).await?;
} }
join_all(handles).await; join_all(join_handles).await;
error!("No functionality is configured; exiting"); error!("No functionality is configured; exiting");
exit(1); exit(1);

View File

@@ -14,16 +14,19 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::{fmt, process::exit, time::Duration}; use std::{fmt, path::Path, process::exit, time::Duration};
use futures::TryFutureExt;
use matrix_sdk::{ use matrix_sdk::{
config::SyncSettings, config::SyncSettings,
media::MediaFormat,
room::Room, room::Room,
ruma::{OwnedRoomOrAliasId, OwnedUserId, RoomOrAliasId, UserId}, ruma::{OwnedRoomOrAliasId, OwnedUserId, RoomOrAliasId, UserId},
BaseRoom, Client, BaseRoom, Client,
}; };
use mime_sniffer::MimeTypeSnifferExt;
use serde::de; use serde::de;
use tokio::time::sleep; use tokio::{fs, time::sleep};
use crate::config::Config; use crate::config::Config;
@@ -60,6 +63,34 @@ pub async fn connect(config: &Config) -> anyhow::Result<Client> {
Ok(client) Ok(client)
} }
pub async fn set_avatar_if_needed<P: AsRef<Path>>(client: &Client, avatar_path: P) -> anyhow::Result<()> {
let new_avatar = fs::read(&avatar_path).await?;
let account = client.account();
let cur_avatar = account.get_avatar(MediaFormat::File).await?;
if cur_avatar.as_ref() != Some(&new_avatar) {
// Falling back to extension matching because for some reason mime-sniffer can't handle SVG
// files, even though it seems like it should be able to.
if let Some(media_type) = new_avatar
.as_slice()
.sniff_mime_type_ext()
.or_else(|| mime_guess2::from_path(&avatar_path).first())
{
info!("Setting avatar image from '{}'", avatar_path.as_ref().to_string_lossy());
account
.upload_avatar(&media_type, new_avatar)
.map_err(Into::into)
.map_ok(|_| ())
.await
} else {
Err(anyhow!("Cannot determine media type of avatar image"))
}
} else {
Ok(())
}
}
pub async fn ensure_room_joined(matrix_client: &Client, room_id: &OwnedRoomOrAliasId) -> anyhow::Result<Room> { pub async fn ensure_room_joined(matrix_client: &Client, room_id: &OwnedRoomOrAliasId) -> anyhow::Result<Room> {
fn room_matches(a_room: &BaseRoom, our_room: &OwnedRoomOrAliasId) -> bool { fn room_matches(a_room: &BaseRoom, our_room: &OwnedRoomOrAliasId) -> bool {
let our_room_str = our_room.as_str(); let our_room_str = our_room.as_str();