From 8551540798aee72ea8d755eeb476e9d0e3d44deb Mon Sep 17 00:00:00 2001 From: oxalica Date: Wed, 25 Sep 2024 11:15:31 -0400 Subject: [PATCH] refactor(types)!: blow up big lib.rs into submods --- blah-types/benches/crypto_ops.rs | 5 +- blah-types/src/crypto.rs | 152 +++++++ blah-types/src/identity.rs | 4 +- blah-types/src/lib.rs | 723 +------------------------------ blah-types/src/msg.rs | 544 +++++++++++++++++++++++ blah-types/src/server.rs | 39 ++ blahctl/src/main.rs | 3 +- blahd/src/database.rs | 12 +- blahd/src/event.rs | 3 +- blahd/src/feed.rs | 5 +- blahd/src/lib.rs | 15 +- blahd/src/middleware.rs | 3 +- blahd/src/register.rs | 4 +- blahd/tests/webapi.rs | 11 +- 14 files changed, 778 insertions(+), 745 deletions(-) create mode 100644 blah-types/src/crypto.rs create mode 100644 blah-types/src/msg.rs create mode 100644 blah-types/src/server.rs diff --git a/blah-types/benches/crypto_ops.rs b/blah-types/benches/crypto_ops.rs index bfb8997..de17d29 100644 --- a/blah-types/benches/crypto_ops.rs +++ b/blah-types/benches/crypto_ops.rs @@ -2,9 +2,8 @@ use std::hint::black_box; use std::time::Instant; -use blah_types::{ - get_timestamp, ChatPayload, Id, PubKey, SignExt, Signee, UserKey, UserRegisterPayload, -}; +use blah_types::msg::{ChatPayload, UserRegisterPayload}; +use blah_types::{get_timestamp, Id, PubKey, SignExt, Signee, UserKey}; use criterion::{criterion_group, criterion_main, Criterion}; use ed25519_dalek::SigningKey; use rand::rngs::mock::StepRng; diff --git a/blah-types/src/crypto.rs b/blah-types/src/crypto.rs new file mode 100644 index 0000000..60934bb --- /dev/null +++ b/blah-types/src/crypto.rs @@ -0,0 +1,152 @@ +//! Cryptographic operations and types for user signatures. + +use std::fmt; +use std::str::FromStr; +use std::time::SystemTime; + +use ed25519_dalek::{ + Signature, SignatureError, Signer, SigningKey, VerifyingKey, PUBLIC_KEY_LENGTH, + SIGNATURE_LENGTH, +}; +use rand::RngCore; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct UserKey { + pub id_key: PubKey, + pub act_key: PubKey, +} + +#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(transparent)] +pub struct PubKey(#[serde(with = "hex::serde")] pub [u8; PUBLIC_KEY_LENGTH]); + +impl FromStr for PubKey { + type Err = hex::FromHexError; + + fn from_str(s: &str) -> Result { + hex::FromHex::from_hex(s).map(Self) + } +} + +impl fmt::Debug for PubKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("PubKey").field(&self.to_string()).finish() + } +} + +impl fmt::Display for PubKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut buf = [0u8; PUBLIC_KEY_LENGTH * 2]; + hex::encode_to_slice(self.0, &mut buf).expect("buf size is correct"); + f.write_str(std::str::from_utf8(&buf).expect("hex must be UTF-8")) + } +} + +impl From for PubKey { + fn from(vk: VerifyingKey) -> Self { + Self(vk.to_bytes()) + } +} + +impl From<&VerifyingKey> for PubKey { + fn from(vk: &VerifyingKey) -> Self { + Self(vk.to_bytes()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Signed { + #[serde(with = "hex::serde")] + pub sig: [u8; SIGNATURE_LENGTH], + pub signee: Signee, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Signee { + pub nonce: u32, + pub payload: T, + pub timestamp: u64, + #[serde(flatten)] + pub user: UserKey, +} + +pub trait SignExt: Sized { + fn sign_msg_with( + self, + id_key: &PubKey, + act_key: &SigningKey, + timestamp: u64, + rng: &mut (impl RngCore + ?Sized), + ) -> Result, SignatureError>; + + fn sign_msg( + self, + id_key: &PubKey, + act_key: &SigningKey, + ) -> Result, SignatureError> { + self.sign_msg_with(id_key, act_key, get_timestamp(), &mut rand::thread_rng()) + } +} + +impl SignExt for T { + fn sign_msg_with( + self, + id_key: &PubKey, + act_key: &SigningKey, + timestamp: u64, + rng: &mut (impl RngCore + ?Sized), + ) -> Result, SignatureError> { + Signed::new(id_key, act_key, timestamp, rng, self) + } +} + +pub fn get_timestamp() -> u64 { + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("after UNIX epoch") + .as_secs() +} + +impl Signed { + /// Get the canonically serialized signee bytes. + pub fn canonical_signee(&self) -> Vec { + serde_jcs::to_vec(&self.signee).expect("serialization cannot fail") + } + + /// Sign the payload with the given `key`. + /// + /// This operation only fail when serialization of `payload` fails. + pub fn new( + id_key: &PubKey, + act_key: &SigningKey, + timestamp: u64, + rng: &mut (impl RngCore + ?Sized), + payload: T, + ) -> Result { + let signee = Signee { + nonce: rng.next_u32(), + payload, + timestamp, + user: UserKey { + act_key: act_key.verifying_key().into(), + id_key: id_key.clone(), + }, + }; + let canonical_signee = serde_jcs::to_vec(&signee).map_err(|_| SignatureError::new())?; + let sig = act_key.sign(&canonical_signee).to_bytes(); + + Ok(Self { sig, signee }) + } + + /// Verify `sig` is valid for `signee`. + /// + /// Note that this does not check validity of timestamp and other data. + pub fn verify(&self) -> Result<(), SignatureError> { + VerifyingKey::from_bytes(&self.signee.user.act_key.0)? + .verify_strict(&self.canonical_signee(), &Signature::from_bytes(&self.sig))?; + Ok(()) + } +} diff --git a/blah-types/src/identity.rs b/blah-types/src/identity.rs index 4091451..659325c 100644 --- a/blah-types/src/identity.rs +++ b/blah-types/src/identity.rs @@ -1,6 +1,6 @@ -use core::fmt; -use std::ops; +//! User identity description. use std::str::FromStr; +use std::{fmt, ops}; use ed25519_dalek::SignatureError; use serde::{Deserialize, Serialize}; diff --git a/blah-types/src/lib.rs b/blah-types/src/lib.rs index faabc4f..699f95b 100644 --- a/blah-types/src/lib.rs +++ b/blah-types/src/lib.rs @@ -1,723 +1,12 @@ -use std::fmt; -use std::str::FromStr; -use std::time::SystemTime; - -use bitflags_serde_shim::impl_serde_for_bitflags; -use ed25519_dalek::{ - Signature, SignatureError, Signer, SigningKey, VerifyingKey, PUBLIC_KEY_LENGTH, - SIGNATURE_LENGTH, -}; -use identity::IdUrl; -use rand::RngCore; -use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; -use serde_with::{serde_as, DisplayFromStr}; -use url::Url; - // Re-export of public dependencies. pub use bitflags; pub use ed25519_dalek; pub use url; +pub use crypto::{get_timestamp, PubKey, SignExt, Signed, Signee, UserKey}; +pub use msg::Id; + +pub mod crypto; pub mod identity; - -pub const X_BLAH_NONCE: &str = "x-blah-nonce"; -pub const X_BLAH_DIFFICULTY: &str = "x-blah-difficulty"; - -/// An opaque server-specific ID for rooms, messages, and etc. -/// It's currently serialized as a string for JavaScript's convenience. -#[serde_as] -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] -#[serde(transparent)] -pub struct Id(#[serde_as(as = "DisplayFromStr")] pub i64); - -impl fmt::Display for Id { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) - } -} - -impl Id { - pub const MIN: Self = Id(i64::MIN); - pub const MAX: Self = Id(i64::MAX); - pub const INVALID: Self = Self::MAX; -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct WithMsgId { - pub cid: Id, - #[serde(flatten)] - pub msg: T, -} - -impl WithMsgId { - pub fn new(cid: Id, msg: T) -> Self { - Self { cid, msg } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct UserKey { - pub id_key: PubKey, - pub act_key: PubKey, -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(transparent)] -pub struct PubKey(#[serde(with = "hex::serde")] pub [u8; PUBLIC_KEY_LENGTH]); - -impl FromStr for PubKey { - type Err = hex::FromHexError; - - fn from_str(s: &str) -> Result { - hex::FromHex::from_hex(s).map(Self) - } -} - -impl fmt::Debug for PubKey { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_tuple("PubKey").field(&self.to_string()).finish() - } -} - -impl fmt::Display for PubKey { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut buf = [0u8; PUBLIC_KEY_LENGTH * 2]; - hex::encode_to_slice(self.0, &mut buf).expect("buf size is correct"); - f.write_str(std::str::from_utf8(&buf).expect("hex must be UTF-8")) - } -} - -impl From for PubKey { - fn from(vk: VerifyingKey) -> Self { - Self(vk.to_bytes()) - } -} - -impl From<&VerifyingKey> for PubKey { - fn from(vk: &VerifyingKey) -> Self { - Self(vk.to_bytes()) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct Signed { - #[serde(with = "hex::serde")] - pub sig: [u8; SIGNATURE_LENGTH], - pub signee: Signee, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct Signee { - pub nonce: u32, - pub payload: T, - pub timestamp: u64, - #[serde(flatten)] - pub user: UserKey, -} - -pub trait SignExt: Sized { - fn sign_msg_with( - self, - id_key: &PubKey, - act_key: &SigningKey, - timestamp: u64, - rng: &mut (impl RngCore + ?Sized), - ) -> Result, SignatureError>; - - fn sign_msg( - self, - id_key: &PubKey, - act_key: &SigningKey, - ) -> Result, SignatureError> { - self.sign_msg_with(id_key, act_key, get_timestamp(), &mut rand::thread_rng()) - } -} - -impl SignExt for T { - fn sign_msg_with( - self, - id_key: &PubKey, - act_key: &SigningKey, - timestamp: u64, - rng: &mut (impl RngCore + ?Sized), - ) -> Result, SignatureError> { - Signed::new(id_key, act_key, timestamp, rng, self) - } -} - -pub fn get_timestamp() -> u64 { - SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .expect("after UNIX epoch") - .as_secs() -} - -impl Signed { - /// Get the canonically serialized signee bytes. - pub fn canonical_signee(&self) -> Vec { - serde_jcs::to_vec(&self.signee).expect("serialization cannot fail") - } - - /// Sign the payload with the given `key`. - /// - /// This operation only fail when serialization of `payload` fails. - pub fn new( - id_key: &PubKey, - act_key: &SigningKey, - timestamp: u64, - rng: &mut (impl RngCore + ?Sized), - payload: T, - ) -> Result { - let signee = Signee { - nonce: rng.next_u32(), - payload, - timestamp, - user: UserKey { - act_key: act_key.verifying_key().into(), - id_key: id_key.clone(), - }, - }; - let canonical_signee = serde_jcs::to_vec(&signee).map_err(|_| SignatureError::new())?; - let sig = act_key.sign(&canonical_signee).to_bytes(); - - Ok(Self { sig, signee }) - } - - /// Verify `sig` is valid for `signee`. - /// - /// Note that this does not check validity of timestamp and other data. - pub fn verify(&self) -> Result<(), SignatureError> { - VerifyingKey::from_bytes(&self.signee.user.act_key.0)? - .verify_strict(&self.canonical_signee(), &Signature::from_bytes(&self.sig))?; - Ok(()) - } -} - -/// Register a user on a chat server. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(tag = "typ", rename = "user_register")] -pub struct UserRegisterPayload { - pub server_url: Url, - pub id_url: IdUrl, - pub id_key: PubKey, - pub challenge_nonce: u32, -} - -// FIXME: `deny_unknown_fields` breaks this. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(tag = "typ", rename = "chat")] -pub struct ChatPayload { - pub rich_text: RichText, - pub room: Id, -} - -/// Ref: -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)] -#[serde(transparent)] -pub struct RichText(pub Vec); - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct RichTextPiece { - pub attrs: TextAttrs, - pub text: String, -} - -impl Serialize for RichTextPiece { - fn serialize(&self, ser: S) -> Result - 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, PartialEq, Eq, Deserialize)] -#[serde(untagged)] -enum RichTextPieceRaw { - Text(String), - TextWithAttrs(String, TextAttrs), -} - -fn is_default(v: &T) -> bool { - *v == T::default() -} - -impl<'de> Deserialize<'de> for RichText { - fn deserialize(de: D) -> Result - where - D: Deserializer<'de>, - { - let pieces = >::deserialize(de)?; - if pieces - .iter() - .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(|raw| { - let (text, attrs) = match raw { - RichTextPieceRaw::Text(text) => (text, TextAttrs::default()), - RichTextPieceRaw::TextWithAttrs(text, attrs) => (text, attrs), - }; - RichTextPiece { text, attrs } - }) - .collect(), - ); - if !this.is_canonical() { - return Err(de::Error::custom("not in canonical form")); - } - Ok(this) - } -} - -// TODO: This protocol format is quite large. Could use bitflags for database. -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub struct TextAttrs { - #[serde(default, rename = "b", skip_serializing_if = "is_default")] - pub bold: bool, - #[serde(default, rename = "m", skip_serializing_if = "is_default")] - pub code: bool, - #[serde(default, skip_serializing_if = "is_default")] - pub hashtag: bool, - #[serde(default, rename = "i", skip_serializing_if = "is_default")] - pub italic: bool, - // TODO: Should we validate and/or filter the URL. - #[serde(default, skip_serializing_if = "is_default")] - pub link: Option, - #[serde(default, rename = "s", skip_serializing_if = "is_default")] - pub strike: bool, - #[serde(default, rename = "u", skip_serializing_if = "is_default")] - pub underline: bool, -} - -impl From<&'_ str> for RichText { - fn from(text: &'_ str) -> Self { - text.to_owned().into() - } -} - -impl From for RichText { - fn from(text: String) -> Self { - if text.is_empty() { - Self::default() - } else { - Self(vec![RichTextPiece { - text, - attrs: TextAttrs::default(), - }]) - } - } -} - -impl From<&'_ str> for RichTextPiece { - fn from(text: &'_ str) -> Self { - text.to_owned().into() - } -} - -impl From for RichTextPiece { - fn from(text: String) -> Self { - Self { - text, - attrs: TextAttrs::default(), - } - } -} - -impl RichText { - /// Is this rich text valid and in the canonical form? - /// - /// This is automatically enforced by `Deserialize` impl. - pub fn is_canonical(&self) -> bool { - self.0.iter().all(|p| !p.text.is_empty()) - && self.0.windows(2).all(|w| w[0].attrs != w[1].attrs) - } - - /// Format the text into plain text, stripping all styles. - pub fn plain_text(&self) -> impl fmt::Display + '_ { - struct Fmt<'a>(&'a RichText); - impl fmt::Display for Fmt<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - for p in &self.0 .0 { - f.write_str(&p.text)?; - } - Ok(()) - } - } - - Fmt(self) - } - - /// Format the text into HTML. - pub fn html(&self) -> impl fmt::Display + '_ { - struct Fmt<'a>(&'a RichText); - impl fmt::Display for Fmt<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - for p in &self.0 .0 { - let tags = [ - (p.attrs.bold, "", ""), - (p.attrs.code, "", ""), - (p.attrs.italic, "", ""), - (p.attrs.strike, "", ""), - (p.attrs.underline, "", ""), - (p.attrs.hashtag || p.attrs.link.is_some(), "", ""), - ]; - for (cond, begin, _) in tags { - if cond { - f.write_str(begin)?; - } - } - if p.attrs.hashtag { - // TODO: Link target for hashtag? - write!(f, r#""#)?; - } else if let Some(link) = &p.attrs.link { - let href = html_escape::encode_quoted_attribute(link); - write!(f, r#"; - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RoomMetadata { - /// Room id. - pub rid: Id, - /// Plain text room title. None for peer chat. - pub title: Option, - /// Room attributes. - pub attrs: RoomAttrs, - - // Extra information is only available for some APIs. - /// The last message in the room. - #[serde(skip_serializing_if = "Option::is_none")] - pub last_msg: Option>, - /// The current user's last seen message's `cid`. - #[serde(skip_serializing_if = "Option::is_none")] - pub last_seen_cid: Option, - /// The number of unseen messages, ie. the number of messages from `last_seen_cid` to - /// `last_msg.cid`. - /// This may or may not be a precise number. - #[serde(skip_serializing_if = "Option::is_none")] - pub unseen_cnt: Option, - /// The member permission of current user in the room, or `None` if it is not a member. - /// Only available with authentication. - #[serde(skip_serializing_if = "Option::is_none")] - pub member_permission: Option, - /// The peer user, if this is a peer chat room. - #[serde(skip_serializing_if = "Option::is_none")] - pub peer_user: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(tag = "typ")] -pub enum CreateRoomPayload { - #[serde(rename = "create_room")] - Group(CreateGroup), - #[serde(rename = "create_peer_chat")] - PeerChat(CreatePeerChat), -} - -/// Multi-user room. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct CreateGroup { - pub attrs: RoomAttrs, - pub title: String, -} - -/// Peer-to-peer chat room with exactly two symmetric users. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct CreatePeerChat { - pub peer: PubKey, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(tag = "typ", rename = "delete_room")] -pub struct DeleteRoomPayload { - pub room: Id, -} - -/// A collection of room members, with these invariants: -/// 1. Sorted by userkeys. -/// 2. No duplicated users. -#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] -#[serde(try_from = "Vec")] -pub struct RoomMemberList(pub Vec); - -impl Serialize for RoomMemberList { - fn serialize(&self, ser: S) -> Result - where - S: Serializer, - { - self.0.serialize(ser) - } -} - -impl TryFrom> for RoomMemberList { - type Error = &'static str; - - fn try_from(members: Vec) -> Result { - if members.windows(2).all(|w| w[0].user.0 < w[1].user.0) { - Ok(Self(members)) - } else { - Err("unsorted or duplicated users") - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RoomMember { - pub permission: MemberPermission, - pub user: PubKey, -} - -/// Proof of room membership for read-access. -/// -/// TODO: Should we use JWT here instead? -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(tag = "typ", rename = "auth")] -pub struct AuthPayload {} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -// `typ` is provided by `RoomAdminOp`. -pub struct RoomAdminPayload { - pub room: Id, - #[serde(flatten)] - pub op: RoomAdminOp, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(tag = "typ", rename_all = "snake_case")] -pub enum RoomAdminOp { - AddMember { - permission: MemberPermission, - user: PubKey, - }, - RemoveMember { - user: PubKey, - }, - // TODO: RU -} - -bitflags::bitflags! { - /// TODO: Is this a really all about permission, or is a generic `UserFlags`? - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub struct ServerPermission: i32 { - const CREATE_ROOM = 1 << 0; - - const ACCEPT_PEER_CHAT = 1 << 16; - - const ALL = !0; - } - - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub struct MemberPermission: i32 { - const POST_CHAT = 1 << 0; - const ADD_MEMBER = 1 << 1; - const DELETE_ROOM = 1 << 2; - - const MAX_SELF_ADD = Self::POST_CHAT.bits(); - const MAX_PEER_CHAT = Self::POST_CHAT.bits() | Self::DELETE_ROOM.bits(); - - const ALL = !0; - } - - #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] - pub struct RoomAttrs: i32 { - // NB. Used by schema. - const PUBLIC_READABLE = 1 << 0; - const PUBLIC_JOINABLE = 1 << 1; - - const GROUP_ATTRS = (1 << 16) - 1; - - // NB. Used by schema. - const PEER_CHAT = 1 << 16; - - const _ = !0; - } -} - -impl_serde_for_bitflags!(ServerPermission); -impl_serde_for_bitflags!(MemberPermission); -impl_serde_for_bitflags!(RoomAttrs); - -#[cfg(feature = "rusqlite")] -mod sql_impl { - use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef}; - use rusqlite::{Result, ToSql}; - - use super::*; - - impl ToSql for Id { - fn to_sql(&self) -> Result> { - self.0.to_sql() - } - } - - impl FromSql for Id { - fn column_result(value: ValueRef<'_>) -> FromSqlResult { - i64::column_result(value).map(Self) - } - } - - impl ToSql for PubKey { - fn to_sql(&self) -> Result> { - // TODO: Extensive key format? - self.0.to_sql() - } - } - - impl FromSql for PubKey { - fn column_result(value: ValueRef<'_>) -> FromSqlResult { - let rawkey = <[u8; PUBLIC_KEY_LENGTH]>::column_result(value)?; - let key = VerifyingKey::from_bytes(&rawkey) - .map_err(|err| FromSqlError::Other(format!("invalid pubkey: {err}").into()))?; - Ok(key.into()) - } - } - - impl ToSql for RichText { - fn to_sql(&self) -> Result> { - assert!(self.is_canonical()); - let json = serde_json::to_string(&self).expect("serialization cannot fail"); - Ok(json.into()) - } - } - - impl FromSql for RichText { - fn column_result(value: ValueRef<'_>) -> FromSqlResult { - serde_json::from_str::(value.as_str()?) - .map_err(|err| FromSqlError::Other(format!("invalid rich text: {err}").into())) - } - } - - macro_rules! impl_flag_to_from_sql { - ($($name:ident),*) => { - $( - impl ToSql for $name { - fn to_sql(&self) -> Result> { - Ok(self.bits().into()) - } - } - - impl FromSql for $name { - fn column_result(value: ValueRef<'_>) -> FromSqlResult { - i32::column_result(value).map($name::from_bits_retain) - } - } - )* - }; - } - - impl_flag_to_from_sql!(ServerPermission, MemberPermission, RoomAttrs); -} - -#[cfg(test)] -mod tests { - use expect_test::expect; - - use super::*; - - #[test] - fn canonical_msg() { - let mut fake_rng = rand::rngs::mock::StepRng::new(0x42, 1); - let id_key = SigningKey::from_bytes(&[0x42; 32]); - let act_key = SigningKey::from_bytes(&[0x43; 32]); - let timestamp = 0xDEAD_BEEF; - let msg = ChatPayload { - rich_text: RichText::from("hello"), - room: Id(42), - } - .sign_msg_with( - &id_key.verifying_key().into(), - &act_key, - timestamp, - &mut fake_rng, - ) - .unwrap(); - - let json = serde_jcs::to_string(&msg).unwrap(); - let expect = expect![[ - r#"{"sig":"74ca2895ac94e741e086bae28ce8c282bf375e3e59a3408f562420d72e98d799f7e627879aa883fa0804a0799eb9b90398150b0150c2e3550635ff28b9991502","signee":{"act_key":"22fc297792f0b6ffc0bfcfdb7edb0c0aa14e025a365ec0e342e86e3829cb74b6","id_key":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","nonce":66,"payload":{"rich_text":["hello"],"room":"42","typ":"chat"},"timestamp":3735928559}}"# - ]]; - expect.assert_eq(&json); - - let roundtrip_msg = serde_json::from_str::>(&json).unwrap(); - assert_eq!(roundtrip_msg, msg); - roundtrip_msg.verify().unwrap(); - } - - #[test] - fn rich_text_serde() { - let raw = r#"["before ",["bold ",{"b":true}],["italic bold ",{"b":true,"i":true}],"end"]"#; - let text = serde_json::from_str::(raw).unwrap(); - assert!(text.is_canonical()); - assert_eq!( - text, - RichText(vec![ - "before ".into(), - RichTextPiece { - text: "bold ".into(), - attrs: TextAttrs { - bold: true, - ..TextAttrs::default() - } - }, - RichTextPiece { - text: "italic bold ".into(), - attrs: TextAttrs { - italic: true, - bold: true, - ..TextAttrs::default() - } - }, - "end".into(), - ]), - ); - let got = serde_json::to_string(&text).unwrap(); - assert_eq!(got, raw); - } - - #[test] - fn room_admin_serde() { - let data = RoomAdminPayload { - room: Id(42), - op: RoomAdminOp::AddMember { - permission: MemberPermission::POST_CHAT, - user: PubKey([0x42; PUBLIC_KEY_LENGTH]), - }, - }; - let raw = serde_jcs::to_string(&data).unwrap(); - - let expect = expect![[ - r#"{"permission":1,"room":"42","typ":"add_member","user":"4242424242424242424242424242424242424242424242424242424242424242"}"# - ]]; - expect.assert_eq(&raw); - let roundtrip = serde_json::from_str::(&raw).unwrap(); - assert_eq!(roundtrip, data); - } -} +pub mod msg; +pub mod server; diff --git a/blah-types/src/msg.rs b/blah-types/src/msg.rs new file mode 100644 index 0000000..c172639 --- /dev/null +++ b/blah-types/src/msg.rs @@ -0,0 +1,544 @@ +//! Core message subtypes. +use std::fmt; + +use bitflags_serde_shim::impl_serde_for_bitflags; +use serde::{de, ser, Deserialize, Serialize}; +use serde_with::{serde_as, DisplayFromStr}; +use url::Url; + +use crate::identity::IdUrl; +use crate::{PubKey, Signed}; + +/// An opaque server-specific ID for rooms, messages, and etc. +/// It's currently serialized as a string for JavaScript's convenience. +#[serde_as] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +#[serde(transparent)] +pub struct Id(#[serde_as(as = "DisplayFromStr")] pub i64); + +impl fmt::Display for Id { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +impl Id { + pub const MIN: Self = Id(i64::MIN); + pub const MAX: Self = Id(i64::MAX); + pub const INVALID: Self = Self::MAX; +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct WithMsgId { + pub cid: Id, + #[serde(flatten)] + pub msg: T, +} + +impl WithMsgId { + pub fn new(cid: Id, msg: T) -> Self { + Self { cid, msg } + } +} + +/// Register a user on a chat server. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "typ", rename = "user_register")] +pub struct UserRegisterPayload { + pub server_url: Url, + pub id_url: IdUrl, + pub id_key: PubKey, + pub challenge_nonce: u32, +} + +// FIXME: `deny_unknown_fields` breaks this. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "typ", rename = "chat")] +pub struct ChatPayload { + pub rich_text: RichText, + pub room: Id, +} + +/// Ref: +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)] +#[serde(transparent)] +pub struct RichText(pub Vec); + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RichTextPiece { + pub attrs: TextAttrs, + pub text: String, +} + +impl Serialize for RichTextPiece { + fn serialize(&self, ser: S) -> Result + where + S: ser::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, PartialEq, Eq, Deserialize)] +#[serde(untagged)] +enum RichTextPieceRaw { + Text(String), + TextWithAttrs(String, TextAttrs), +} + +fn is_default(v: &T) -> bool { + *v == T::default() +} + +impl<'de> Deserialize<'de> for RichText { + fn deserialize(de: D) -> Result + where + D: de::Deserializer<'de>, + { + let pieces = >::deserialize(de)?; + if pieces + .iter() + .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(|raw| { + let (text, attrs) = match raw { + RichTextPieceRaw::Text(text) => (text, TextAttrs::default()), + RichTextPieceRaw::TextWithAttrs(text, attrs) => (text, attrs), + }; + RichTextPiece { text, attrs } + }) + .collect(), + ); + if !this.is_canonical() { + return Err(de::Error::custom("not in canonical form")); + } + Ok(this) + } +} + +// TODO: This protocol format is quite large. Could use bitflags for database. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct TextAttrs { + #[serde(default, rename = "b", skip_serializing_if = "is_default")] + pub bold: bool, + #[serde(default, rename = "m", skip_serializing_if = "is_default")] + pub code: bool, + #[serde(default, skip_serializing_if = "is_default")] + pub hashtag: bool, + #[serde(default, rename = "i", skip_serializing_if = "is_default")] + pub italic: bool, + // TODO: Should we validate and/or filter the URL. + #[serde(default, skip_serializing_if = "is_default")] + pub link: Option, + #[serde(default, rename = "s", skip_serializing_if = "is_default")] + pub strike: bool, + #[serde(default, rename = "u", skip_serializing_if = "is_default")] + pub underline: bool, +} + +impl From<&'_ str> for RichText { + fn from(text: &'_ str) -> Self { + text.to_owned().into() + } +} + +impl From for RichText { + fn from(text: String) -> Self { + if text.is_empty() { + Self::default() + } else { + Self(vec![RichTextPiece { + text, + attrs: TextAttrs::default(), + }]) + } + } +} + +impl From<&'_ str> for RichTextPiece { + fn from(text: &'_ str) -> Self { + text.to_owned().into() + } +} + +impl From for RichTextPiece { + fn from(text: String) -> Self { + Self { + text, + attrs: TextAttrs::default(), + } + } +} + +impl RichText { + /// Is this rich text valid and in the canonical form? + /// + /// This is automatically enforced by `Deserialize` impl. + pub fn is_canonical(&self) -> bool { + self.0.iter().all(|p| !p.text.is_empty()) + && self.0.windows(2).all(|w| w[0].attrs != w[1].attrs) + } + + /// Format the text into plain text, stripping all styles. + pub fn plain_text(&self) -> impl fmt::Display + '_ { + struct Fmt<'a>(&'a RichText); + impl fmt::Display for Fmt<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for p in &self.0 .0 { + f.write_str(&p.text)?; + } + Ok(()) + } + } + + Fmt(self) + } + + /// Format the text into HTML. + pub fn html(&self) -> impl fmt::Display + '_ { + struct Fmt<'a>(&'a RichText); + impl fmt::Display for Fmt<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for p in &self.0 .0 { + let tags = [ + (p.attrs.bold, "", ""), + (p.attrs.code, "", ""), + (p.attrs.italic, "", ""), + (p.attrs.strike, "", ""), + (p.attrs.underline, "", ""), + (p.attrs.hashtag || p.attrs.link.is_some(), "", ""), + ]; + for (cond, begin, _) in tags { + if cond { + f.write_str(begin)?; + } + } + if p.attrs.hashtag { + // TODO: Link target for hashtag? + write!(f, r#""#)?; + } else if let Some(link) = &p.attrs.link { + let href = html_escape::encode_quoted_attribute(link); + write!(f, r#"; +pub type SignedChatMsgWithId = WithMsgId; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "typ")] +pub enum CreateRoomPayload { + #[serde(rename = "create_room")] + Group(CreateGroup), + #[serde(rename = "create_peer_chat")] + PeerChat(CreatePeerChat), +} + +/// Multi-user room. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CreateGroup { + pub attrs: RoomAttrs, + pub title: String, +} + +/// Peer-to-peer chat room with exactly two symmetric users. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CreatePeerChat { + pub peer: PubKey, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "typ", rename = "delete_room")] +pub struct DeleteRoomPayload { + pub room: Id, +} + +/// A collection of room members, with these invariants: +/// 1. Sorted by userkeys. +/// 2. No duplicated users. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +#[serde(try_from = "Vec")] +pub struct RoomMemberList(pub Vec); + +impl Serialize for RoomMemberList { + fn serialize(&self, ser: S) -> Result + where + S: ser::Serializer, + { + self.0.serialize(ser) + } +} + +impl TryFrom> for RoomMemberList { + type Error = &'static str; + + fn try_from(members: Vec) -> Result { + if members.windows(2).all(|w| w[0].user.0 < w[1].user.0) { + Ok(Self(members)) + } else { + Err("unsorted or duplicated users") + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RoomMember { + pub permission: MemberPermission, + pub user: PubKey, +} + +/// Proof of room membership for read-access. +/// +/// TODO: Should we use JWT here instead? +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "typ", rename = "auth")] +pub struct AuthPayload {} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +// `typ` is provided by `RoomAdminOp`. +pub struct RoomAdminPayload { + pub room: Id, + #[serde(flatten)] + pub op: RoomAdminOp, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "typ", rename_all = "snake_case")] +pub enum RoomAdminOp { + AddMember { + permission: MemberPermission, + user: PubKey, + }, + RemoveMember { + user: PubKey, + }, + // TODO: RU +} + +bitflags::bitflags! { + /// TODO: Is this a really all about permission, or is a generic `UserFlags`? + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub struct ServerPermission: i32 { + const CREATE_ROOM = 1 << 0; + + const ACCEPT_PEER_CHAT = 1 << 16; + + const ALL = !0; + } + + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub struct MemberPermission: i32 { + const POST_CHAT = 1 << 0; + const ADD_MEMBER = 1 << 1; + const DELETE_ROOM = 1 << 2; + + const MAX_SELF_ADD = Self::POST_CHAT.bits(); + const MAX_PEER_CHAT = Self::POST_CHAT.bits() | Self::DELETE_ROOM.bits(); + + const ALL = !0; + } + + #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] + pub struct RoomAttrs: i32 { + // NB. Used by schema. + const PUBLIC_READABLE = 1 << 0; + const PUBLIC_JOINABLE = 1 << 1; + + const GROUP_ATTRS = (1 << 16) - 1; + + // NB. Used by schema. + const PEER_CHAT = 1 << 16; + + const _ = !0; + } +} + +impl_serde_for_bitflags!(ServerPermission); +impl_serde_for_bitflags!(MemberPermission); +impl_serde_for_bitflags!(RoomAttrs); + +#[cfg(feature = "rusqlite")] +mod sql_impl { + use ed25519_dalek::{VerifyingKey, PUBLIC_KEY_LENGTH}; + use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef}; + use rusqlite::{Result, ToSql}; + + use super::*; + + impl ToSql for Id { + fn to_sql(&self) -> Result> { + self.0.to_sql() + } + } + + impl FromSql for Id { + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + i64::column_result(value).map(Self) + } + } + + impl ToSql for PubKey { + fn to_sql(&self) -> Result> { + // TODO: Extensive key format? + self.0.to_sql() + } + } + + impl FromSql for PubKey { + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + let rawkey = <[u8; PUBLIC_KEY_LENGTH]>::column_result(value)?; + let key = VerifyingKey::from_bytes(&rawkey) + .map_err(|err| FromSqlError::Other(format!("invalid pubkey: {err}").into()))?; + Ok(key.into()) + } + } + + impl ToSql for RichText { + fn to_sql(&self) -> Result> { + assert!(self.is_canonical()); + let json = serde_json::to_string(&self).expect("serialization cannot fail"); + Ok(json.into()) + } + } + + impl FromSql for RichText { + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + serde_json::from_str::(value.as_str()?) + .map_err(|err| FromSqlError::Other(format!("invalid rich text: {err}").into())) + } + } + + macro_rules! impl_flag_to_from_sql { + ($($name:ident),*) => { + $( + impl ToSql for $name { + fn to_sql(&self) -> Result> { + Ok(self.bits().into()) + } + } + + impl FromSql for $name { + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + i32::column_result(value).map($name::from_bits_retain) + } + } + )* + }; + } + + impl_flag_to_from_sql!(ServerPermission, MemberPermission, RoomAttrs); +} + +#[cfg(test)] +mod tests { + use ed25519_dalek::{SigningKey, PUBLIC_KEY_LENGTH}; + use expect_test::expect; + + use crate::SignExt; + + use super::*; + + #[test] + fn canonical_msg() { + let mut fake_rng = rand::rngs::mock::StepRng::new(0x42, 1); + let id_key = SigningKey::from_bytes(&[0x42; 32]); + let act_key = SigningKey::from_bytes(&[0x43; 32]); + let timestamp = 0xDEAD_BEEF; + let msg = ChatPayload { + rich_text: RichText::from("hello"), + room: Id(42), + } + .sign_msg_with( + &id_key.verifying_key().into(), + &act_key, + timestamp, + &mut fake_rng, + ) + .unwrap(); + + let json = serde_jcs::to_string(&msg).unwrap(); + let expect = expect![[ + r#"{"sig":"74ca2895ac94e741e086bae28ce8c282bf375e3e59a3408f562420d72e98d799f7e627879aa883fa0804a0799eb9b90398150b0150c2e3550635ff28b9991502","signee":{"act_key":"22fc297792f0b6ffc0bfcfdb7edb0c0aa14e025a365ec0e342e86e3829cb74b6","id_key":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","nonce":66,"payload":{"rich_text":["hello"],"room":"42","typ":"chat"},"timestamp":3735928559}}"# + ]]; + expect.assert_eq(&json); + + let roundtrip_msg = serde_json::from_str::>(&json).unwrap(); + assert_eq!(roundtrip_msg, msg); + roundtrip_msg.verify().unwrap(); + } + + #[test] + fn rich_text_serde() { + let raw = r#"["before ",["bold ",{"b":true}],["italic bold ",{"b":true,"i":true}],"end"]"#; + let text = serde_json::from_str::(raw).unwrap(); + assert!(text.is_canonical()); + assert_eq!( + text, + RichText(vec![ + "before ".into(), + RichTextPiece { + text: "bold ".into(), + attrs: TextAttrs { + bold: true, + ..TextAttrs::default() + } + }, + RichTextPiece { + text: "italic bold ".into(), + attrs: TextAttrs { + italic: true, + bold: true, + ..TextAttrs::default() + } + }, + "end".into(), + ]), + ); + let got = serde_json::to_string(&text).unwrap(); + assert_eq!(got, raw); + } + + #[test] + fn room_admin_serde() { + let data = RoomAdminPayload { + room: Id(42), + op: RoomAdminOp::AddMember { + permission: MemberPermission::POST_CHAT, + user: PubKey([0x42; PUBLIC_KEY_LENGTH]), + }, + }; + let raw = serde_jcs::to_string(&data).unwrap(); + + let expect = expect![[ + r#"{"permission":1,"room":"42","typ":"add_member","user":"4242424242424242424242424242424242424242424242424242424242424242"}"# + ]]; + expect.assert_eq(&raw); + let roundtrip = serde_json::from_str::(&raw).unwrap(); + assert_eq!(roundtrip, data); + } +} diff --git a/blah-types/src/server.rs b/blah-types/src/server.rs new file mode 100644 index 0000000..13f5ac7 --- /dev/null +++ b/blah-types/src/server.rs @@ -0,0 +1,39 @@ +//! Data types and constants for Chat Server interaction. + +use serde::{Deserialize, Serialize}; + +use crate::msg::{Id, MemberPermission, RoomAttrs, SignedChatMsgWithId}; +use crate::PubKey; + +pub const X_BLAH_NONCE: &str = "x-blah-nonce"; +pub const X_BLAH_DIFFICULTY: &str = "x-blah-difficulty"; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RoomMetadata { + /// Room id. + pub rid: Id, + /// Plain text room title. None for peer chat. + pub title: Option, + /// Room attributes. + pub attrs: RoomAttrs, + + // Extra information is only available for some APIs. + /// The last message in the room. + #[serde(skip_serializing_if = "Option::is_none")] + pub last_msg: Option, + /// The current user's last seen message's `cid`. + #[serde(skip_serializing_if = "Option::is_none")] + pub last_seen_cid: Option, + /// The number of unseen messages, ie. the number of messages from `last_seen_cid` to + /// `last_msg.cid`. + /// This may or may not be a precise number. + #[serde(skip_serializing_if = "Option::is_none")] + pub unseen_cnt: Option, + /// The member permission of current user in the room, or `None` if it is not a member. + /// Only available with authentication. + #[serde(skip_serializing_if = "Option::is_none")] + pub member_permission: Option, + /// The peer user, if this is a peer chat room. + #[serde(skip_serializing_if = "Option::is_none")] + pub peer_user: Option, +} diff --git a/blahctl/src/main.rs b/blahctl/src/main.rs index db6f4b7..189772c 100644 --- a/blahctl/src/main.rs +++ b/blahctl/src/main.rs @@ -4,7 +4,8 @@ use std::time::SystemTime; use anyhow::{ensure, Context, Result}; use blah_types::identity::{IdUrl, UserActKeyDesc, UserIdentityDesc, UserProfile}; -use blah_types::{bitflags, get_timestamp, PubKey, RoomAttrs, ServerPermission, SignExt}; +use blah_types::msg::{RoomAttrs, ServerPermission}; +use blah_types::{bitflags, get_timestamp, PubKey, SignExt}; use clap::value_parser; use ed25519_dalek::pkcs8::spki::der::pem::LineEnding; use ed25519_dalek::pkcs8::{DecodePrivateKey, DecodePublicKey, EncodePrivateKey}; diff --git a/blahd/src/database.rs b/blahd/src/database.rs index 3310366..748f129 100644 --- a/blahd/src/database.rs +++ b/blahd/src/database.rs @@ -3,10 +3,12 @@ use std::path::PathBuf; use anyhow::{ensure, Context}; use blah_types::identity::UserIdentityDesc; -use blah_types::{ - ChatPayload, Id, MemberPermission, PubKey, RoomAttrs, RoomMetadata, ServerPermission, - SignedChatMsg, Signee, UserKey, WithMsgId, +use blah_types::msg::{ + ChatPayload, MemberPermission, RoomAttrs, ServerPermission, SignedChatMsg, SignedChatMsgWithId, + WithMsgId, }; +use blah_types::server::RoomMetadata; +use blah_types::{Id, PubKey, Signee, UserKey}; use parking_lot::Mutex; use rusqlite::{named_params, params, prepare_cached_and_bind, Connection, OpenFlags, Row}; use serde::Deserialize; @@ -127,7 +129,7 @@ impl Database { } } -fn parse_msg(rid: Id, row: &Row<'_>) -> Result> { +fn parse_msg(rid: Id, row: &Row<'_>) -> Result { Ok(WithMsgId { cid: row.get("cid")?, msg: SignedChatMsg { @@ -366,7 +368,7 @@ pub trait TransactionOps { after_cid: Id, before_cid: Id, page_len: NonZero, - ) -> Result>> { + ) -> Result> { prepare_cached_and_bind!( self.conn(), r" diff --git a/blahd/src/event.rs b/blahd/src/event.rs index b2657b6..2ae9a34 100644 --- a/blahd/src/event.rs +++ b/blahd/src/event.rs @@ -9,7 +9,8 @@ use std::time::Duration; use anyhow::{bail, Context as _, Result}; use axum::extract::ws::{Message, WebSocket}; -use blah_types::{AuthPayload, Signed, SignedChatMsg}; +use blah_types::msg::{AuthPayload, SignedChatMsg}; +use blah_types::Signed; use futures_util::future::Either; use futures_util::stream::SplitSink; use futures_util::{stream_select, SinkExt as _, Stream, StreamExt}; diff --git a/blahd/src/feed.rs b/blahd/src/feed.rs index e2e2416..706d612 100644 --- a/blahd/src/feed.rs +++ b/blahd/src/feed.rs @@ -6,7 +6,8 @@ use std::time::{Duration, SystemTime}; use axum::http::header; use axum::response::{IntoResponse, Response}; use axum::Json; -use blah_types::{Id, SignedChatMsg, WithMsgId}; +use blah_types::msg::{SignedChatMsgWithId, WithMsgId}; +use blah_types::Id; use serde::{Deserialize, Serialize}; use url::Url; @@ -32,7 +33,7 @@ impl Default for Config { pub struct FeedData { pub rid: Id, pub title: String, - pub msgs: Vec>, + pub msgs: Vec, pub self_url: Url, pub next_url: Option, } diff --git a/blahd/src/lib.rs b/blahd/src/lib.rs index 2c34a1b..62174bd 100644 --- a/blahd/src/lib.rs +++ b/blahd/src/lib.rs @@ -10,12 +10,13 @@ use axum::response::Response; use axum::routing::{get, post}; use axum::{Json, Router}; use axum_extra::extract::WithRejection as R; -use blah_types::{ - get_timestamp, ChatPayload, CreateGroup, CreatePeerChat, CreateRoomPayload, DeleteRoomPayload, - Id, MemberPermission, RoomAdminOp, RoomAdminPayload, RoomAttrs, RoomMetadata, ServerPermission, - Signed, SignedChatMsg, UserKey, UserRegisterPayload, WithMsgId, X_BLAH_DIFFICULTY, - X_BLAH_NONCE, +use blah_types::msg::{ + ChatPayload, CreateGroup, CreatePeerChat, CreateRoomPayload, DeleteRoomPayload, + MemberPermission, RoomAdminOp, RoomAdminPayload, RoomAttrs, ServerPermission, + SignedChatMsgWithId, UserRegisterPayload, }; +use blah_types::server::{RoomMetadata, X_BLAH_DIFFICULTY, X_BLAH_NONCE}; +use blah_types::{get_timestamp, Id, Signed, UserKey}; use database::{Transaction, TransactionOps}; use feed::FeedData; use id::IdExt; @@ -345,7 +346,7 @@ impl Pagination { #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct RoomMsgs { - pub msgs: Vec>, + pub msgs: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub skip_token: Option, } @@ -460,7 +461,7 @@ fn query_room_msgs( txn: &Transaction<'_>, rid: Id, pagination: Pagination, -) -> Result<(Vec>, Option), ApiError> { +) -> Result<(Vec, Option), ApiError> { let page_len = pagination.effective_page_len(st); let msgs = txn.list_room_msgs( rid, diff --git a/blahd/src/middleware.rs b/blahd/src/middleware.rs index 8ab8316..0faa5e5 100644 --- a/blahd/src/middleware.rs +++ b/blahd/src/middleware.rs @@ -8,7 +8,8 @@ use axum::extract::{FromRef, FromRequest, FromRequestParts, Request}; use axum::http::{header, request, StatusCode}; use axum::response::{IntoResponse, Response}; use axum::{async_trait, Json}; -use blah_types::{AuthPayload, Signed, UserKey}; +use blah_types::msg::AuthPayload; +use blah_types::{Signed, UserKey}; use serde::de::DeserializeOwned; use serde::Serialize; diff --git a/blahd/src/register.rs b/blahd/src/register.rs index 0127be0..4bfab15 100644 --- a/blahd/src/register.rs +++ b/blahd/src/register.rs @@ -4,7 +4,9 @@ use std::time::{Duration, Instant}; use anyhow::{anyhow, ensure}; use axum::http::{HeaderMap, HeaderName, StatusCode}; use blah_types::identity::{IdUrl, UserIdentityDesc}; -use blah_types::{get_timestamp, Signed, UserRegisterPayload, X_BLAH_DIFFICULTY, X_BLAH_NONCE}; +use blah_types::msg::UserRegisterPayload; +use blah_types::server::{X_BLAH_DIFFICULTY, X_BLAH_NONCE}; +use blah_types::{get_timestamp, Signed}; use http_body_util::BodyExt; use parking_lot::Mutex; use rand::rngs::OsRng; diff --git a/blahd/tests/webapi.rs b/blahd/tests/webapi.rs index f2f52d2..1f40b9b 100644 --- a/blahd/tests/webapi.rs +++ b/blahd/tests/webapi.rs @@ -10,12 +10,13 @@ use std::time::{Duration, Instant}; use anyhow::{Context, Result}; use axum::http::HeaderMap; use blah_types::identity::{IdUrl, UserActKeyDesc, UserIdentityDesc, UserProfile}; -use blah_types::{ +use blah_types::msg::{ AuthPayload, ChatPayload, CreateGroup, CreatePeerChat, CreateRoomPayload, DeleteRoomPayload, - Id, MemberPermission, RichText, RoomAdminOp, RoomAdminPayload, RoomAttrs, RoomMetadata, - ServerPermission, SignExt, Signed, SignedChatMsg, UserKey, UserRegisterPayload, WithMsgId, - X_BLAH_DIFFICULTY, X_BLAH_NONCE, + MemberPermission, RichText, RoomAdminOp, RoomAdminPayload, RoomAttrs, ServerPermission, + SignedChatMsg, SignedChatMsgWithId, UserRegisterPayload, WithMsgId, }; +use blah_types::server::{RoomMetadata, X_BLAH_DIFFICULTY, X_BLAH_NONCE}; +use blah_types::{Id, SignExt, Signed, UserKey}; use blahd::{AppState, Database, RoomList, RoomMsgs}; use ed25519_dalek::SigningKey; use expect_test::expect; @@ -329,7 +330,7 @@ impl Server { rid: Id, user: &User, text: &str, - ) -> impl Future>> + use<'_> { + ) -> impl Future> + use<'_> { let msg = self.sign( user, ChatPayload {