198 lines
5.7 KiB
Rust
198 lines
5.7 KiB
Rust
use std::collections::HashMap;
|
|
|
|
use matrix_sdk::{
|
|
authentication::matrix::MatrixSession,
|
|
ruma::{OwnedEventId, OwnedRoomId, OwnedUserId},
|
|
};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// One prior version of an edited message.
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct EditRecord {
|
|
pub body: String,
|
|
pub ts: i64,
|
|
pub ts_human: String,
|
|
}
|
|
|
|
/// Serializable shape for one timeline event, used both in matrix_turn JSON
|
|
/// (input to the shard) and tool response JSON (get_room_history,
|
|
/// fetch_event).
|
|
#[derive(Debug, Serialize)]
|
|
#[serde(tag = "kind", rename_all = "lowercase")]
|
|
pub enum WireEvent {
|
|
Message {
|
|
event_id: String,
|
|
event_id_short: String,
|
|
sender: String,
|
|
is_self: bool,
|
|
ts: i64,
|
|
ts_human: String,
|
|
body: String,
|
|
in_reply_to: Option<String>,
|
|
read_by: Vec<String>,
|
|
/// Prior versions of this message, oldest first. Empty when the
|
|
/// message has never been edited.
|
|
edit_history: Vec<EditRecord>,
|
|
},
|
|
Reaction {
|
|
sender: String,
|
|
is_self: bool,
|
|
ts: i64,
|
|
ts_human: String,
|
|
target_event_id: String,
|
|
target_event_id_short: String,
|
|
key: String,
|
|
},
|
|
Edit {
|
|
sender: String,
|
|
is_self: bool,
|
|
ts: i64,
|
|
ts_human: String,
|
|
target_event_id: String,
|
|
target_event_id_short: String,
|
|
old_body: String,
|
|
new_body: String,
|
|
},
|
|
/// Synthetic event from the daemon (not a Matrix event). Currently used
|
|
/// to tell the shard "you were rate-limited; events held for X seconds."
|
|
Notice {
|
|
text: String,
|
|
ts: i64,
|
|
ts_human: String,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct RoomInfo {
|
|
pub room_id: String,
|
|
pub name: String,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct MemberInfo {
|
|
pub user_id: String,
|
|
pub display_name: String,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct FetchEventResult {
|
|
pub event: Option<WireEvent>,
|
|
pub context_before: Vec<WireEvent>,
|
|
pub earlier_handle: Option<String>,
|
|
}
|
|
|
|
pub const DEFAULT_MODEL: &str = "claude-sonnet-4-6";
|
|
pub const DEFAULT_MAX_HISTORY: usize = 20;
|
|
pub const DEFAULT_RATE_LIMIT_PER_MIN: u32 = 1;
|
|
pub const DEFAULT_RATE_BURST_CAPACITY: u32 = 3;
|
|
pub const DEFAULT_SESSION_IDLE_MINUTES: u64 = 10;
|
|
pub const DEFAULT_SESSION_MAX_EVENTS: u32 = 100;
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct Config {
|
|
pub homeserver: String,
|
|
pub username: String,
|
|
pub password: String,
|
|
pub rate_limit_per_min: Option<u32>,
|
|
pub rate_burst_capacity: Option<u32>,
|
|
pub model: Option<String>,
|
|
pub max_history: Option<usize>,
|
|
pub session_idle_minutes: Option<u64>,
|
|
pub session_max_events: Option<u32>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
pub struct PersistedSession {
|
|
pub homeserver: String,
|
|
pub db_path: std::path::PathBuf,
|
|
pub user_session: MatrixSession,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub sync_token: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub enum TimelineItem {
|
|
Message {
|
|
event_id: OwnedEventId,
|
|
sender: OwnedUserId,
|
|
/// Latest version of the message body. If the user edited this message,
|
|
/// this is the most recent edit's content.
|
|
body: String,
|
|
is_self: bool,
|
|
/// Unix seconds. 0 if unknown.
|
|
ts: i64,
|
|
in_reply_to: Option<OwnedEventId>,
|
|
/// Prior versions of this message (oldest first), excluding the
|
|
/// current `body`. Empty when the message has never been edited.
|
|
edit_history: Vec<EditRecord>,
|
|
},
|
|
Reaction {
|
|
sender: OwnedUserId,
|
|
target_event_id: OwnedEventId,
|
|
key: String,
|
|
is_self: bool,
|
|
ts: i64,
|
|
},
|
|
/// A single edit applied to a message. Surfaced as its own chronological
|
|
/// event so the shard sees edits as they happen. The target message also
|
|
/// has its `edit_history` updated for at-a-glance reference.
|
|
Edit {
|
|
sender: OwnedUserId,
|
|
target_event_id: OwnedEventId,
|
|
old_body: String,
|
|
new_body: String,
|
|
is_self: bool,
|
|
ts: i64,
|
|
},
|
|
}
|
|
|
|
impl TimelineItem {
|
|
pub fn ts(&self) -> i64 {
|
|
match self {
|
|
Self::Message { ts, .. } | Self::Reaction { ts, .. } | Self::Edit { ts, .. } => *ts,
|
|
}
|
|
}
|
|
|
|
pub fn event_id(&self) -> Option<&OwnedEventId> {
|
|
match self {
|
|
Self::Message { event_id, .. } => Some(event_id),
|
|
Self::Reaction { .. } | Self::Edit { .. } => None,
|
|
}
|
|
}
|
|
|
|
pub fn sender(&self) -> &OwnedUserId {
|
|
match self {
|
|
Self::Message { sender, .. }
|
|
| Self::Reaction { sender, .. }
|
|
| Self::Edit { sender, .. } => sender,
|
|
}
|
|
}
|
|
|
|
pub fn is_self(&self) -> bool {
|
|
match self {
|
|
Self::Message { is_self, .. }
|
|
| Self::Reaction { is_self, .. }
|
|
| Self::Edit { is_self, .. } => *is_self,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct DaemonState {
|
|
pub own_user_id: OwnedUserId,
|
|
/// Per-room: the latest event_id that's been "shown" to Claude. Events
|
|
/// after this are "new" on the next invocation. Cleared on daemon restart.
|
|
pub last_shown: HashMap<OwnedRoomId, OwnedEventId>,
|
|
/// Rooms with unprocessed events. The Instant is when the room first
|
|
/// entered the queue (or last became empty, then refilled). Used to
|
|
/// surface rate-limit delays to the shard via a synthetic notice event.
|
|
pub pending_rooms: Vec<(OwnedRoomId, std::time::Instant)>,
|
|
pub rate_budget: f64,
|
|
pub rate_limit_per_min: u32,
|
|
pub rate_burst_capacity: u32,
|
|
pub last_rate_check: std::time::Instant,
|
|
pub model: String,
|
|
pub max_history: usize,
|
|
pub session_idle_minutes: u64,
|
|
pub session_max_events: u32,
|
|
}
|
|
|