diff --git a/Cargo.lock b/Cargo.lock
index 84dde2d..fbf0dd2 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -195,6 +195,7 @@ dependencies = [
"rusqlite",
"serde",
"serde_json",
+ "thiserror",
"tokio",
"tokio-tungstenite 0.17.1",
"toss",
diff --git a/cove-tui/Cargo.toml b/cove-tui/Cargo.toml
index 545f0fd..3d6be6e 100644
--- a/cove-tui/Cargo.toml
+++ b/cove-tui/Cargo.toml
@@ -15,6 +15,7 @@ parking_lot = "0.12.1"
rusqlite = { version = "0.27.0", features = ["chrono"] }
serde = { version = "1.0.137", features = ["derive"] }
serde_json = "1.0.81"
+thiserror = "1.0.31"
tokio = { version = "1.19.2", features = ["full"] }
tokio-tungstenite = "0.17.1"
toss = { git = "https://github.com/Garmelon/toss.git", rev = "761519c1a7cdc950eab70fd6539c71bf22919a50" }
diff --git a/cove-tui/src/euph/api.rs b/cove-tui/src/euph/api.rs
index 9339dde..d4729dd 100644
--- a/cove-tui/src/euph/api.rs
+++ b/cove-tui/src/euph/api.rs
@@ -1,24 +1,13 @@
//! Models the euphoria API at .
mod events;
+mod packet;
mod room_cmds;
mod session_cmds;
mod types;
-use serde::{Deserialize, Serialize};
-use serde_json::Value;
-
pub use events::*;
+pub use packet::*;
pub use room_cmds::*;
pub use session_cmds::*;
pub use types::*;
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct Packet {
- pub id: Option,
- pub r#type: PacketType,
- pub data: Option,
- pub error: Option,
- pub throttled: Option,
- pub throttled_reason: Option,
-}
diff --git a/cove-tui/src/euph/api/events.rs b/cove-tui/src/euph/api/events.rs
index 1d79b03..0fa7afd 100644
--- a/cove-tui/src/euph/api/events.rs
+++ b/cove-tui/src/euph/api/events.rs
@@ -2,7 +2,10 @@
use serde::{Deserialize, Serialize};
-use super::{AuthOption, Message, PersonalAccountView, SessionView, Snowflake, Time, UserId};
+use super::{
+ has_packet_type, AuthOption, HasPacketType, Message, PacketType, PersonalAccountView,
+ SessionView, Snowflake, Time, UserId,
+};
/// Indicates that access to a room is denied.
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -17,6 +20,8 @@ pub struct BounceEvent {
pub ip: Option,
}
+has_packet_type!(BounceEvent);
+
/// Indicates that the session is being closed. The client will subsequently be
/// disconnected.
///
@@ -28,6 +33,8 @@ pub struct DisconnectEvent {
pub reason: String,
}
+has_packet_type!(DisconnectEvent);
+
/// Sent by the server to the client when a session is started.
///
/// It includes information about the client's authentication and associated
@@ -51,10 +58,14 @@ pub struct HelloEvent {
pub version: String,
}
+has_packet_type!(HelloEvent);
+
/// Indicates a session just joined the room.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JoinEvent(pub SessionView);
+has_packet_type!(JoinEvent);
+
/// Sent to all sessions of an agent when that agent is logged in (except for
/// the session that issued the login command).
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -62,11 +73,15 @@ pub struct LoginEvent {
pub account_id: Snowflake,
}
+has_packet_type!(LoginEvent);
+
/// Sent to all sessions of an agent when that agent is logged out (except for
/// the session that issued the logout command).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogoutEvent;
+has_packet_type!(LogoutEvent);
+
/// Indicates some server-side event that impacts the presence of sessions in a
/// room.
///
@@ -82,6 +97,8 @@ pub struct NetworkEvent {
pub server_era: String,
}
+has_packet_type!(NetworkEvent);
+
/// Announces a nick change by another session in the room.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NickEvent {
@@ -95,6 +112,8 @@ pub struct NickEvent {
pub to: String,
}
+has_packet_type!(NickEvent);
+
/// Indicates that a message in the room has been modified or deleted.
///
/// If the client offers a user interface and the indicated message is currently
@@ -110,10 +129,14 @@ pub struct EditMessageEvent {
pub message: Message,
}
+has_packet_type!(EditMessageEvent);
+
/// Indicates a session just disconnected from the room.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PartEvent(pub SessionView);
+has_packet_type!(PartEvent);
+
/// Represents a server-to-client ping.
///
/// The client should send back a ping-reply with the same value for the time
@@ -127,6 +150,8 @@ pub struct PingEvent {
pub next: Time,
}
+has_packet_type!(PingEvent);
+
/// Informs the client that another user wants to chat with them privately.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PmInitiateEvent {
@@ -140,10 +165,14 @@ pub struct PmInitiateEvent {
pub pm_id: Snowflake,
}
+has_packet_type!(PmInitiateEvent);
+
/// Indicates a message received by the room from another session.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SendEvent(pub Message);
+has_packet_type!(SendEvent);
+
/// Indicates that a session has successfully joined a room.
///
/// It also offers a snapshot of the room’s state and recent history.
@@ -168,3 +197,5 @@ pub struct SnapshotEvent {
/// If given, this room is for private chat with the given user.
pub pm_with_user_id: Option,
}
+
+has_packet_type!(SnapshotEvent);
diff --git a/cove-tui/src/euph/api/packet.rs b/cove-tui/src/euph/api/packet.rs
new file mode 100644
index 0000000..ddd4d73
--- /dev/null
+++ b/cove-tui/src/euph/api/packet.rs
@@ -0,0 +1,270 @@
+use std::fmt;
+
+use serde::de::DeserializeOwned;
+use serde::{Deserialize, Serialize};
+use serde_json::Value;
+
+/// The type of a packet.
+///
+/// Not all of these types have their corresponding data modeled as a struct.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case")]
+pub enum PacketType {
+ // Asynchronous events
+ /// See [`BounceEvent`](super::BounceEvent).
+ BounceEvent,
+ /// See [`DisconnectEvent`](super::DisconnectEvent).
+ DisconnectEvent,
+ /// See [`HelloEvent`](super::HelloEvent).
+ HelloEvent,
+ /// See [`JoinEvent`](super::JoinEvent).
+ JoinEvent,
+ /// See [`LoginEvent`](super::LoginEvent).
+ LoginEvent,
+ /// See [`LogoutEvent`](super::LogoutEvent).
+ LogoutEvent,
+ /// See [`NetworkEvent`](super::NetworkEvent).
+ NetworkEvent,
+ /// See [`NickEvent`](super::NickEvent).
+ NickEvent,
+ /// See [`EditMessageEvent`](super::EditMessageEvent).
+ EditMessageEvent,
+ /// See [`PartEvent`](super::PartEvent).
+ PartEvent,
+ /// See [`PingEvent`](super::PingEvent).
+ PingEvent,
+ /// See [`PmInitiateEvent`](super::PmInitiateEvent).
+ PmInitiateEvent,
+ /// See [`SendEvent`](super::SendEvent).
+ SendEvent,
+ /// See [`SnapshotEvent`](super::SnapshotEvent).
+ SnapshotEvent,
+
+ // Session commands
+ /// See [`Auth`](super::Auth).
+ Auth,
+ /// See [`AuthReply`](super::AuthReply).
+ AuthReply,
+ /// See [`Ping`](super::Ping).
+ Ping,
+ /// See [`PingReply`](super::PingReply).
+ PingReply,
+
+ // Chat room commands
+ /// See [`GetMessage`](super::GetMessage).
+ GetMessage,
+ /// See [`GetMessageReply`](super::GetMessageReply).
+ GetMessageReply,
+ /// See [`Log`](super::Log).
+ Log,
+ /// See [`LogReply`](super::LogReply).
+ LogReply,
+ /// See [`Nick`](super::Nick).
+ Nick,
+ /// See [`NickReply`](super::NickReply).
+ NickReply,
+ /// See [`PmInitiate`](super::PmInitiate).
+ PmInitiate,
+ /// See [`PmInitiateReply`](super::PmInitiateReply).
+ PmInitiateReply,
+ /// See [`Send`](super::Send).
+ Send,
+ /// See [`SendReply`](super::SendReply).
+ SendReply,
+ /// See [`Who`](super::Who).
+ Who,
+ /// See [`WhoReply`](super::WhoReply).
+ WhoReply,
+
+ // Account commands
+ /// Not implemented.
+ ChangeEmail,
+ /// Not implemented.
+ ChangeEmailReply,
+ /// Not implemented.
+ ChangeName,
+ /// Not implemented.
+ ChangeNameReply,
+ /// Not implemented.
+ ChangePassword,
+ /// Not implemented.
+ ChangePasswordReply,
+ /// Not implemented.
+ Login,
+ /// Not implemented.
+ LoginReply,
+ /// Not implemented.
+ Logout,
+ /// Not implemented.
+ LogoutReply,
+ /// Not implemented.
+ RegisterAccount,
+ /// Not implemented.
+ RegisterAccountReply,
+ /// Not implemented.
+ ResendVerificationEmail,
+ /// Not implemented.
+ ResendVerificationEmailReply,
+ /// Not implemented.
+ ResetPassword,
+ /// Not implemented.
+ ResetPasswordReply,
+
+ // Room host commands
+ /// Not implemented.
+ Ban,
+ /// Not implemented.
+ BanReply,
+ /// Not implemented.
+ EditMessage,
+ /// Not implemented.
+ EditMessageReply,
+ /// Not implemented.
+ GrantAccess,
+ /// Not implemented.
+ GrantAccessReply,
+ /// Not implemented.
+ GrantManager,
+ /// Not implemented.
+ GrantManagerReply,
+ /// Not implemented.
+ RevokeAccess,
+ /// Not implemented.
+ RevokeAccessReply,
+ /// Not implemented.
+ RevokeManager,
+ /// Not implemented.
+ RevokeManagerReply,
+ /// Not implemented.
+ Unban,
+ /// Not implemented.
+ UnbanReply,
+
+ // Staff commands
+ /// Not implemented.
+ StaffCreateRoom,
+ /// Not implemented.
+ StaffCreateRoomReply,
+ /// Not implemented.
+ StaffEnrollOtp,
+ /// Not implemented.
+ StaffEnrollOtpReply,
+ /// Not implemented.
+ StaffGrantManager,
+ /// Not implemented.
+ StaffGrantManagerReply,
+ /// Not implemented.
+ StaffInvade,
+ /// Not implemented.
+ StaffInvadeReply,
+ /// Not implemented.
+ StaffLockRoom,
+ /// Not implemented.
+ StaffLockRoomReply,
+ /// Not implemented.
+ StaffRevokeAccess,
+ /// Not implemented.
+ StaffRevokeAccessReply,
+ /// Not implemented.
+ StaffValidateOtp,
+ /// Not implemented.
+ StaffValidateOtpReply,
+ /// Not implemented.
+ UnlockStaffCapability,
+ /// Not implemented.
+ UnlockStaffCapabilityReply,
+}
+
+impl fmt::Display for PacketType {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match serde_json::to_value(self) {
+ Ok(Value::String(s)) => write!(f, "{}", s),
+ _ => Err(fmt::Error),
+ }
+ }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct Packet {
+ pub id: Option,
+ pub r#type: PacketType,
+ pub data: Option,
+ pub error: Option,
+ #[serde(default)]
+ pub throttled: bool,
+ pub throttled_reason: Option,
+}
+
+pub trait HasPacketType {
+ fn packet_type() -> PacketType;
+}
+
+macro_rules! has_packet_type {
+ ($name:ident) => {
+ impl HasPacketType for $name {
+ fn packet_type() -> PacketType {
+ PacketType::$name
+ }
+ }
+ };
+}
+pub(crate) use has_packet_type;
+
+pub trait ToPacket {
+ fn to_packet(self, id: Option) -> Packet;
+}
+
+impl ToPacket for T {
+ fn to_packet(self, id: Option) -> Packet {
+ Packet {
+ id,
+ r#type: Self::packet_type(),
+ data: Some(serde_json::to_value(self).expect("malformed packet")),
+ error: None,
+ throttled: false,
+ throttled_reason: None,
+ }
+ }
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum DecodeError {
+ #[error("incorrect packet type: expected {expected}, got {actual}")]
+ IncorrectType {
+ expected: PacketType,
+ actual: PacketType,
+ },
+ #[error("throttled: {0}")]
+ Throttled(String),
+ #[error("error: {0}")]
+ Error(String),
+ #[error("no data")]
+ NoData,
+ #[error("{0}")]
+ SerdeJson(#[from] serde_json::Error),
+}
+
+pub trait FromPacket: Sized {
+ fn from_packet(packet: Packet) -> Result;
+}
+
+impl FromPacket for T {
+ fn from_packet(packet: Packet) -> Result {
+ if packet.r#type != Self::packet_type() {
+ Err(DecodeError::IncorrectType {
+ expected: Self::packet_type(),
+ actual: packet.r#type,
+ })
+ } else if packet.throttled {
+ let reason = packet
+ .throttled_reason
+ .unwrap_or_else(|| "no reason given".to_string());
+ Err(DecodeError::Throttled(reason))
+ } else if let Some(error) = packet.error {
+ Err(DecodeError::Error(error))
+ } else {
+ let data = packet.data.unwrap_or_default();
+ Ok(serde_json::from_value(data)?)
+ }
+ }
+}
diff --git a/cove-tui/src/euph/api/room_cmds.rs b/cove-tui/src/euph/api/room_cmds.rs
index 02dddd4..4469478 100644
--- a/cove-tui/src/euph/api/room_cmds.rs
+++ b/cove-tui/src/euph/api/room_cmds.rs
@@ -2,7 +2,7 @@
use serde::{Deserialize, Serialize};
-use super::{Message, SessionView, Snowflake, UserId};
+use super::{has_packet_type, HasPacketType, Message, PacketType, SessionView, Snowflake, UserId};
/// Retrieve the full content of a single message in the room.
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -11,10 +11,14 @@ pub struct GetMessage {
pub id: Snowflake,
}
+has_packet_type!(GetMessage);
+
/// The message retrieved by [`GetMessage`].
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetMessageReply(pub Message);
+has_packet_type!(GetMessageReply);
+
/// Request messages from the room's message log.
///
/// This can be used to supplement the log provided by snapshot-event (for
@@ -27,6 +31,8 @@ pub struct Log {
pub before: Option,
}
+has_packet_type!(Log);
+
/// List of messages from the room's message log.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogReply {
@@ -36,6 +42,8 @@ pub struct LogReply {
pub before: Option,
}
+has_packet_type!(LogReply);
+
/// Set the name you present to the room.
///
/// This name applies to all messages sent during this session, until the nick
@@ -46,6 +54,8 @@ pub struct Nick {
pub name: String,
}
+has_packet_type!(Nick);
+
/// Confirms the [`Nick`] command.
///
/// Returns the session's former and new names (the server may modify the
@@ -62,6 +72,8 @@ pub struct NickReply {
pub to: String,
}
+has_packet_type!(NickReply);
+
/// Constructs a virtual room for private messaging between the client and the
/// given [`UserId`].
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -70,6 +82,8 @@ pub struct PmInitiate {
pub user_id: UserId,
}
+has_packet_type!(PmInitiate);
+
/// Provides the PMID for the requested private messaging room.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PmInitiateReply {
@@ -79,6 +93,8 @@ pub struct PmInitiateReply {
pub to_nick: String,
}
+has_packet_type!(PmInitiateReply);
+
/// Send a message to a room.
///
/// The session must be successfully joined with the room. This message will be
@@ -98,19 +114,27 @@ pub struct Send {
pub parent: Option,
}
+has_packet_type!(Send);
+
/// The message that was sent.
///
/// this includes the message id, which was populated by the server.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SendReply(pub Message);
+has_packet_type!(SendReply);
+
/// Request a list of sessions currently joined in the room.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Who;
+has_packet_type!(Who);
+
/// Lists the sessions currently joined in the room.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WhoReply {
/// A list of session views.
listing: Vec,
}
+
+has_packet_type!(WhoReply);
diff --git a/cove-tui/src/euph/api/session_cmds.rs b/cove-tui/src/euph/api/session_cmds.rs
index e169f7d..8ba846d 100644
--- a/cove-tui/src/euph/api/session_cmds.rs
+++ b/cove-tui/src/euph/api/session_cmds.rs
@@ -2,7 +2,7 @@
use serde::{Deserialize, Serialize};
-use super::{AuthOption, Time};
+use super::{has_packet_type, AuthOption, HasPacketType, PacketType, Time};
/// Attempt to join a private room.
///
@@ -16,6 +16,8 @@ pub struct Auth {
pub passcode: Option,
}
+has_packet_type!(Auth);
+
/// Reports whether the [`Auth`] command succeeded.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthReply {
@@ -25,6 +27,8 @@ pub struct AuthReply {
pub reason: Option,
}
+has_packet_type!(AuthReply);
+
/// Initiate a client-to-server ping.
///
/// The server will send back a [`PingReply`] with the same timestamp as soon as
@@ -35,9 +39,13 @@ pub struct Ping {
pub time: Time,
}
+has_packet_type!(Ping);
+
/// Response to a [`Ping`] command or [`PingEvent`](super::PingEvent).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PingReply {
/// The timestamp of the ping being replied to.
pub time: Option