Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 64 additions & 10 deletions src/channels/matrix.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
use crate::channels::traits::{Channel, ChannelMessage, SendMessage};
use crate::config::schema::MatrixRoomConfig;
use async_trait::async_trait;
use matrix_sdk::{
authentication::matrix::MatrixSession,
config::SyncSettings,
ruma::{
events::reaction::ReactionEventContent,
events::relation::{Annotation, InReplyTo, Thread},
events::room::message::Relation,
events::room::message::{
MessageType, OriginalSyncRoomMessageEvent, RoomMessageEventContent,
},
Expand Down Expand Up @@ -38,6 +40,8 @@ pub struct MatrixChannel {
http_client: Client,
reaction_events: Arc<RwLock<HashMap<String, String>>>,
voice_mode: Arc<AtomicBool>,
room_configs: HashMap<String, MatrixRoomConfig>,
bot_display_name: Arc<RwLock<Option<String>>>,
}

impl std::fmt::Debug for MatrixChannel {
Expand Down Expand Up @@ -138,6 +142,7 @@ impl MatrixChannel {
owner_hint,
device_id_hint,
None,
HashMap::new(),
)
}

Expand All @@ -149,6 +154,7 @@ impl MatrixChannel {
owner_hint: Option<String>,
device_id_hint: Option<String>,
zeroclaw_dir: Option<PathBuf>,
room_configs: HashMap<String, MatrixRoomConfig>,
) -> Self {
let homeserver = homeserver.trim_end_matches('/').to_string();
let access_token = access_token.trim().to_string();
Expand All @@ -172,6 +178,8 @@ impl MatrixChannel {
http_client: Client::new(),
reaction_events: Arc::new(RwLock::new(HashMap::new())),
voice_mode: Arc::new(AtomicBool::new(false)),
room_configs,
bot_display_name: Arc::new(RwLock::new(None)),
}
}

Expand Down Expand Up @@ -681,11 +689,29 @@ impl Channel for MatrixChannel {

let _ = client.sync_once(SyncSettings::new()).await;

tracing::info!(
"Matrix channel listening on room {} (configured as {})...",
target_room_id,
self.room_id
);
// Resolve bot display name for mention detection
if let Ok(Some(name)) = client.account().get_display_name().await {
*self.bot_display_name.write().await = Some(name);
}

if self.room_configs.is_empty() {
tracing::info!(
"Matrix channel listening on room {} (configured as {})...",
target_room_id,
self.room_id
);
} else {
let enabled: Vec<_> = self.room_configs.iter()
.filter(|(_, cfg)| cfg.enabled)
.map(|(id, _)| id.as_str())
.collect();
tracing::info!(
"Matrix channel listening on {} rooms (default: {}): {:?}",
enabled.len(),
self.room_id,
enabled
);
}

let recent_event_cache = Arc::new(Mutex::new((
std::collections::VecDeque::new(),
Expand All @@ -700,6 +726,8 @@ impl Channel for MatrixChannel {
let homeserver_for_handler = self.homeserver.clone();
let access_token_for_handler = self.access_token.clone();
let voice_mode_for_handler = Arc::clone(&self.voice_mode);
let room_configs_for_handler = self.room_configs.clone();
let bot_display_name_for_handler = Arc::clone(&self.bot_display_name);

client.add_event_handler(move |event: OriginalSyncRoomMessageEvent, room: Room| {
let tx = tx_handler.clone();
Expand All @@ -710,13 +738,25 @@ impl Channel for MatrixChannel {
let homeserver = homeserver_for_handler.clone();
let access_token = access_token_for_handler.clone();
let voice_mode = Arc::clone(&voice_mode_for_handler);
let room_configs = room_configs_for_handler.clone();
let bot_display_name = Arc::clone(&bot_display_name_for_handler);

async move {
if false
/* multi-room: room_id filter disabled */
{
return;
}
// Multi-room: filter events by room config
let event_room_id = room.room_id().to_string();
let room_config = if room_configs.is_empty() {
// Single-room mode: only accept the configured target room
if room.room_id() != target_room {
return;
}
None
} else {
// Multi-room mode: check room map
match room_configs.get(&event_room_id) {
Some(cfg) if cfg.enabled => Some(cfg.clone()),
_ => return, // room not in config or disabled
}
};

if event.sender == my_user_id {
return;
Expand Down Expand Up @@ -849,6 +889,20 @@ impl Channel for MatrixChannel {
return;
}

// Per-room mention check
if let Some(ref cfg) = room_config {
if cfg.require_mention {
let mentioned = body.contains(my_user_id.as_str())
|| bot_display_name.read().await
.as_ref()
.map(|name| body.to_lowercase().contains(&name.to_lowercase()))
.unwrap_or(false);
if !mentioned {
return;
}
}
}

let event_id = event.event_id.to_string();
{
let mut guard = dedupe.lock().await;
Expand Down
4 changes: 3 additions & 1 deletion src/channels/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1683,7 +1683,8 @@ async fn process_channel_message(
msg
};

let target_channel = ctx.channels_by_name.get(&msg.channel).cloned();
let channel_name = msg.channel.split(':').next().unwrap_or(&msg.channel);
let target_channel = ctx.channels_by_name.get(channel_name).cloned();
if let Err(err) = maybe_apply_runtime_config_update(ctx.as_ref()).await {
tracing::warn!("Failed to apply runtime config update: {err}");
}
Expand Down Expand Up @@ -2940,6 +2941,7 @@ fn collect_configured_channels(
mx.user_id.clone(),
mx.device_id.clone(),
config.config_path.parent().map(|path| path.to_path_buf()),
mx.rooms.clone(),
)),
});
}
Expand Down
18 changes: 18 additions & 0 deletions src/config/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3123,6 +3123,17 @@ impl ChannelConfig for IMessageConfig {
}
}

/// Per-room configuration for Matrix multi-room support.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct MatrixRoomConfig {
/// Whether this room is enabled. Defaults to true.
#[serde(default = "default_true")]
pub enabled: bool,
/// If true, the bot only responds when mentioned. Defaults to false.
#[serde(default)]
pub require_mention: bool,
}

/// Matrix channel configuration.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct MatrixConfig {
Expand All @@ -3140,6 +3151,10 @@ pub struct MatrixConfig {
pub room_id: String,
/// Allowed Matrix user IDs. Empty = deny all.
pub allowed_users: Vec<String>,
/// Per-room overrides. Keys are room IDs (e.g. "!abc:matrix.org").
/// When absent, only `room_id` is listened to (backward compatible).
#[serde(default)]
pub rooms: HashMap<String, MatrixRoomConfig>,
}

impl ChannelConfig for MatrixConfig {
Expand Down Expand Up @@ -6240,6 +6255,7 @@ tool_dispatcher = "xml"
device_id: Some("DEVICE123".into()),
room_id: "!room123:matrix.org".into(),
allowed_users: vec!["@user:matrix.org".into()],
rooms: HashMap::new(),
};
let json = serde_json::to_string(&mc).unwrap();
let parsed: MatrixConfig = serde_json::from_str(&json).unwrap();
Expand All @@ -6260,6 +6276,7 @@ tool_dispatcher = "xml"
device_id: None,
room_id: "!abc:synapse.local".into(),
allowed_users: vec!["@admin:synapse.local".into(), "*".into()],
rooms: HashMap::new(),
};
let toml_str = toml::to_string(&mc).unwrap();
let parsed: MatrixConfig = toml::from_str(&toml_str).unwrap();
Expand Down Expand Up @@ -6364,6 +6381,7 @@ allowed_users = ["@ops:matrix.org"]
nostr: None,
clawdtalk: None,
message_timeout_secs: 300,
rooms: HashMap::new(),
};
let toml_str = toml::to_string_pretty(&c).unwrap();
let parsed: ChannelsConfig = toml::from_str(&toml_str).unwrap();
Expand Down
1 change: 1 addition & 0 deletions src/integrations/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -865,6 +865,7 @@ mod tests {
device_id: None,
room_id: "!r:m".into(),
allowed_users: vec![],
rooms: std::collections::HashMap::new(),
});
let entries = all_integrations();
let mx = entries.iter().find(|e| e.name == "Matrix").unwrap();
Expand Down
1 change: 1 addition & 0 deletions src/onboard/wizard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4011,6 +4011,7 @@ fn setup_channels() -> Result<ChannelsConfig> {
device_id: detected_device_id,
room_id,
allowed_users,
rooms: std::collections::HashMap::new(),
});
}
ChannelMenuChoice::Signal => {
Expand Down