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:
Eureka 2026-11-12 00:50:27 +00:00
commit 2ecb450f80
19 changed files with 301 additions and 3 deletions

View file

@ -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.

View file

@ -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 = [

View file

@ -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(|_| ())

View file

@ -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")
}

View file

@ -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"));

View file

@ -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)

View file

@ -0,0 +1 @@
95a67265712d6631ab42726964676564526f6f6dc0c0c0

View file

@ -0,0 +1 @@
•¦req-f1«BridgedRoomÀÀÀ

View file

@ -0,0 +1 @@
95a67265712d6631a653746f726564b1216162633a67616d6563756c742e6f726794d985636463646364636463646364636463646364636463646364636463646364636463646364636463646364636463646364636463646364636463646364636463642f616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261622e706e67a9696d6167652f706e67cd0800d94061626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162a84578697374696e67

View file

@ -0,0 +1 @@
95a67265712d6631a653746f726564b1216162633a67616d6563756c742e6f726794d985636463646364636463646364636463646364636463646364636463646364636463646364636463646364636463646364636463646364636463646364636463642f616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261622e706e67a9696d6167652f706e67cd0800d94061626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162c0

View file

@ -0,0 +1 @@
95a67265712d6631a653746f726564b1216162633a67616d6563756c742e6f726794d985636463646364636463646364636463646364636463646364636463646364636463646364636463646364636463646364636463646364636463646364636463642f616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261622e706e67a9696d6167652f706e67cd0800d94061626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162a6506f73746564

View file

@ -0,0 +1 @@
95a67265712d6631b053746f726564556e7665726966696564b1216162633a67616d6563756c742e6f726794d985636463646364636463646364636463646364636463646364636463646364636463646364636463646364636463646364636463646364636463646364636463642f616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261622e706e67a9696d6167652f706e67cd0800d94061626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162616261626162c0

View file

@ -0,0 +1 @@
94a67265712d6631a6657572656b61b323737761726d3a67616d6563756c742e6f7267a62466696c6531

View file

@ -0,0 +1 @@
”¦req-f1¦eureka³#swarm:gamecult.org¦$file1

View file

@ -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]