Simplify rich text on-wire format

This commit is contained in:
oxalica 2024-08-30 13:04:57 -04:00
parent c492bb2537
commit 370722731b
5 changed files with 49 additions and 73 deletions

59
Cargo.lock generated
View file

@ -107,7 +107,7 @@ checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.76",
"syn",
]
[[package]]
@ -240,8 +240,7 @@ dependencies = [
"serde-aux",
"serde-constant",
"serde_json",
"serde_tuple",
"syn 2.0.76",
"syn",
"tokio",
"tokio-stream",
"tower-http",
@ -352,7 +351,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.76",
"syn",
]
[[package]]
@ -432,7 +431,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.76",
"syn",
]
[[package]]
@ -585,7 +584,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.76",
"syn",
]
[[package]]
@ -1051,7 +1050,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.76",
"syn",
]
[[package]]
@ -1110,7 +1109,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.76",
"syn",
]
[[package]]
@ -1432,7 +1431,7 @@ checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.76",
"syn",
]
[[package]]
@ -1457,27 +1456,6 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_tuple"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4f025b91216f15a2a32aa39669329a475733590a015835d1783549a56d09427"
dependencies = [
"serde",
"serde_tuple_macros",
]
[[package]]
name = "serde_tuple_macros"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4076151d1a2b688e25aaf236997933c66e18b870d0369f8b248b8ab2be630d7e"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
@ -1579,17 +1557,6 @@ version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.76"
@ -1699,7 +1666,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.76",
"syn",
]
[[package]]
@ -1812,7 +1779,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.76",
"syn",
]
[[package]]
@ -1977,7 +1944,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.76",
"syn",
"wasm-bindgen-shared",
]
@ -2011,7 +1978,7 @@ checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.76",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@ -2193,7 +2160,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.76",
"syn",
]
[[package]]

View file

@ -22,7 +22,6 @@ serde = { version = "1.0.209", features = ["derive"] }
serde-aux = "4.5.0"
serde-constant = "0.1.0"
serde_json = "1.0.127"
serde_tuple = "0.5.0"
tokio = { version = "1.39.3", features = ["macros", "rt-multi-thread", "sync"] }
tokio-stream = { version = "0.1.15", features = ["sync"] }
tower-http = { version = "0.5.2", features = ["cors", "limit"] }

View file

@ -83,7 +83,7 @@ paths:
payload:
typ: chat
room: 7ed9e067-ec37-4054-9fc2-b1bd890929bd
rich_text: [["before "],["bold ",{"b":true}],["italic bold ",{"b":true,"i":true}],["end"]]
rich_text: ["before ",["bold ",{"b":true}],["italic bold ",{"b":true,"i":true}],"end"]
timestamp: 1724966284
user: 83ce46ced47ec0391c64846cbb6c507250ead4985b6a044d68751edc46015dd7
responses:

View file

@ -122,8 +122,8 @@ async function showChatMsg(chat) {
function richTextToHtml(richText) {
let ret = ''
for (let [text, attrs] of richText) {
if (attrs === undefined) attrs = {};
for (let e of richText) {
const [text, attrs] = typeof e === 'string' ? [e, {}] : e;
// Incomplete cases.
const tags = [
[attrs.b, 'b'],
@ -241,7 +241,7 @@ async function postChat(text) {
if (text.startsWith('[')) {
richText = JSON.parse(text);
} else {
richText = [[text]];
richText = [text];
}
const signedPayload = await signData({
typ: 'chat',

View file

@ -12,8 +12,7 @@ use ed25519_dalek::{
Signature, Signer, SigningKey, VerifyingKey, PUBLIC_KEY_LENGTH, SIGNATURE_LENGTH,
};
use rand_core::RngCore;
use serde::{de, Deserialize, Deserializer, Serialize};
use serde_tuple::{Deserialize_tuple, Serialize_tuple};
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use uuid::Uuid;
const TIMESTAMP_TOLERENCE: u64 = 90;
@ -93,22 +92,31 @@ pub struct ChatPayload {
#[serde(transparent)]
pub struct RichText(pub Vec<RichTextPiece>);
// NB. This field is excluded from field order check, because it has tuple representation.
#[derive(Debug, PartialEq, Eq, Serialize_tuple)]
#[derive(Debug, PartialEq, Eq)]
pub struct RichTextPiece {
pub text: String,
#[serde(skip_serializing_if = "is_default::<TextAttrs>")]
pub attrs: TextAttrs,
pub text: String,
}
/// The protocol representation of `RichTextPiece` which keeps nullity of `attrs` for
/// canonicalization check.
// NB. This field is excluded from field order check, because it has tuple representation.
#[derive(Debug, Deserialize_tuple)]
struct RichTextPieceRaw {
pub text: String,
#[serde(default, skip_serializing_if = "is_default::<TextAttrs>")]
pub attrs: Option<TextAttrs>,
impl Serialize for RichTextPiece {
fn serialize<S>(&self, ser: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if is_default(&self.attrs) {
self.text.serialize(ser)
} else {
(&self.text, &self.attrs).serialize(ser)
}
}
}
/// The protocol representation of `RichTextPiece`.
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
enum RichTextPieceRaw {
Text(String),
TextWithAttrs(String, TextAttrs),
}
fn is_default<T: Default + PartialEq>(v: &T) -> bool {
@ -123,16 +131,19 @@ impl<'de> Deserialize<'de> for RichText {
let pieces = <Vec<RichTextPieceRaw>>::deserialize(de)?;
if pieces
.iter()
.any(|p| matches!(&p.attrs, Some(attrs) if is_default(attrs)))
.any(|p| matches!(&p, RichTextPieceRaw::TextWithAttrs(_, attrs) if is_default(attrs)))
{
return Err(de::Error::custom("not in canonical form"));
}
let this = Self(
pieces
.into_iter()
.map(|RichTextPieceRaw { text, attrs }| RichTextPiece {
text,
attrs: attrs.unwrap_or_default(),
.map(|raw| {
let (text, attrs) = match raw {
RichTextPieceRaw::Text(text) => (text, TextAttrs::default()),
RichTextPieceRaw::TextWithAttrs(text, attrs) => (text, attrs),
};
RichTextPiece { text, attrs }
})
.collect(),
);
@ -144,7 +155,7 @@ impl<'de> Deserialize<'de> for RichText {
}
// TODO: This protocol format is quite large. Could use bitflags for database.
#[derive(Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct TextAttrs {
#[serde(default, rename = "b", skip_serializing_if = "is_default")]
pub bold: bool,
@ -286,7 +297,7 @@ pub struct RoomMemberList(pub Vec<RoomMember>);
impl Serialize for RoomMemberList {
fn serialize<S>(&self, ser: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
S: Serializer,
{
self.0.serialize(ser)
}
@ -465,8 +476,7 @@ mod tests {
#[test]
fn rich_text_serde() {
let raw =
r#"[["before "],["bold ",{"b":true}],["italic bold ",{"b":true,"i":true}],["end"]]"#;
let raw = r#"["before ",["bold ",{"b":true}],["italic bold ",{"b":true,"i":true}],"end"]"#;
let text = serde_json::from_str::<RichText>(raw).unwrap();
assert!(text.is_canonical());
assert_eq!(