feat(#1681): add matrix send_file + send_file_dm tools
This commit is contained in:
parent
1b0f035a05
commit
cbbd6f3d6e
6 changed files with 218 additions and 20 deletions
|
|
@ -184,25 +184,33 @@ pub async fn send_message(client: &Client, room_ref: &str, body: &str) -> Daemon
|
|||
}
|
||||
}
|
||||
|
||||
/// Find the existing DM room with `user_id` or create one. Shared by
|
||||
/// `send_dm` and `send_file_dm`. `direct_targets()` (cached state) is
|
||||
/// used rather than the async `is_direct()`.
|
||||
async fn resolve_or_create_dm(
|
||||
client: &Client,
|
||||
user_id: &str,
|
||||
) -> Result<matrix_sdk::Room, DaemonResponse> {
|
||||
let uid: OwnedUserId = user_id
|
||||
.parse()
|
||||
.map_err(|e| DaemonResponse::error(format!("invalid user_id {user_id}: {e}")))?;
|
||||
if let Some(room) = client
|
||||
.joined_rooms()
|
||||
.into_iter()
|
||||
.find(|r| r.direct_targets().iter().any(|t| t.as_str() == uid.as_str()))
|
||||
{
|
||||
return Ok(room);
|
||||
}
|
||||
client
|
||||
.create_dm(&uid)
|
||||
.await
|
||||
.map_err(|e| DaemonResponse::error(format!("create_dm {uid}: {e}")))
|
||||
}
|
||||
|
||||
pub async fn send_dm(client: &Client, user_id: &str, body: &str) -> DaemonResponse {
|
||||
let uid: OwnedUserId = match user_id.parse() {
|
||||
Ok(u) => u,
|
||||
Err(e) => return DaemonResponse::error(format!("invalid user_id {user_id}: {e}")),
|
||||
};
|
||||
// Find existing DM or create one.
|
||||
let room = client.joined_rooms().into_iter().find(|r| {
|
||||
// is_direct() is async; check direct_targets() instead which
|
||||
// reads from cached state.
|
||||
r.direct_targets()
|
||||
.iter()
|
||||
.any(|t| t.as_str() == uid.as_str())
|
||||
});
|
||||
let room = match room {
|
||||
Some(r) => r,
|
||||
None => match client.create_dm(&uid).await {
|
||||
Ok(r) => r,
|
||||
Err(e) => return DaemonResponse::error(format!("create_dm {uid}: {e}")),
|
||||
},
|
||||
let room = match resolve_or_create_dm(client, user_id).await {
|
||||
Ok(r) => r,
|
||||
Err(e) => return e,
|
||||
};
|
||||
if let Some(reject) = unread_guard(&room) {
|
||||
return reject;
|
||||
|
|
@ -212,12 +220,106 @@ pub async fn send_dm(client: &Client, user_id: &str, body: &str) -> DaemonRespon
|
|||
Ok(resp) => DaemonResponse::ok(&serde_json::json!({
|
||||
"event_id": resp.event_id.to_string(),
|
||||
"room_id": room.room_id().to_string(),
|
||||
"user_id": uid.to_string(),
|
||||
"user_id": user_id,
|
||||
})),
|
||||
Err(e) => DaemonResponse::error(format!("send DM to {uid}: {e}")),
|
||||
Err(e) => DaemonResponse::error(format!("send DM to {user_id}: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Maximum file size accepted by `send_file` / `send_file_dm`.
|
||||
const MAX_UPLOAD_BYTES: u64 = 50 * 1024 * 1024;
|
||||
|
||||
/// Read a local file and post it as a matrix attachment to `room`,
|
||||
/// inferring the MIME type from the file extension. An optional
|
||||
/// `caption` is sent as a best-effort follow-up text message (the
|
||||
/// attachment has already landed, so a caption failure is non-fatal).
|
||||
/// Shared by `send_file` and `send_file_dm`.
|
||||
async fn upload_attachment(
|
||||
room: &matrix_sdk::Room,
|
||||
path: &str,
|
||||
caption: Option<&str>,
|
||||
) -> DaemonResponse {
|
||||
let p = std::path::Path::new(path);
|
||||
let meta = match tokio::fs::metadata(p).await {
|
||||
Ok(m) => m,
|
||||
Err(e) => return DaemonResponse::error(format!("stat {path}: {e}")),
|
||||
};
|
||||
if !meta.is_file() {
|
||||
return DaemonResponse::error(format!("{path} is not a regular file"));
|
||||
}
|
||||
if meta.len() > MAX_UPLOAD_BYTES {
|
||||
return DaemonResponse::error(format!(
|
||||
"{path} is {} bytes, over the {MAX_UPLOAD_BYTES}-byte upload cap",
|
||||
meta.len()
|
||||
));
|
||||
}
|
||||
let data = match tokio::fs::read(p).await {
|
||||
Ok(d) => d,
|
||||
Err(e) => return DaemonResponse::error(format!("read {path}: {e}")),
|
||||
};
|
||||
let filename = p
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or("file")
|
||||
.to_owned();
|
||||
let content_type: mime::Mime = mime_guess::from_path(p).first_or_octet_stream();
|
||||
let resp = match room
|
||||
.send_attachment(
|
||||
filename.clone(),
|
||||
&content_type,
|
||||
data,
|
||||
matrix_sdk::attachment::AttachmentConfig::new(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return DaemonResponse::error(format!("send attachment to {}: {e}", room.room_id()));
|
||||
}
|
||||
};
|
||||
if let Some(c) = caption.filter(|c| !c.is_empty()) {
|
||||
let _ = room.send(RoomMessageEventContent::text_markdown(c)).await;
|
||||
}
|
||||
DaemonResponse::ok(&serde_json::json!({
|
||||
"event_id": resp.event_id.to_string(),
|
||||
"room_id": room.room_id().to_string(),
|
||||
"filename": filename,
|
||||
"mime": content_type.essence_str(),
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn send_file(
|
||||
client: &Client,
|
||||
room_ref: &str,
|
||||
path: &str,
|
||||
caption: Option<&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;
|
||||
}
|
||||
upload_attachment(&room, path, caption).await
|
||||
}
|
||||
|
||||
pub async fn send_file_dm(
|
||||
client: &Client,
|
||||
user_id: &str,
|
||||
path: &str,
|
||||
caption: Option<&str>,
|
||||
) -> DaemonResponse {
|
||||
let room = match resolve_or_create_dm(client, user_id).await {
|
||||
Ok(r) => r,
|
||||
Err(e) => return e,
|
||||
};
|
||||
if let Some(reject) = unread_guard(&room) {
|
||||
return reject;
|
||||
}
|
||||
upload_attachment(&room, path, caption).await
|
||||
}
|
||||
|
||||
pub async fn send_reaction(
|
||||
client: &Client,
|
||||
room_ref: &str,
|
||||
|
|
|
|||
Loading…
Reference in a new issue