use super::framing::INVALID_PARAMS;
use super::negotiate::Refusal;
use super::server::Server;
use super::tools::ToolResult;
use crate::network::Rail;
use serde_json::{Map, Value, json};
use std::io::Read as _;
use zeroize::Zeroize as _;
pub(super) fn entry() -> Value {
json!({"name":"send","description":"Publish UTF-8 text once on this seat's grant and return the server's id or exact refusal.",
"inputSchema":{"type":"object","required":["subject","text"],"properties":{
"subject":{"type":"string"},"text":{"type":"string"},"key":{"type":"string","minLength":1}},"additionalProperties":false}})
}
pub(super) fn send<R: Rail>(
server: &Server<R>,
arguments: &Map<String, Value>,
) -> Result<ToolResult, Refusal> {
if arguments
.keys()
.any(|key| !matches!(key.as_str(), "subject" | "text" | "key"))
{
return Err(invalid("send takes only subject, text and an optional key"));
}
let subject = super::tools::string(arguments, "subject")?;
let text = super::tools::string(arguments, "text")?;
let data = json!({"subject":subject});
if let Err(words) = server.listen_config.subject(subject) {
return Ok(ToolResult::failed(&words, data));
}
if text.len() > server.listen_config.content_bytes {
return Ok(ToolResult::failed(
&format!(
"text is {} bytes; the bound is {}",
text.len(),
server.listen_config.content_bytes
),
data,
));
}
let Some(source) = &server.channel_source else {
return Ok(ToolResult::failed(
"the ticket has no seat configuration for channel tools",
data,
));
};
let key = match arguments.get("key") {
Some(Value::String(key))
if !key.is_empty() && key.len() <= server.listen_config.key_bytes =>
{
key.clone()
}
Some(_) => {
return Err(invalid(&format!(
"key must be a nonempty string of at most {} bytes",
server.listen_config.key_bytes
)));
}
None => match fresh_key(server.listen_config.key_bytes) {
Ok(key) => key,
Err(words) => return Ok(ToolResult::failed(&words, data)),
},
};
Ok(
match super::publish_wire::publish(source, subject, text, &key) {
Ok(id) => ToolResult::ok(json!({"subject":subject,"id":id})),
Err(words) => ToolResult::failed(&words, data),
},
)
}
fn fresh_key(bound: usize) -> Result<String, String> {
if bound < 64 {
return Err(format!("a fresh key is 64 bytes; key_bytes is {bound}"));
}
let mut entropy = [0; 32];
let result =
std::fs::File::open("/dev/urandom").and_then(|mut source| source.read_exact(&mut entropy));
let key = result
.map(|()| hex::encode(entropy))
.map_err(|error| format!("send key entropy failed: {}", error.kind()));
entropy.zeroize();
key
}
fn invalid(words: &str) -> Refusal {
Refusal {
code: INVALID_PARAMS,
message: words.to_owned(),
data: None,
}
}