feat(#1671): reject matrix send when room has unread messages
This commit is contained in:
parent
d89666baa9
commit
f6e1651dff
2 changed files with 43 additions and 3 deletions
|
|
@ -168,7 +168,9 @@ impl MatrixBridge {
|
|||
#[tool(
|
||||
description = "Post a plain-text or markdown message to a matrix room. \
|
||||
`room` is either a room id (!abc:server) or alias (#name:server). \
|
||||
Returns the new event id."
|
||||
Returns the new event id. Rejected with a hint if the room still has \
|
||||
unread messages — read_room then mark_read the latest event first so \
|
||||
you don't talk over messages you haven't seen."
|
||||
)]
|
||||
async fn send_message(&self, Parameters(args): Parameters<SendMessageArgs>) -> String {
|
||||
render(
|
||||
|
|
@ -181,7 +183,9 @@ impl MatrixBridge {
|
|||
}
|
||||
|
||||
#[tool(description = "Open (or reuse) a direct message room with `user_id` \
|
||||
(@user:server) and post `body` to it.")]
|
||||
(@user:server) and post `body` to it. If the DM room already exists \
|
||||
and has unread messages, the send is rejected with a hint — read_room \
|
||||
then mark_read the latest event first.")]
|
||||
async fn send_dm(&self, Parameters(args): Parameters<SendDmArgs>) -> String {
|
||||
render(
|
||||
round_trip(DaemonRequest::SendDm {
|
||||
|
|
@ -209,7 +213,9 @@ impl MatrixBridge {
|
|||
}
|
||||
|
||||
#[tool(description = "Reply to a specific matrix event in a room, threaded \
|
||||
via m.in_reply_to. Returns the reply's event id.")]
|
||||
via m.in_reply_to. Returns the reply's event id. Rejected with a hint \
|
||||
if the room still has unread messages — read_room then mark_read the \
|
||||
latest event first.")]
|
||||
async fn send_reply(&self, Parameters(args): Parameters<SendReplyArgs>) -> String {
|
||||
render(
|
||||
round_trip(DaemonRequest::SendReply {
|
||||
|
|
|
|||
|
|
@ -134,11 +134,39 @@ fn extract_body_sync(event: &matrix_sdk::ruma::events::AnySyncTimelineEvent) ->
|
|||
}
|
||||
}
|
||||
|
||||
/// Refuse to post into a room the agent hasn't caught up on. Returns
|
||||
/// `Some(error)` with a helpful hint when the room still has unread
|
||||
/// notifications (the agent must `read_room` then `mark_read` the
|
||||
/// latest event first), or `None` when the send may proceed.
|
||||
///
|
||||
/// Read-state is the matrix unread-notification count, the same signal
|
||||
/// the wake path and `get_loose_ends` use, so "caught up" here means
|
||||
/// exactly what those surfaces mean. Reactions and `mark_read` are not
|
||||
/// gated — only message-posting tools (`send_message`, `send_reply`,
|
||||
/// `send_dm`) so an agent can't talk over messages it hasn't seen.
|
||||
fn unread_guard(room: &matrix_sdk::Room) -> Option<DaemonResponse> {
|
||||
let count = room.unread_notification_counts().notification_count;
|
||||
if count == 0 {
|
||||
return None;
|
||||
}
|
||||
let label = room
|
||||
.canonical_alias()
|
||||
.map_or_else(|| room.room_id().to_string(), |a| a.to_string());
|
||||
Some(DaemonResponse::error(format!(
|
||||
"refusing to send: {count} unread message(s) in {label}. \
|
||||
use read_room to view them, then mark_read the latest event before \
|
||||
sending so you don't talk over messages you haven't seen."
|
||||
)))
|
||||
}
|
||||
|
||||
pub async fn send_message(client: &Client, room_ref: &str, body: &str) -> DaemonResponse {
|
||||
let room = match resolve_room(client, room_ref).await {
|
||||
Ok(r) => r,
|
||||
Err(e) => return e,
|
||||
};
|
||||
if let Some(reject) = unread_guard(&room) {
|
||||
return reject;
|
||||
}
|
||||
let content = RoomMessageEventContent::text_markdown(body);
|
||||
match room.send(content).await {
|
||||
Ok(resp) => DaemonResponse::ok(&serde_json::json!({
|
||||
|
|
@ -169,6 +197,9 @@ pub async fn send_dm(client: &Client, user_id: &str, body: &str) -> DaemonRespon
|
|||
Err(e) => return DaemonResponse::error(format!("create_dm {uid}: {e}")),
|
||||
},
|
||||
};
|
||||
if let Some(reject) = unread_guard(&room) {
|
||||
return reject;
|
||||
}
|
||||
let content = RoomMessageEventContent::text_markdown(body);
|
||||
match room.send(content).await {
|
||||
Ok(resp) => DaemonResponse::ok(&serde_json::json!({
|
||||
|
|
@ -214,6 +245,9 @@ pub async fn send_reply(
|
|||
Ok(r) => r,
|
||||
Err(e) => return e,
|
||||
};
|
||||
if let Some(reject) = unread_guard(&room) {
|
||||
return reject;
|
||||
}
|
||||
let eid: OwnedEventId = match event_id.parse() {
|
||||
Ok(e) => e,
|
||||
Err(e) => return DaemonResponse::error(format!("invalid event_id {event_id}: {e}")),
|
||||
|
|
|
|||
Loading…
Reference in a new issue