gatewayd: serve, posting approvals, marking them, and the listener
Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
//! `brokerd`'s side of one `gateway.sock` connection, for tests: ask, read what comes back,
|
||||
//! answer with an outcome, or close. Do not edit.
|
||||
|
||||
#![allow(dead_code)] // each test file uses a different part of this module
|
||||
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
|
||||
use proto::{
|
||||
ApprovalOutcome, ApprovalReply, ApprovalRequest, CallId, DataClass, Envelope, GatewayOutcome,
|
||||
Message, PROTOCOL_VERSION, PendingApproval, SessionId, Timestamp, read_frame, write_frame,
|
||||
};
|
||||
|
||||
pub struct Asking {
|
||||
stream: UnixStream,
|
||||
pub posts: Vec<String>,
|
||||
}
|
||||
|
||||
/// An approval of `session`, with these arguments.
|
||||
pub fn pending(approval: u64, session: &str, arguments: &str) -> PendingApproval {
|
||||
PendingApproval {
|
||||
approval,
|
||||
session: SessionId::new(session).unwrap(),
|
||||
call: CallId(3),
|
||||
tool: "read_file".to_string(),
|
||||
arguments: arguments.to_string(),
|
||||
grant: "notes".to_string(),
|
||||
taint: DataClass::Private,
|
||||
created: Timestamp::parse("2026-09-24T08:05:00.000Z").unwrap(),
|
||||
expires: Timestamp::parse("2026-09-24T08:20:00.000Z").unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
fn frame(id: u64, last: bool, msg: Message) -> Envelope {
|
||||
Envelope {
|
||||
v: PROTOCOL_VERSION,
|
||||
id,
|
||||
r#final: last,
|
||||
msg,
|
||||
}
|
||||
}
|
||||
|
||||
/// Connect and ask; `Ok` with the posts, or `Err` with gatewayd's error detail.
|
||||
pub fn ask(socket: &Path, info: PendingApproval) -> Result<Asking, String> {
|
||||
let mut stream = UnixStream::connect(socket).map_err(|e| e.to_string())?;
|
||||
stream
|
||||
.set_read_timeout(Some(Duration::from_secs(10)))
|
||||
.unwrap();
|
||||
write_frame(
|
||||
&mut stream,
|
||||
&frame(
|
||||
1,
|
||||
true,
|
||||
Message::ApprovalRequest(ApprovalRequest { approval: info }),
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
match read_frame(&mut stream).map_err(|e| e.to_string())?.msg {
|
||||
Message::ApprovalPosted(p) => Ok(Asking {
|
||||
stream,
|
||||
posts: p.posts,
|
||||
}),
|
||||
Message::Error(e) => Err(e.detail),
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
impl Asking {
|
||||
/// The answer, within `wait`, or `None`.
|
||||
pub fn answer(&mut self, wait: Duration) -> Option<ApprovalReply> {
|
||||
self.stream.set_read_timeout(Some(wait)).unwrap();
|
||||
match read_frame(&mut self.stream) {
|
||||
Ok(Envelope {
|
||||
msg: Message::ApprovalReply(r),
|
||||
..
|
||||
}) => Some(r),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn outcome(&mut self, outcome: GatewayOutcome) {
|
||||
write_frame(
|
||||
&mut self.stream,
|
||||
&frame(
|
||||
2,
|
||||
true,
|
||||
Message::ApprovalOutcome(ApprovalOutcome { outcome }),
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn close(self) {
|
||||
let _ = self.stream.shutdown(std::net::Shutdown::Both);
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,10 @@ struct Inner {
|
||||
posts: Vec<(String, String, String)>,
|
||||
/// Every REST call: method and path.
|
||||
calls: Vec<(String, String)>,
|
||||
/// Every edit: the post and its new text.
|
||||
edits: Vec<(String, String)>,
|
||||
/// When set, `POST /posts` is refused with 400.
|
||||
refuse_posts: bool,
|
||||
}
|
||||
|
||||
pub struct FakeMm {
|
||||
@@ -223,6 +227,28 @@ impl FakeMm {
|
||||
pub fn calls(&self) -> Vec<(String, String)> {
|
||||
self.inner.lock().unwrap().calls.clone()
|
||||
}
|
||||
|
||||
/// The id of the n-th post made (from 0).
|
||||
pub fn post_id(n: usize) -> String {
|
||||
id('x', u32::try_from(n).unwrap())
|
||||
}
|
||||
|
||||
pub fn edits(&self) -> Vec<(String, String)> {
|
||||
self.inner.lock().unwrap().edits.clone()
|
||||
}
|
||||
|
||||
/// Wait until at least `n` edits were made, for at most `wait`.
|
||||
pub fn wait_edits(&self, n: usize, wait: Duration) -> Vec<(String, String)> {
|
||||
let until = Instant::now() + wait;
|
||||
while self.edits().len() < n && Instant::now() < until {
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
self.edits()
|
||||
}
|
||||
|
||||
pub fn refuse_posts(&self, refuse: bool) {
|
||||
self.inner.lock().unwrap().refuse_posts = refuse;
|
||||
}
|
||||
}
|
||||
|
||||
fn connection(mut stream: TcpStream, inner: &Mutex<Inner>, ws_tx: &mpsc::Sender<WsPeer>) {
|
||||
@@ -298,6 +324,9 @@ fn rest(inner: &Mutex<Inner>, method: &str, path: &str, body: &[u8]) -> (u16, Va
|
||||
};
|
||||
(201, json!({"id": channel, "type": "D"}))
|
||||
}
|
||||
("POST", "/api/v4/posts") if inner.refuse_posts => {
|
||||
(400, json!({"id": "api.post.create_post.refused"}))
|
||||
}
|
||||
("POST", "/api/v4/posts") => {
|
||||
let p: Value = serde_json::from_slice(body).unwrap();
|
||||
let n = u32::try_from(inner.posts.len()).unwrap();
|
||||
@@ -326,6 +355,27 @@ fn rest(inner: &Mutex<Inner>, method: &str, path: &str, body: &[u8]) -> (u16, Va
|
||||
.unwrap_or(json!({"order": [], "posts": {}})),
|
||||
)
|
||||
}
|
||||
("PUT", p) if p.starts_with("/api/v4/posts/") && p.ends_with("/patch") => {
|
||||
let post_id = p
|
||||
.trim_start_matches("/api/v4/posts/")
|
||||
.trim_end_matches("/patch")
|
||||
.to_string();
|
||||
let message = serde_json::from_slice::<Value>(body).unwrap()["message"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
inner.edits.push((post_id.clone(), message.clone()));
|
||||
(200, post(&post_id, BOT, DM, "", &message, 1))
|
||||
}
|
||||
("GET", p) if p.starts_with("/api/v4/users/") => {
|
||||
let user = p.trim_start_matches("/api/v4/users/");
|
||||
let name = if user == KYLE {
|
||||
"kyle".to_string()
|
||||
} else {
|
||||
format!("user-{}", &user[..4])
|
||||
};
|
||||
(200, json!({"id": user, "username": name}))
|
||||
}
|
||||
_ => (404, json!({"message": "not found"})),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user