Fetch wire: chat_fetch request/receipt documents, Chat::fetch, RudpChat::fetch, Bifrost persona-chat-v7 bytes as persona-chat-v9
First half of cut-persona-save-attachment.r7: the 4-slot request, the 5-slot receipt (nested ChatImage) with its one validity table (Stored with nil receipt allowed), the 14 outcome words, ReceiptMode, and the exchange() fetch beside open_direct including UnknownKind -> MouthRefused. Fixtures copied byte for byte from Bifrost 3a6b45d. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
ecf147dbd7
commit
2ecb450f80
19 changed files with 301 additions and 3 deletions
|
|
@ -39,6 +39,9 @@ pub const SEND_RECEIPT: &str = "gamecult.persona.chat_send_receipt.v2";
|
|||
/// Her request that the mouth open a DM with a person, and its answer.
|
||||
pub const OPEN_DIRECT_REQUEST: &str = "gamecult.persona.chat_open_direct_request.v1";
|
||||
pub const OPEN_DIRECT_RECEIPT: &str = "gamecult.persona.chat_open_direct_receipt.v1";
|
||||
/// Her request that the mouth fetch one event's image, verify it and post the receipt as her, and its answer.
|
||||
pub const FETCH_REQUEST: &str = "gamecult.persona.chat_fetch_request.v1";
|
||||
pub const FETCH_RECEIPT: &str = "gamecult.persona.chat_fetch_receipt.v1";
|
||||
pub const REFUSAL: &str = "gamecult.persona.chat_refusal.v1";
|
||||
|
||||
/// One earlier line of a room, delivered with a message for its context.
|
||||
|
|
@ -688,6 +691,159 @@ impl TryFrom<RawOpenReceipt> for ChatOpenDirectReceipt {
|
|||
}
|
||||
}
|
||||
|
||||
/// A request that the mouth fetch the image of `event_id` in `room` (an alias or room id) for `persona`: the reference is room and
|
||||
/// event only. `request_id` makes a repeat the same fetch.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ChatFetchRequest {
|
||||
pub request_id: String,
|
||||
pub persona: String,
|
||||
pub room: String,
|
||||
pub event_id: String,
|
||||
}
|
||||
|
||||
/// How the mouth answered a fetch, as its fourteen typed names. Whether the file may be fetched is the mouth's, derived from Matrix.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum FetchOutcome {
|
||||
/// Fetched, verified against the event's hash, stored. The receipt, if any, says whether it was posted.
|
||||
Stored,
|
||||
/// Fetched and stored but the event carried no hash: not verified, no receipt, never marked read.
|
||||
StoredUnverified,
|
||||
NotAMember,
|
||||
BridgedRoom,
|
||||
TooLarge,
|
||||
TypeRefused,
|
||||
NoAttachment,
|
||||
HashMismatch,
|
||||
RateLimited,
|
||||
Brake,
|
||||
PersonaMismatch,
|
||||
UnknownPersona,
|
||||
FetchFailed,
|
||||
StoreFailed,
|
||||
}
|
||||
|
||||
impl FetchOutcome {
|
||||
const ALL: [Self; 14] = [
|
||||
Self::Stored,
|
||||
Self::StoredUnverified,
|
||||
Self::NotAMember,
|
||||
Self::BridgedRoom,
|
||||
Self::TooLarge,
|
||||
Self::TypeRefused,
|
||||
Self::NoAttachment,
|
||||
Self::HashMismatch,
|
||||
Self::RateLimited,
|
||||
Self::Brake,
|
||||
Self::PersonaMismatch,
|
||||
Self::UnknownPersona,
|
||||
Self::FetchFailed,
|
||||
Self::StoreFailed,
|
||||
];
|
||||
|
||||
/// The mouth's spelling: a MessagePack string.
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Stored => "Stored",
|
||||
Self::StoredUnverified => "StoredUnverified",
|
||||
Self::NotAMember => "NotAMember",
|
||||
Self::BridgedRoom => "BridgedRoom",
|
||||
Self::TooLarge => "TooLarge",
|
||||
Self::TypeRefused => "TypeRefused",
|
||||
Self::NoAttachment => "NoAttachment",
|
||||
Self::HashMismatch => "HashMismatch",
|
||||
Self::RateLimited => "RateLimited",
|
||||
Self::Brake => "Brake",
|
||||
Self::PersonaMismatch => "PersonaMismatch",
|
||||
Self::UnknownPersona => "UnknownPersona",
|
||||
Self::FetchFailed => "FetchFailed",
|
||||
Self::StoreFailed => "StoreFailed",
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the mouth stored a file for this outcome: the two that carry a room id and an image.
|
||||
pub fn stored(self) -> bool {
|
||||
matches!(self, Self::Stored | Self::StoredUnverified)
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for FetchOutcome {
|
||||
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
serializer.serialize_str(self.name())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for FetchOutcome {
|
||||
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
let name = String::deserialize(deserializer)?;
|
||||
Self::ALL.into_iter().find(|outcome| outcome.name() == name).ok_or_else(|| serde::de::Error::custom("the mouth named a fetch outcome this side does not know"))
|
||||
}
|
||||
}
|
||||
|
||||
/// The receipt the mouth posted as the Persona for a `Stored` fetch, or found already posted.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum ReceiptMode {
|
||||
Posted,
|
||||
Existing,
|
||||
}
|
||||
|
||||
impl ReceiptMode {
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Posted => "Posted",
|
||||
Self::Existing => "Existing",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for ReceiptMode {
|
||||
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
serializer.serialize_str(self.name())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ReceiptMode {
|
||||
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
let name = String::deserialize(deserializer)?;
|
||||
[Self::Posted, Self::Existing].into_iter().find(|mode| mode.name() == name).ok_or_else(|| serde::de::Error::custom("the mouth named a receipt mode this side does not know"))
|
||||
}
|
||||
}
|
||||
|
||||
/// The mouth's answer to a fetch: `[request_id, outcome, room_id, image, receipt]`. `room_id` and `image` are present exactly for
|
||||
/// `Stored` and `StoredUnverified`; `receipt` only with `Stored` (`Stored` with none is valid: nothing was posted, so it is not
|
||||
/// marked read). A receipt that says otherwise does not decode.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(try_from = "RawFetchReceipt")]
|
||||
pub struct ChatFetchReceipt {
|
||||
pub request_id: String,
|
||||
pub outcome: FetchOutcome,
|
||||
pub room_id: Option<String>,
|
||||
pub image: Option<ChatImage>,
|
||||
pub receipt: Option<ReceiptMode>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct RawFetchReceipt {
|
||||
request_id: String,
|
||||
outcome: FetchOutcome,
|
||||
room_id: Option<String>,
|
||||
image: Option<ChatImage>,
|
||||
receipt: Option<ReceiptMode>,
|
||||
}
|
||||
|
||||
impl TryFrom<RawFetchReceipt> for ChatFetchReceipt {
|
||||
type Error = &'static str;
|
||||
|
||||
fn try_from(raw: RawFetchReceipt) -> Result<Self, Self::Error> {
|
||||
if raw.outcome.stored() != (raw.room_id.is_some() && raw.image.is_some()) || (!raw.outcome.stored() && (raw.room_id.is_some() || raw.image.is_some())) {
|
||||
return Err("the fetch receipt's room and image do not match its outcome");
|
||||
}
|
||||
if raw.receipt.is_some() && raw.outcome != FetchOutcome::Stored {
|
||||
return Err("the fetch receipt posts a receipt for an outcome that is not Stored");
|
||||
}
|
||||
Ok(Self { request_id: raw.request_id, outcome: raw.outcome, room_id: raw.room_id, image: raw.image, receipt: raw.receipt })
|
||||
}
|
||||
}
|
||||
|
||||
/// A document as the mouth reads it: a positional tuple.
|
||||
pub fn encode<T: Serialize>(document: &T) -> Result<Vec<u8>> {
|
||||
rmp_serde::to_vec(document).context("the chat document does not encode")
|
||||
|
|
@ -706,6 +862,9 @@ pub trait Chat {
|
|||
fn send(&self, request: &ChatSendRequest) -> Result<ChatSendReceipt>;
|
||||
/// Ask the mouth to open a DM. A refusal is a receipt; only a transport fault or a mouth that does not serve the kind is an error.
|
||||
fn open_direct(&self, request: &ChatOpenDirectRequest) -> Result<ChatOpenDirectReceipt>;
|
||||
/// Ask the mouth to fetch, verify and receipt one event's image. A refusal is a receipt; only a transport fault or a mouth
|
||||
/// that does not serve the kind is an error.
|
||||
fn fetch(&self, request: &ChatFetchRequest) -> Result<ChatFetchReceipt>;
|
||||
/// Every delivery up to and including `through_seq` is answered; the mouth
|
||||
/// advances its sync position only now, so a start that dies first has its
|
||||
/// deliveries again.
|
||||
|
|
|
|||
|
|
@ -185,6 +185,74 @@ fn open_direct_fixtures_round_trip() {
|
|||
golden(UNKNOWN_KIND_BYTES, &ChatRefusal { persona: "eureka".into(), refusal: Refusal::UnknownKind });
|
||||
}
|
||||
|
||||
const FETCH_REQUEST_BYTES: &[u8] = include_bytes!("../../tests/fixtures/persona-chat-v9/gamecult.persona.chat_fetch_request.v1.msgpack");
|
||||
const FETCH_POSTED_BYTES: &[u8] = include_bytes!("../../tests/fixtures/persona-chat-v9/gamecult.persona.chat_fetch_receipt.v1.stored-posted.msgpack");
|
||||
const FETCH_EXISTING_BYTES: &[u8] = include_bytes!("../../tests/fixtures/persona-chat-v9/gamecult.persona.chat_fetch_receipt.v1.stored-existing.msgpack");
|
||||
const FETCH_NIL_BYTES: &[u8] = include_bytes!("../../tests/fixtures/persona-chat-v9/gamecult.persona.chat_fetch_receipt.v1.stored-nil.msgpack");
|
||||
const FETCH_UNVERIFIED_BYTES: &[u8] = include_bytes!("../../tests/fixtures/persona-chat-v9/gamecult.persona.chat_fetch_receipt.v1.stored-unverified.msgpack");
|
||||
const FETCH_REFUSED_BYTES: &[u8] = include_bytes!("../../tests/fixtures/persona-chat-v9/gamecult.persona.chat_fetch_receipt.v1.refused.msgpack");
|
||||
|
||||
fn fetch_image() -> ChatImage {
|
||||
let hash = "ab".repeat(32);
|
||||
ChatImage { path: format!("{}/{hash}.png", "cd".repeat(32)), mime: "image/png".into(), size: 2048, sha256: hash }
|
||||
}
|
||||
|
||||
fn fetch_receipt(outcome: FetchOutcome, stored: bool, receipt: Option<ReceiptMode>) -> ChatFetchReceipt {
|
||||
ChatFetchReceipt { request_id: "req-f1".into(), outcome, room_id: stored.then(|| "!abc:gamecult.org".to_string()), image: stored.then(fetch_image), receipt }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mouth_fixtures_roundtrip() {
|
||||
golden(FETCH_REQUEST_BYTES, &ChatFetchRequest { request_id: "req-f1".into(), persona: "eureka".into(), room: "#swarm:gamecult.org".into(), event_id: "$file1".into() });
|
||||
golden(FETCH_POSTED_BYTES, &fetch_receipt(FetchOutcome::Stored, true, Some(ReceiptMode::Posted)));
|
||||
golden(FETCH_EXISTING_BYTES, &fetch_receipt(FetchOutcome::Stored, true, Some(ReceiptMode::Existing)));
|
||||
golden(FETCH_NIL_BYTES, &fetch_receipt(FetchOutcome::Stored, true, None));
|
||||
golden(FETCH_UNVERIFIED_BYTES, &fetch_receipt(FetchOutcome::StoredUnverified, true, None));
|
||||
golden(FETCH_REFUSED_BYTES, &ChatFetchReceipt { request_id: "req-f1".into(), outcome: FetchOutcome::BridgedRoom, room_id: None, image: None, receipt: None });
|
||||
// The receipt is a 5-slot tuple whose image is its own 4-slot tuple.
|
||||
assert_eq!(FETCH_POSTED_BYTES[0], 0x95);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_receipt_checker_follows_the_one_table() {
|
||||
use FetchOutcome::*;
|
||||
let decodes = |receipt: &ChatFetchReceipt| {
|
||||
let wire = (&receipt.request_id, receipt.outcome, &receipt.room_id, &receipt.image, receipt.receipt);
|
||||
decode::<ChatFetchReceipt>(&encode(&wire).unwrap()).is_ok()
|
||||
};
|
||||
for mode in [None, Some(ReceiptMode::Posted), Some(ReceiptMode::Existing)] {
|
||||
assert!(decodes(&fetch_receipt(Stored, true, mode)), "Stored with {mode:?}");
|
||||
}
|
||||
assert!(decodes(&fetch_receipt(StoredUnverified, true, None)));
|
||||
assert!(!decodes(&fetch_receipt(StoredUnverified, true, Some(ReceiptMode::Posted))), "an unverified fetch posts no receipt");
|
||||
assert!(!decodes(&fetch_receipt(StoredUnverified, true, Some(ReceiptMode::Existing))));
|
||||
for outcome in FetchOutcome::ALL {
|
||||
let stored = outcome.stored();
|
||||
assert!(!decodes(&fetch_receipt(outcome, !stored, None)), "{outcome:?} with the other half's ids");
|
||||
assert!(decodes(&fetch_receipt(outcome, stored, None)), "{outcome:?}");
|
||||
if outcome != Stored {
|
||||
assert!(!decodes(&fetch_receipt(outcome, stored, Some(ReceiptMode::Posted))), "{outcome:?} with a receipt");
|
||||
}
|
||||
let mut half = fetch_receipt(outcome, stored, None);
|
||||
half.room_id = (!stored).then(|| "!r:x".to_string());
|
||||
assert!(!decodes(&half), "{outcome:?} with only a room id");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_fetch_outcome_is_spelled_as_the_mouth_spells_it_and_no_other_decodes() {
|
||||
let words = ["Stored", "StoredUnverified", "NotAMember", "BridgedRoom", "TooLarge", "TypeRefused", "NoAttachment", "HashMismatch", "RateLimited", "Brake", "PersonaMismatch", "UnknownPersona", "FetchFailed", "StoreFailed"];
|
||||
assert_eq!(FetchOutcome::ALL.map(FetchOutcome::name), words);
|
||||
for word in words {
|
||||
assert_eq!(decode::<FetchOutcome>(&encode(&word).unwrap()).unwrap().name(), word);
|
||||
}
|
||||
// The organ-local words are never on the wire.
|
||||
for local in ["NoStore", "Unserved", "Unanswered", "stored", ""] {
|
||||
assert!(decode::<FetchOutcome>(&encode(&local).unwrap()).is_err(), "{local}");
|
||||
}
|
||||
assert!(decode::<ReceiptMode>(&encode(&"Skipped").unwrap()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_outcome_is_spelled_as_the_mouth_spells_it() {
|
||||
let all = [
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ use serde::de::DeserializeOwned;
|
|||
use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::chat_client::{
|
||||
ACK, Chat, ChatAck, ChatDelivery, ChatOpenDirectReceipt, ChatOpenDirectRequest, ChatPending, ChatPendingRequest, ChatRead, ChatRefusal, MouthRefused, Refusal, reply_warnings, ChatSendReceipt, ChatSendRequest, PENDING, PENDING_REQUEST, PENDING_REQUEST_V1, PENDING_REQUEST_V4, PENDING_V2, PENDING_V4, OPEN_DIRECT_RECEIPT, OPEN_DIRECT_REQUEST, PENDING_V1, READ, REFUSAL, SEND_RECEIPT,
|
||||
ACK, Chat, ChatAck, ChatDelivery, ChatFetchReceipt, ChatFetchRequest, ChatOpenDirectReceipt, ChatOpenDirectRequest, ChatPending, ChatPendingRequest, ChatRead, ChatRefusal, MouthRefused, Refusal, reply_warnings, ChatSendReceipt, ChatSendRequest, PENDING, PENDING_REQUEST, PENDING_REQUEST_V1, PENDING_REQUEST_V4, PENDING_V2, PENDING_V4, OPEN_DIRECT_RECEIPT, OPEN_DIRECT_REQUEST, FETCH_RECEIPT, FETCH_REQUEST, PENDING_V1, READ, REFUSAL, SEND_RECEIPT,
|
||||
SEND_REQUEST, decode, encode,
|
||||
};
|
||||
|
||||
|
|
@ -229,6 +229,25 @@ impl Chat for RudpChat {
|
|||
Ok(receipt)
|
||||
}
|
||||
|
||||
fn fetch(&self, request: &ChatFetchRequest) -> Result<ChatFetchReceipt> {
|
||||
// As an open: a mouth that does not serve the kind answers a refusal keyed by the persona, and the fetch is known not sent.
|
||||
let p = self.persona.as_str();
|
||||
let answers = [(FETCH_RECEIPT, request.request_id.as_str()), (REFUSAL, p)];
|
||||
let (kind, payload) = self.exchange(FETCH_REQUEST, &request.request_id, request, Some(&answers))?.ok_or_else(|| anyhow!("no answer payload"))?;
|
||||
if kind == REFUSAL {
|
||||
let refusal: ChatRefusal = decode(&payload)?;
|
||||
if refusal.persona != self.persona || refusal.refusal != Refusal::UnknownKind {
|
||||
bail!("the mouth refused the fetch: {}", refusal.refusal.name());
|
||||
}
|
||||
return Err(MouthRefused(refusal.refusal).into());
|
||||
}
|
||||
let receipt: ChatFetchReceipt = decode(&payload)?;
|
||||
if receipt.request_id != request.request_id {
|
||||
bail!("the mouth answered another fetch");
|
||||
}
|
||||
Ok(receipt)
|
||||
}
|
||||
|
||||
fn ack(&self, through_seq: u64) -> Result<()> {
|
||||
let ack = ChatAck { persona: self.persona.clone(), through_seq };
|
||||
self.exchange(ACK, &format!("{}:{through_seq}", self.persona), &ack, None).map(|_| ())
|
||||
|
|
|
|||
|
|
@ -101,6 +101,10 @@ impl Chat for FakeChat {
|
|||
panic!("the heartbeat opens nothing")
|
||||
}
|
||||
|
||||
fn fetch(&self, _: &crate::chat_client::ChatFetchRequest) -> Result<crate::chat_client::ChatFetchReceipt> {
|
||||
panic!("the heartbeat fetches nothing")
|
||||
}
|
||||
|
||||
fn ack(&self, _: u64) -> Result<()> {
|
||||
panic!("the heartbeat never acks a delivery")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -193,6 +193,10 @@ impl Chat for FakeChat {
|
|||
Ok(ChatOpenDirectReceipt { request_id: request.request_id.clone(), outcome, room_id: landed.then(|| "!dm:chat".to_string()), event_id: landed.then(|| "$open".to_string()) })
|
||||
}
|
||||
|
||||
fn fetch(&self, _: &crate::chat_client::ChatFetchRequest) -> Result<crate::chat_client::ChatFetchReceipt> {
|
||||
panic!("no test asks the fake mouth to fetch yet")
|
||||
}
|
||||
|
||||
fn read(&self, read: &ChatRead) -> Result<()> {
|
||||
if self.fail_read.get() {
|
||||
return Err(anyhow!("the mouth is down"));
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ use std::thread::JoinHandle;
|
|||
use std::time::Duration;
|
||||
|
||||
use cultnet_rs::{CultNetMessage, CultNetRawDocumentRecord, CultNetRawPayloadEncoding, CultNetRudpSocketTransportConnection, CultNetRudpSocketTransportOptions};
|
||||
use mind_body::chat_client::{self, ReplyLink, ReplyViolation, Basis, Form, ChatRefusal, MouthRefused, Refusal, REFUSAL, ACK, Chat, ChatAck, ChatOpenDirectReceipt, ChatOpenDirectRequest, DirectOutcome, OPEN_DIRECT_RECEIPT, OPEN_DIRECT_REQUEST, ChatPending, ChatPendingRequest, ChatRead, ChatSendReceipt, ChatSendRequest, PENDING, PENDING_REQUEST, PENDING_REQUEST_V1, PENDING_REQUEST_V4, PENDING_V1, PENDING_V2, PENDING_V4, READ, SEND_RECEIPT, SEND_REQUEST};
|
||||
use mind_body::chat_client::{self, ReplyLink, ReplyViolation, Basis, Form, ChatRefusal, MouthRefused, Refusal, REFUSAL, ACK, Chat, ChatAck, ChatFetchReceipt, ChatFetchRequest, ChatOpenDirectReceipt, ChatOpenDirectRequest, DirectOutcome, FetchOutcome, FETCH_RECEIPT, FETCH_REQUEST, ReceiptMode, OPEN_DIRECT_RECEIPT, OPEN_DIRECT_REQUEST, ChatPending, ChatPendingRequest, ChatRead, ChatSendReceipt, ChatSendRequest, PENDING, PENDING_REQUEST, PENDING_REQUEST_V1, PENDING_REQUEST_V4, PENDING_V1, PENDING_V2, PENDING_V4, READ, SEND_RECEIPT, SEND_REQUEST};
|
||||
use mind_body::chat_rudp::{RudpChat, connection_id};
|
||||
|
||||
const ACK_BYTES: &[u8] = include_bytes!("fixtures/persona-chat-v1/gamecult.persona.chat_ack.v1.msgpack");
|
||||
|
|
@ -35,6 +35,10 @@ const UNKNOWN_KIND_BYTES: &[u8] = include_bytes!("fixtures/persona-chat-v4/gamec
|
|||
const REFUSAL_BYTES: &[u8] = include_bytes!("fixtures/persona-chat-v1/gamecult.persona.chat_refusal.v1.msgpack");
|
||||
const OPENED_BYTES: &[u8] = include_bytes!("fixtures/persona-chat-v4/gamecult.persona.chat_open_direct_receipt.v1.opened.msgpack");
|
||||
|
||||
// Bifrost 3a6b45d's own golden bytes for the fetch, copied unchanged as `persona-chat-v9/` (`tests/fixtures/persona-chat-v7/` there).
|
||||
const FETCH_REQUEST_BYTES: &[u8] = include_bytes!("fixtures/persona-chat-v9/gamecult.persona.chat_fetch_request.v1.msgpack");
|
||||
const FETCH_POSTED_BYTES: &[u8] = include_bytes!("fixtures/persona-chat-v9/gamecult.persona.chat_fetch_receipt.v1.stored-posted.msgpack");
|
||||
|
||||
const KEY: &str = "connection-key-for-test";
|
||||
const TAG: &str = "cultnet.transport.rudp.v0";
|
||||
|
||||
|
|
@ -128,6 +132,7 @@ fn serve(key: &str, bind: &str, pending_kind: &'static str, pending_bytes: &'sta
|
|||
PENDING_REQUEST_V1 if pending_kind != PENDING && for_eureka(&chat_client::decode::<ChatPendingRequest>(&document.payload).unwrap().persona) => Some(answer(pending_kind, "eureka", pending_bytes)),
|
||||
SEND_REQUEST if for_eureka(&chat_client::decode::<ChatSendRequest>(&document.payload).unwrap().persona) => Some(answer(SEND_RECEIPT, &document.record_key, SEND_RECEIPT_BYTES)),
|
||||
OPEN_DIRECT_REQUEST if for_eureka(&chat_client::decode::<ChatOpenDirectRequest>(&document.payload).unwrap().persona) => Some(answer(open.0, open.1.unwrap_or(&document.record_key), open.2)),
|
||||
FETCH_REQUEST if for_eureka(&chat_client::decode::<ChatFetchRequest>(&document.payload).unwrap().persona) => Some(answer(FETCH_RECEIPT, &document.record_key, FETCH_POSTED_BYTES)),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(reply) = reply {
|
||||
|
|
@ -332,6 +337,21 @@ fn an_open_crosses_a_real_rudp_session_in_the_mouths_bytes_and_a_receipt_for_ano
|
|||
assert_eq!(seen[0], (OPEN_DIRECT_REQUEST.to_string(), OPEN_REQUEST_BYTES.to_vec()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_fetch_crosses_a_real_rudp_session_in_the_mouths_bytes_and_a_receipt_for_another_fetch_is_refused() {
|
||||
let mouth = mouth(KEY);
|
||||
let chat = client(&mouth, KEY, "eureka", Duration::from_secs(5));
|
||||
let request = chat_client::decode::<ChatFetchRequest>(FETCH_REQUEST_BYTES).unwrap();
|
||||
let receipt = chat.fetch(&request).unwrap();
|
||||
assert_eq!(receipt, chat_client::decode::<ChatFetchReceipt>(FETCH_POSTED_BYTES).unwrap());
|
||||
assert_eq!((receipt.outcome, receipt.receipt), (FetchOutcome::Stored, Some(ReceiptMode::Posted)));
|
||||
|
||||
let another = ChatFetchRequest { request_id: "req-other".into(), ..request };
|
||||
assert!(chat.fetch(&another).is_err(), "the answer names req-f1, not req-other");
|
||||
let seen = mouth.seen.lock().unwrap().clone();
|
||||
assert_eq!(seen[0], (FETCH_REQUEST.to_string(), FETCH_REQUEST_BYTES.to_vec()));
|
||||
}
|
||||
|
||||
/// A mouth that answers every open with `reply`.
|
||||
fn opening_with(reply: OpenReply) -> Mouth {
|
||||
serve(KEY, "127.0.0.1:0", PENDING, PENDING_BYTES, reply)
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
95a67265712d6631ab42726964676564526f6f6dc0c0c0
|
||||
|
|
@ -0,0 +1 @@
|
|||
•¦req-f1«BridgedRoomÀÀÀ
|
||||
|
|
@ -0,0 +1 @@
|
|||
95a67265712d6631a653746f726564b1216162633a67616d6563756c742e6f726794d985636463646364636463646364636463646364636463646364636463646364636463646364636463646364636463646364636463646364636463646364636463642f616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261622e706e67a9696d6167652f706e67cd0800d94061626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162a84578697374696e67
|
||||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
|||
95a67265712d6631a653746f726564b1216162633a67616d6563756c742e6f726794d985636463646364636463646364636463646364636463646364636463646364636463646364636463646364636463646364636463646364636463646364636463642f616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261622e706e67a9696d6167652f706e67cd0800d94061626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162c0
|
||||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
|||
95a67265712d6631a653746f726564b1216162633a67616d6563756c742e6f726794d985636463646364636463646364636463646364636463646364636463646364636463646364636463646364636463646364636463646364636463646364636463642f616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261622e706e67a9696d6167652f706e67cd0800d94061626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162a6506f73746564
|
||||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
|||
95a67265712d6631b053746f726564556e7665726966696564b1216162633a67616d6563756c742e6f726794d985636463646364636463646364636463646364636463646364636463646364636463646364636463646364636463646364636463646364636463646364636463642f616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261622e706e67a9696d6167652f706e67cd0800d94061626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162c0
|
||||
Binary file not shown.
1
crates/mind-body/tests/fixtures/persona-chat-v9/gamecult.persona.chat_fetch_request.v1.hex
vendored
Normal file
1
crates/mind-body/tests/fixtures/persona-chat-v9/gamecult.persona.chat_fetch_request.v1.hex
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
94a67265712d6631a6657572656b61b323737761726d3a67616d6563756c742e6f7267a62466696c6531
|
||||
|
|
@ -0,0 +1 @@
|
|||
”¦req-f1¦eureka³#swarm:gamecult.org¦$file1
|
||||
|
|
@ -17,10 +17,14 @@ mind_body::address: struct Name # [derive (Clone , Debug , PartialEq , Eq , serd
|
|||
mind_body::chat_client: # [derive (Clone , Copy , Debug , Default , PartialEq , Eq)] pub enum Form { # [default] Message , Notice , Edit , }
|
||||
mind_body::chat_client: # [derive (Clone , Copy , Debug , PartialEq , Eq)] pub enum Basis { Direct , Tag , Reply , Room , }
|
||||
mind_body::chat_client: # [derive (Clone , Copy , Debug , PartialEq , Eq)] pub enum DirectOutcome { Opened , Existing , Brake , PersonaMismatch , UnknownPersona , EmptyText , ForeignUser , NotAPerson , NoSuchUser , InvitePending , Declined , DailyLimit , SendFailed , }
|
||||
mind_body::chat_client: # [derive (Clone , Copy , Debug , PartialEq , Eq)] pub enum FetchOutcome { Stored , StoredUnverified , NotAMember , BridgedRoom , TooLarge , TypeRefused , NoAttachment , HashMismatch , RateLimited , Brake , PersonaMismatch , UnknownPersona , FetchFailed , StoreFailed , }
|
||||
mind_body::chat_client: # [derive (Clone , Copy , Debug , PartialEq , Eq)] pub enum ReceiptMode { Posted , Existing , }
|
||||
mind_body::chat_client: # [derive (Clone , Copy , Debug , PartialEq , Eq)] pub enum Refusal { Brake , PersonaMismatch , UnknownRoom , EmptyText , SendFailed , UnknownPersona , RoomMoved , UnknownKind , }
|
||||
mind_body::chat_client: # [derive (Clone , Copy , Debug , PartialEq , Eq)] pub enum ReplyViolation { NotATriple , BadEventId , BadSenderId , ExcerptNotText , ExcerptTooLong , }
|
||||
mind_body::chat_client: # [derive (Clone , Debug , Default , PartialEq , Eq)] pub enum ReplyLink { # [default] Absent , Quote (ReplyTo) , Dropped (ReplyViolation) , }
|
||||
mind_body::chat_client: const ACK : & str
|
||||
mind_body::chat_client: const FETCH_RECEIPT : & str
|
||||
mind_body::chat_client: const FETCH_REQUEST : & str
|
||||
mind_body::chat_client: const OPEN_DIRECT_RECEIPT : & str
|
||||
mind_body::chat_client: const OPEN_DIRECT_REQUEST : & str
|
||||
mind_body::chat_client: const PENDING : & str
|
||||
|
|
@ -42,8 +46,13 @@ mind_body::chat_client: impl Basis
|
|||
mind_body::chat_client: impl Basis: fn name (self) -> & 'static str
|
||||
mind_body::chat_client: impl DirectOutcome
|
||||
mind_body::chat_client: impl DirectOutcome: fn name (self) -> & 'static str
|
||||
mind_body::chat_client: impl FetchOutcome
|
||||
mind_body::chat_client: impl FetchOutcome: fn name (self) -> & 'static str
|
||||
mind_body::chat_client: impl FetchOutcome: fn stored (self) -> bool
|
||||
mind_body::chat_client: impl Form
|
||||
mind_body::chat_client: impl Form: fn name (self) -> & 'static str
|
||||
mind_body::chat_client: impl ReceiptMode
|
||||
mind_body::chat_client: impl ReceiptMode: fn name (self) -> & 'static str
|
||||
mind_body::chat_client: impl Refusal
|
||||
mind_body::chat_client: impl Refusal: fn name (self) -> & 'static str
|
||||
mind_body::chat_client: impl ReplyLink
|
||||
|
|
@ -52,20 +61,27 @@ mind_body::chat_client: impl ReplyViolation
|
|||
mind_body::chat_client: impl ReplyViolation: fn name (self) -> & 'static str
|
||||
mind_body::chat_client: impl Serialize for Basis
|
||||
mind_body::chat_client: impl Serialize for DirectOutcome
|
||||
mind_body::chat_client: impl Serialize for FetchOutcome
|
||||
mind_body::chat_client: impl Serialize for Form
|
||||
mind_body::chat_client: impl Serialize for ReceiptMode
|
||||
mind_body::chat_client: impl Serialize for Refusal
|
||||
mind_body::chat_client: impl Serialize for ReplyLink
|
||||
mind_body::chat_client: impl TryFrom < RawFetchReceipt > for ChatFetchReceipt
|
||||
mind_body::chat_client: impl TryFrom < RawOpenReceipt > for ChatOpenDirectReceipt
|
||||
mind_body::chat_client: impl std :: error :: Error for MouthRefused
|
||||
mind_body::chat_client: impl std :: fmt :: Display for MouthRefused
|
||||
mind_body::chat_client: impl< 'de > Deserialize < 'de > for Basis
|
||||
mind_body::chat_client: impl< 'de > Deserialize < 'de > for DirectOutcome
|
||||
mind_body::chat_client: impl< 'de > Deserialize < 'de > for FetchOutcome
|
||||
mind_body::chat_client: impl< 'de > Deserialize < 'de > for Form
|
||||
mind_body::chat_client: impl< 'de > Deserialize < 'de > for ReceiptMode
|
||||
mind_body::chat_client: impl< 'de > Deserialize < 'de > for Refusal
|
||||
mind_body::chat_client: impl< 'de > Deserialize < 'de > for ReplyLink
|
||||
mind_body::chat_client: pub trait Chat { fn pending (& self , after_seq : u64) -> Result < ChatPending > ; fn send (& self , request : & ChatSendRequest) -> Result < ChatSendReceipt > ; fn open_direct (& self , request : & ChatOpenDirectRequest) -> Result < ChatOpenDirectReceipt > ; fn ack (& self , through_seq : u64) -> Result < () > ; fn read (& self , read : & ChatRead) -> Result < () > ; }
|
||||
mind_body::chat_client: pub trait Chat { fn pending (& self , after_seq : u64) -> Result < ChatPending > ; fn send (& self , request : & ChatSendRequest) -> Result < ChatSendReceipt > ; fn open_direct (& self , request : & ChatOpenDirectRequest) -> Result < ChatOpenDirectReceipt > ; fn fetch (& self , request : & ChatFetchRequest) -> Result < ChatFetchReceipt > ; fn ack (& self , through_seq : u64) -> Result < () > ; fn read (& self , read : & ChatRead) -> Result < () > ; }
|
||||
mind_body::chat_client: struct ChatAck # [derive (Clone , Debug , PartialEq , Eq , Serialize , Deserialize)] pub-fields[persona: String, through_seq: u64]
|
||||
mind_body::chat_client: struct ChatDelivery # [derive (Clone , Debug , PartialEq , Eq , Serialize , Deserialize)] pub-fields[seq: u64, room_alias: String, room_id: String, event_id: String, sender: String, sender_display: String, text: String, at_unix_ms: u64, context: Vec < ContextLine >, mentions: Vec < String >, is_direct: bool, image: Option < ChatImage >, basis: Option < Basis >, form: Form, reply_to: ReplyLink]
|
||||
mind_body::chat_client: struct ChatFetchReceipt # [derive (Clone , Debug , PartialEq , Eq , Serialize , Deserialize)] pub-fields[request_id: String, outcome: FetchOutcome, room_id: Option < String >, image: Option < ChatImage >, receipt: Option < ReceiptMode >]
|
||||
mind_body::chat_client: struct ChatFetchRequest # [derive (Clone , Debug , PartialEq , Eq , Serialize , Deserialize)] pub-fields[request_id: String, persona: String, room: String, event_id: String]
|
||||
mind_body::chat_client: struct ChatImage # [derive (Clone , Debug , PartialEq , Eq , Serialize , Deserialize)] pub-fields[path: String, mime: String, size: u64, sha256: String]
|
||||
mind_body::chat_client: struct ChatOpenDirectReceipt # [derive (Clone , Debug , PartialEq , Eq , Serialize , Deserialize)] pub-fields[request_id: String, outcome: DirectOutcome, room_id: Option < String >, event_id: Option < String >]
|
||||
mind_body::chat_client: struct ChatOpenDirectRequest # [derive (Clone , Debug , PartialEq , Eq , Serialize , Deserialize)] pub-fields[request_id: String, persona: String, user_id: String, text: String]
|
||||
|
|
|
|||
Loading…
Reference in a new issue