feat: multi-account matrix daemon (account-routed mcp surface)
This commit is contained in:
parent
73c53c7d4a
commit
8e79eb4f26
7 changed files with 613 additions and 156 deletions
|
|
@ -11,13 +11,15 @@ use matrix_sdk::Client;
|
|||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
use tokio::net::{UnixListener, UnixStream};
|
||||
|
||||
use crate::accounts::Registry;
|
||||
use crate::handlers;
|
||||
use crate::protocol::{DaemonRequest, DaemonResponse};
|
||||
use crate::protocol::{DaemonOp, DaemonRequest, DaemonResponse};
|
||||
|
||||
/// Start listening on `socket_path` and serve forever. Removes any
|
||||
/// stale socket file first (daemon restart after a non-clean shutdown
|
||||
/// would otherwise hit EADDRINUSE).
|
||||
pub async fn serve(socket_path: &Path, client: Client) -> Result<()> {
|
||||
/// would otherwise hit EADDRINUSE). `registry` resolves each request's
|
||||
/// `account` to the matrix client that serves it.
|
||||
pub async fn serve(socket_path: &Path, registry: Arc<Registry>) -> Result<()> {
|
||||
let _ = tokio::fs::remove_file(socket_path).await;
|
||||
if let Some(parent) = socket_path.parent() {
|
||||
tokio::fs::create_dir_all(parent)
|
||||
|
|
@ -27,27 +29,26 @@ pub async fn serve(socket_path: &Path, client: Client) -> Result<()> {
|
|||
let listener = UnixListener::bind(socket_path)
|
||||
.with_context(|| format!("bind unix socket {}", socket_path.display()))?;
|
||||
tracing::info!(path = %socket_path.display(), "mcp socket listener up");
|
||||
let client = Arc::new(client);
|
||||
loop {
|
||||
let (stream, _) = listener
|
||||
.accept()
|
||||
.await
|
||||
.context("accept connection on mcp socket")?;
|
||||
let client = client.clone();
|
||||
let registry = registry.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = handle_connection(stream, &client).await {
|
||||
if let Err(e) = handle_connection(stream, ®istry).await {
|
||||
tracing::warn!(error = %e, "mcp socket connection error");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_connection(stream: UnixStream, client: &Client) -> Result<()> {
|
||||
async fn handle_connection(stream: UnixStream, registry: &Registry) -> Result<()> {
|
||||
let (reader, mut writer) = stream.into_split();
|
||||
let mut lines = BufReader::new(reader).lines();
|
||||
while let Some(line) = lines.next_line().await? {
|
||||
let response = match serde_json::from_str::<DaemonRequest>(&line) {
|
||||
Ok(req) => dispatch(req, client).await,
|
||||
Ok(req) => dispatch(req, registry).await,
|
||||
Err(e) => DaemonResponse::error(format!("parse request: {e}")),
|
||||
};
|
||||
let mut json = serde_json::to_string(&response)?;
|
||||
|
|
@ -58,49 +59,60 @@ async fn handle_connection(stream: UnixStream, client: &Client) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn dispatch(req: DaemonRequest, client: &Client) -> DaemonResponse {
|
||||
match req {
|
||||
DaemonRequest::Ping => DaemonResponse::ok(&serde_json::json!({"ok": true})),
|
||||
DaemonRequest::SendMessage { room, body } => {
|
||||
handlers::send_message(client, &room, &body).await
|
||||
}
|
||||
DaemonRequest::SendDm { user_id, body } => handlers::send_dm(client, &user_id, &body).await,
|
||||
DaemonRequest::SendFile {
|
||||
async fn dispatch(req: DaemonRequest, registry: &Registry) -> DaemonResponse {
|
||||
// Ping is account-agnostic — answer without resolving a client so a
|
||||
// health probe works even before any account restores.
|
||||
if matches!(req.op, DaemonOp::Ping) {
|
||||
return DaemonResponse::ok(&serde_json::json!({"ok": true}));
|
||||
}
|
||||
let client = match registry.resolve(req.account.as_deref()) {
|
||||
Ok(c) => c,
|
||||
Err(msg) => return DaemonResponse::error(msg),
|
||||
};
|
||||
dispatch_op(req.op, client).await
|
||||
}
|
||||
|
||||
async fn dispatch_op(op: DaemonOp, client: &Client) -> DaemonResponse {
|
||||
match op {
|
||||
DaemonOp::Ping => DaemonResponse::ok(&serde_json::json!({"ok": true})),
|
||||
DaemonOp::SendMessage { room, body } => handlers::send_message(client, &room, &body).await,
|
||||
DaemonOp::SendDm { user_id, body } => handlers::send_dm(client, &user_id, &body).await,
|
||||
DaemonOp::SendFile {
|
||||
room,
|
||||
path,
|
||||
caption,
|
||||
} => handlers::send_file(client, &room, &path, caption.as_deref()).await,
|
||||
DaemonRequest::OpenDm { user_id } => handlers::open_dm(client, &user_id).await,
|
||||
DaemonRequest::SendReaction {
|
||||
DaemonOp::OpenDm { user_id } => handlers::open_dm(client, &user_id).await,
|
||||
DaemonOp::SendReaction {
|
||||
room,
|
||||
event_id,
|
||||
key,
|
||||
} => handlers::send_reaction(client, &room, &event_id, &key).await,
|
||||
DaemonRequest::SendReply {
|
||||
DaemonOp::SendReply {
|
||||
room,
|
||||
event_id,
|
||||
body,
|
||||
} => handlers::send_reply(client, &room, &event_id, &body).await,
|
||||
DaemonRequest::MarkRead { room, event_id } => {
|
||||
DaemonOp::MarkRead { room, event_id } => {
|
||||
handlers::mark_read(client, &room, &event_id).await
|
||||
}
|
||||
DaemonRequest::ListRooms => handlers::list_rooms(client).await,
|
||||
DaemonRequest::ListInvites => handlers::list_invites(client),
|
||||
DaemonRequest::JoinRoom { room } => handlers::join_room(client, &room).await,
|
||||
DaemonRequest::ResolveInvite { room, action } => {
|
||||
DaemonOp::ListRooms => handlers::list_rooms(client).await,
|
||||
DaemonOp::ListInvites => handlers::list_invites(client),
|
||||
DaemonOp::JoinRoom { room } => handlers::join_room(client, &room).await,
|
||||
DaemonOp::ResolveInvite { room, action } => {
|
||||
handlers::resolve_invite(client, &room, action).await
|
||||
}
|
||||
DaemonRequest::InviteUser { room, user_id } => {
|
||||
DaemonOp::InviteUser { room, user_id } => {
|
||||
handlers::invite_user(client, &room, &user_id).await
|
||||
}
|
||||
DaemonRequest::ListRoomMembers { room } => handlers::list_room_members(client, &room).await,
|
||||
DaemonRequest::ReadRoom { room, limit } => handlers::read_room(client, &room, limit).await,
|
||||
DaemonRequest::DownloadFile {
|
||||
DaemonOp::ListRoomMembers { room } => handlers::list_room_members(client, &room).await,
|
||||
DaemonOp::ReadRoom { room, limit } => handlers::read_room(client, &room, limit).await,
|
||||
DaemonOp::DownloadFile {
|
||||
room,
|
||||
event_id,
|
||||
dest_path,
|
||||
} => handlers::download_file(client, &room, &event_id, dest_path.as_deref()).await,
|
||||
DaemonRequest::UnreadCount => handlers::unread_count(client),
|
||||
DaemonRequest::UnreadSummary => handlers::unread_summary(client).await,
|
||||
DaemonOp::UnreadCount => handlers::unread_count(client),
|
||||
DaemonOp::UnreadSummary => handlers::unread_summary(client).await,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue