Change euph packet representation

This commit is contained in:
Joscha 2022-06-22 20:32:55 +02:00
parent 03c1fe7f34
commit a4a8174ea3
8 changed files with 506 additions and 415 deletions

View file

@ -1,13 +1,13 @@
//! Models the euphoria API at <http://api.euphoria.io/>. //! Models the euphoria API at <http://api.euphoria.io/>.
mod events; mod events;
mod packet; pub mod packet;
mod room_cmds; mod room_cmds;
mod session_cmds; mod session_cmds;
mod types; mod types;
pub use events::*; pub use events::*;
pub use packet::*; pub use packet::Data;
pub use room_cmds::*; pub use room_cmds::*;
pub use session_cmds::*; pub use session_cmds::*;
pub use types::*; pub use types::*;

View file

@ -2,10 +2,7 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::{ use super::{AuthOption, Message, PersonalAccountView, SessionView, Snowflake, Time, UserId};
has_packet_type, AuthOption, HasPacketType, Message, PacketType, PersonalAccountView,
SessionView, Snowflake, Time, UserId,
};
/// Indicates that access to a room is denied. /// Indicates that access to a room is denied.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@ -20,8 +17,6 @@ pub struct BounceEvent {
pub ip: Option<String>, pub ip: Option<String>,
} }
has_packet_type!(BounceEvent);
/// Indicates that the session is being closed. The client will subsequently be /// Indicates that the session is being closed. The client will subsequently be
/// disconnected. /// disconnected.
/// ///
@ -33,8 +28,6 @@ pub struct DisconnectEvent {
pub reason: String, pub reason: String,
} }
has_packet_type!(DisconnectEvent);
/// Sent by the server to the client when a session is started. /// Sent by the server to the client when a session is started.
/// ///
/// It includes information about the client's authentication and associated /// It includes information about the client's authentication and associated
@ -58,14 +51,10 @@ pub struct HelloEvent {
pub version: String, pub version: String,
} }
has_packet_type!(HelloEvent);
/// Indicates a session just joined the room. /// Indicates a session just joined the room.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JoinEvent(pub SessionView); pub struct JoinEvent(pub SessionView);
has_packet_type!(JoinEvent);
/// Sent to all sessions of an agent when that agent is logged in (except for /// Sent to all sessions of an agent when that agent is logged in (except for
/// the session that issued the login command). /// the session that issued the login command).
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@ -73,15 +62,11 @@ pub struct LoginEvent {
pub account_id: Snowflake, pub account_id: Snowflake,
} }
has_packet_type!(LoginEvent);
/// Sent to all sessions of an agent when that agent is logged out (except for /// Sent to all sessions of an agent when that agent is logged out (except for
/// the session that issued the logout command). /// the session that issued the logout command).
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogoutEvent; pub struct LogoutEvent;
has_packet_type!(LogoutEvent);
/// Indicates some server-side event that impacts the presence of sessions in a /// Indicates some server-side event that impacts the presence of sessions in a
/// room. /// room.
/// ///
@ -97,8 +82,6 @@ pub struct NetworkEvent {
pub server_era: String, pub server_era: String,
} }
has_packet_type!(NetworkEvent);
/// Announces a nick change by another session in the room. /// Announces a nick change by another session in the room.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NickEvent { pub struct NickEvent {
@ -112,8 +95,6 @@ pub struct NickEvent {
pub to: String, pub to: String,
} }
has_packet_type!(NickEvent);
/// Indicates that a message in the room has been modified or deleted. /// 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 /// If the client offers a user interface and the indicated message is currently
@ -129,14 +110,10 @@ pub struct EditMessageEvent {
pub message: Message, pub message: Message,
} }
has_packet_type!(EditMessageEvent);
/// Indicates a session just disconnected from the room. /// Indicates a session just disconnected from the room.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PartEvent(pub SessionView); pub struct PartEvent(pub SessionView);
has_packet_type!(PartEvent);
/// Represents a server-to-client ping. /// Represents a server-to-client ping.
/// ///
/// The client should send back a ping-reply with the same value for the time /// The client should send back a ping-reply with the same value for the time
@ -150,8 +127,6 @@ pub struct PingEvent {
pub next: Time, pub next: Time,
} }
has_packet_type!(PingEvent);
/// Informs the client that another user wants to chat with them privately. /// Informs the client that another user wants to chat with them privately.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PmInitiateEvent { pub struct PmInitiateEvent {
@ -165,14 +140,10 @@ pub struct PmInitiateEvent {
pub pm_id: Snowflake, pub pm_id: Snowflake,
} }
has_packet_type!(PmInitiateEvent);
/// Indicates a message received by the room from another session. /// Indicates a message received by the room from another session.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SendEvent(pub Message); pub struct SendEvent(pub Message);
has_packet_type!(SendEvent);
/// Indicates that a session has successfully joined a room. /// Indicates that a session has successfully joined a room.
/// ///
/// It also offers a snapshot of the rooms state and recent history. /// It also offers a snapshot of the rooms state and recent history.
@ -197,5 +168,3 @@ pub struct SnapshotEvent {
/// If given, this room is for private chat with the given user. /// If given, this room is for private chat with the given user.
pub pm_with_user_id: Option<String>, pub pm_with_user_id: Option<String>,
} }
has_packet_type!(SnapshotEvent);

View file

@ -1,196 +1,7 @@
use std::fmt;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
/// The type of a packet. use super::PacketType;
///
/// 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, thiserror::Error)]
pub enum PacketError {
#[error("throttled: {0}")]
Throttled(String),
#[error("error: {0}")]
Error(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Packet { pub struct Packet {
@ -203,80 +14,206 @@ pub struct Packet {
pub throttled_reason: Option<String>, pub throttled_reason: Option<String>,
} }
impl Packet { pub trait Command {
pub fn data(self) -> Result<Value, PacketError> { type Reply;
if self.throttled { }
let reason = self
.throttled_reason macro_rules! packets {
.unwrap_or_else(|| "no reason given".to_string()); ( $( $name:ident, )*) => {
Err(PacketError::Throttled(reason)) #[derive(Debug, Clone)]
} else if let Some(error) = self.error { pub enum Data {
Err(PacketError::Error(error)) $( $name(super::$name), )*
} else { Unimplemented,
Ok(self.data.unwrap_or_default())
} }
}
impl Data {
pub fn from_value(ptype: PacketType, value: Value) -> serde_json::Result<Self> {
Ok(match ptype {
$( PacketType::$name => Self::$name(serde_json::from_value(value)?), )*
_ => Self::Unimplemented,
})
}
pub fn to_value(self) -> serde_json::Result<Value> {
Ok(match self{
$( Self::$name(p) => serde_json::to_value(p)?, )*
Self::Unimplemented => panic!("using unimplemented data"),
})
}
pub fn packet_type(&self) -> PacketType {
match self {
$( Self::$name(p) => PacketType::$name, )*
Self::Unimplemented => panic!("using unimplemented data"),
}
}
}
$(
impl From<super::$name> for Data {
fn from(p: super::$name) -> Self {
Self::$name(p)
}
}
impl TryFrom<Data> for super::$name{
type Error = ();
fn try_from(value: Data) -> Result<Self, Self::Error> {
match value {
Data::$name(p) => Ok(p),
_ => Err(())
}
}
}
)*
};
} }
pub trait HasPacketType { macro_rules! events {
fn packet_type() -> PacketType; ( $( $name:ident, )* ) => {
} impl Data {
pub fn is_event(&self) -> bool {
macro_rules! has_packet_type { match self {
($name:ident) => { $( Self::$name(_) => true, )*
impl HasPacketType for $name { _ => false,
fn packet_type() -> PacketType { }
PacketType::$name
} }
} }
}; };
} }
pub(crate) use has_packet_type;
pub trait ToPacket { macro_rules! commands {
fn to_packet(self, id: Option<String>) -> Packet; ( $( $cmd:ident => $rpl:ident, )* ) => {
$(
impl Command for super::$cmd {
type Reply = super::$rpl;
}
)*
};
} }
impl<T: HasPacketType + Serialize> ToPacket for T { packets! {
fn to_packet(self, id: Option<String>) -> Packet { BounceEvent,
Packet { DisconnectEvent,
id, HelloEvent,
r#type: Self::packet_type(), JoinEvent,
data: Some(serde_json::to_value(self).expect("malformed packet")), LoginEvent,
error: None, LogoutEvent,
throttled: false, NetworkEvent,
throttled_reason: None, NickEvent,
} EditMessageEvent,
} PartEvent,
PingEvent,
PmInitiateEvent,
SendEvent,
SnapshotEvent,
Auth,
AuthReply,
Ping,
PingReply,
GetMessage,
GetMessageReply,
Log,
LogReply,
Nick,
NickReply,
PmInitiate,
PmInitiateReply,
Send,
SendReply,
Who,
WhoReply,
} }
#[derive(Debug, thiserror::Error)] events! {
pub enum DecodeError { BounceEvent,
#[error("incorrect packet type: expected {expected}, got {actual}")] DisconnectEvent,
IncorrectType { HelloEvent,
expected: PacketType, JoinEvent,
actual: PacketType, LoginEvent,
}, LogoutEvent,
#[error("{0}")] NetworkEvent,
SerdeJson(#[from] serde_json::Error), NickEvent,
#[error("{0}")] EditMessageEvent,
Packet(#[from] PacketError), PartEvent,
PingEvent,
PmInitiateEvent,
SendEvent,
SnapshotEvent,
} }
pub trait FromPacket: Sized { commands! {
fn from_packet(packet: Packet) -> Result<Self, DecodeError>; Auth => AuthReply,
Ping => PingReply,
GetMessage => GetMessageReply,
Log => LogReply,
Nick => NickReply,
PmInitiate => PmInitiateReply,
Send => SendReply,
Who => WhoReply,
} }
impl<T: HasPacketType + DeserializeOwned> FromPacket for T { #[derive(Debug, Clone)]
fn from_packet(packet: Packet) -> Result<Self, DecodeError> { pub struct ParsedPacket {
if packet.r#type != Self::packet_type() { pub id: Option<String>,
Err(DecodeError::IncorrectType { pub r#type: PacketType,
expected: Self::packet_type(), pub content: Result<Data, String>,
actual: packet.r#type, pub throttled: Option<String>,
}) }
impl ParsedPacket {
pub fn from_packet(packet: Packet) -> serde_json::Result<Self> {
let id = packet.id;
let r#type = packet.r#type;
let content = if let Some(error) = packet.error {
Err(error)
} else { } else {
let data = packet.data()?; let data = packet.data.unwrap_or_default();
Ok(serde_json::from_value(data)?) Ok(Data::from_value(r#type, data)?)
} };
let throttled = if packet.throttled {
let reason = packet
.throttled_reason
.unwrap_or_else(|| "no reason given".to_string());
Some(reason)
} else {
None
};
Ok(Self {
id,
r#type,
content,
throttled,
})
}
pub fn to_packet(self) -> serde_json::Result<Packet> {
let id = self.id;
let r#type = self.r#type;
let throttled = self.throttled.is_some();
let throttled_reason = self.throttled;
Ok(match self.content {
Ok(data) => Packet {
id,
r#type,
data: Some(data.to_value()?),
error: None,
throttled,
throttled_reason,
},
Err(error) => Packet {
id,
r#type,
data: None,
error: Some(error),
throttled,
throttled_reason,
},
})
} }
} }

View file

@ -2,7 +2,7 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::{has_packet_type, HasPacketType, Message, PacketType, SessionView, Snowflake, UserId}; use super::{Message, SessionView, Snowflake, UserId};
/// Retrieve the full content of a single message in the room. /// Retrieve the full content of a single message in the room.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@ -11,14 +11,10 @@ pub struct GetMessage {
pub id: Snowflake, pub id: Snowflake,
} }
has_packet_type!(GetMessage);
/// The message retrieved by [`GetMessage`]. /// The message retrieved by [`GetMessage`].
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetMessageReply(pub Message); pub struct GetMessageReply(pub Message);
has_packet_type!(GetMessageReply);
/// Request messages from the room's message log. /// Request messages from the room's message log.
/// ///
/// This can be used to supplement the log provided by snapshot-event (for /// This can be used to supplement the log provided by snapshot-event (for
@ -31,8 +27,6 @@ pub struct Log {
pub before: Option<Snowflake>, pub before: Option<Snowflake>,
} }
has_packet_type!(Log);
/// List of messages from the room's message log. /// List of messages from the room's message log.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogReply { pub struct LogReply {
@ -42,8 +36,6 @@ pub struct LogReply {
pub before: Option<Snowflake>, pub before: Option<Snowflake>,
} }
has_packet_type!(LogReply);
/// Set the name you present to the room. /// Set the name you present to the room.
/// ///
/// This name applies to all messages sent during this session, until the nick /// This name applies to all messages sent during this session, until the nick
@ -54,8 +46,6 @@ pub struct Nick {
pub name: String, pub name: String,
} }
has_packet_type!(Nick);
/// Confirms the [`Nick`] command. /// Confirms the [`Nick`] command.
/// ///
/// Returns the session's former and new names (the server may modify the /// Returns the session's former and new names (the server may modify the
@ -72,8 +62,6 @@ pub struct NickReply {
pub to: String, pub to: String,
} }
has_packet_type!(NickReply);
/// Constructs a virtual room for private messaging between the client and the /// Constructs a virtual room for private messaging between the client and the
/// given [`UserId`]. /// given [`UserId`].
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@ -82,8 +70,6 @@ pub struct PmInitiate {
pub user_id: UserId, pub user_id: UserId,
} }
has_packet_type!(PmInitiate);
/// Provides the PMID for the requested private messaging room. /// Provides the PMID for the requested private messaging room.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PmInitiateReply { pub struct PmInitiateReply {
@ -93,8 +79,6 @@ pub struct PmInitiateReply {
pub to_nick: String, pub to_nick: String,
} }
has_packet_type!(PmInitiateReply);
/// Send a message to a room. /// Send a message to a room.
/// ///
/// The session must be successfully joined with the room. This message will be /// The session must be successfully joined with the room. This message will be
@ -114,27 +98,19 @@ pub struct Send {
pub parent: Option<Snowflake>, pub parent: Option<Snowflake>,
} }
has_packet_type!(Send);
/// The message that was sent. /// The message that was sent.
/// ///
/// this includes the message id, which was populated by the server. /// this includes the message id, which was populated by the server.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SendReply(pub Message); pub struct SendReply(pub Message);
has_packet_type!(SendReply);
/// Request a list of sessions currently joined in the room. /// Request a list of sessions currently joined in the room.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Who; pub struct Who;
has_packet_type!(Who);
/// Lists the sessions currently joined in the room. /// Lists the sessions currently joined in the room.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WhoReply { pub struct WhoReply {
/// A list of session views. /// A list of session views.
listing: Vec<SessionView>, listing: Vec<SessionView>,
} }
has_packet_type!(WhoReply);

View file

@ -2,7 +2,7 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::{has_packet_type, AuthOption, HasPacketType, PacketType, Time}; use super::{AuthOption, Time};
/// Attempt to join a private room. /// Attempt to join a private room.
/// ///
@ -16,8 +16,6 @@ pub struct Auth {
pub passcode: Option<String>, pub passcode: Option<String>,
} }
has_packet_type!(Auth);
/// Reports whether the [`Auth`] command succeeded. /// Reports whether the [`Auth`] command succeeded.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthReply { pub struct AuthReply {
@ -27,8 +25,6 @@ pub struct AuthReply {
pub reason: Option<String>, pub reason: Option<String>,
} }
has_packet_type!(AuthReply);
/// Initiate a client-to-server ping. /// Initiate a client-to-server ping.
/// ///
/// The server will send back a [`PingReply`] with the same timestamp as soon as /// The server will send back a [`PingReply`] with the same timestamp as soon as
@ -39,13 +35,9 @@ pub struct Ping {
pub time: Time, pub time: Time,
} }
has_packet_type!(Ping);
/// Response to a [`Ping`] command or [`PingEvent`](super::PingEvent). /// Response to a [`Ping`] command or [`PingEvent`](super::PingEvent).
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PingReply { pub struct PingReply {
/// The timestamp of the ping being replied to. /// The timestamp of the ping being replied to.
pub time: Option<Time>, pub time: Option<Time>,
} }
has_packet_type!(PingReply);

View file

@ -10,6 +10,7 @@ use std::fmt;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{de, ser, Deserialize, Serialize}; use serde::{de, ser, Deserialize, Serialize};
use serde_json::Value;
/// Describes an account and its preferred name. /// Describes an account and its preferred name.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@ -61,6 +62,186 @@ pub struct Message {
pub truncated: bool, pub truncated: bool,
} }
/// 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),
}
}
}
/// Describes an account to its owner. /// Describes an account to its owner.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PersonalAccountView { pub struct PersonalAccountView {

View file

@ -15,12 +15,11 @@ use tokio::sync::mpsc;
use tokio::{select, task, time}; use tokio::{select, task, time};
use tokio_tungstenite::{tungstenite, MaybeTlsStream, WebSocketStream}; use tokio_tungstenite::{tungstenite, MaybeTlsStream, WebSocketStream};
use crate::replies::{self, Replies}; use crate::replies::{self, PendingReply, Replies};
use super::api::packet::{Command, Packet, ParsedPacket};
use super::api::{ use super::api::{
BounceEvent, FromPacket, HelloEvent, JoinEvent, NetworkEvent, NickEvent, NickReply, Packet, BounceEvent, Data, HelloEvent, PersonalAccountView, Ping, PingReply, SnapshotEvent,
PacketType, PartEvent, PersonalAccountView, Ping, PingEvent, PingReply, SendEvent,
SnapshotEvent, ToPacket,
}; };
use super::{SessionView, Time, UserId}; use super::{SessionView, Time, UserId};
@ -32,16 +31,34 @@ pub enum Error {
ConnectionClosed, ConnectionClosed,
#[error("packet timed out")] #[error("packet timed out")]
TimedOut, TimedOut,
#[error("incorrect reply type")]
IncorrectReplyType,
#[error("{0}")]
Euph(String),
} }
#[derive(Debug)] #[derive(Debug)]
enum Event { enum Event {
Message(tungstenite::Message), Message(tungstenite::Message),
Send(Packet, oneshot::Sender<Result<Packet, Error>>), SendCmd(Data, oneshot::Sender<PendingReply<Result<Data, String>>>),
SendRpl(Option<String>, Data),
Status(oneshot::Sender<Status>), Status(oneshot::Sender<Status>),
DoPings, DoPings,
} }
impl Event {
fn send_cmd<C: Into<Data>>(
cmd: C,
rpl: oneshot::Sender<PendingReply<Result<Data, String>>>,
) -> Self {
Self::SendCmd(cmd.into(), rpl)
}
fn send_rpl<C: Into<Data>>(id: Option<String>, rpl: C) -> Self {
Self::SendRpl(id, rpl.into())
}
}
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct Joining { pub struct Joining {
hello: Option<HelloEvent>, hello: Option<HelloEvent>,
@ -50,11 +67,11 @@ pub struct Joining {
} }
impl Joining { impl Joining {
fn on_packet(&mut self, packet: Packet) -> anyhow::Result<()> { fn on_data(&mut self, data: Data) -> anyhow::Result<()> {
match packet.r#type { match data {
PacketType::BounceEvent => self.bounce = Some(BounceEvent::from_packet(packet)?), Data::BounceEvent(p) => self.bounce = Some(p),
PacketType::HelloEvent => self.hello = Some(HelloEvent::from_packet(packet)?), Data::HelloEvent(p) => self.hello = Some(p),
PacketType::SnapshotEvent => self.snapshot = Some(SnapshotEvent::from_packet(packet)?), Data::SnapshotEvent(p) => self.snapshot = Some(p),
_ => {} _ => {}
} }
Ok(()) Ok(())
@ -87,43 +104,32 @@ pub struct Joined {
} }
impl Joined { impl Joined {
fn on_packet(&mut self, packet: Packet) -> anyhow::Result<()> { fn on_data(&mut self, data: Data) -> anyhow::Result<()> {
match packet.r#type { match data {
PacketType::JoinEvent => { Data::JoinEvent(p) => {
let packet = JoinEvent::from_packet(packet)?; self.listing.insert(p.0.id.clone(), p.0);
self.listing.insert(packet.0.id.clone(), packet.0);
} }
PacketType::SendEvent => { Data::SendEvent(p) => {
let packet = SendEvent::from_packet(packet)?; self.listing.insert(p.0.sender.id.clone(), p.0.sender);
self.listing
.insert(packet.0.sender.id.clone(), packet.0.sender);
} }
PacketType::PartEvent => { Data::PartEvent(p) => {
let packet = PartEvent::from_packet(packet)?; self.listing.remove(&p.0.id);
self.listing.remove(&packet.0.id);
} }
PacketType::NetworkEvent => { Data::NetworkEvent(p) => {
let p = NetworkEvent::from_packet(packet)?;
if p.r#type == "partition" { if p.r#type == "partition" {
self.listing.retain(|_, s| { self.listing.retain(|_, s| {
!(s.server_id == p.server_id && s.server_era == p.server_era) !(s.server_id == p.server_id && s.server_era == p.server_era)
}); });
} }
} }
PacketType::NickEvent => { Data::NickEvent(p) => {
let packet = NickEvent::from_packet(packet)?; if let Some(session) = self.listing.get_mut(&p.id) {
if let Some(session) = self.listing.get_mut(&packet.id) { session.name = p.to;
session.name = packet.to;
} }
} }
PacketType::NickReply => { Data::NickReply(p) => {
// Since this is a reply, it may contain errors, for example if assert_eq!(self.session.id, p.id);
// the user specified an invalid nick. We can't just die if that self.session.name = p.to;
// happens, so we ignore the error case.
if let Ok(packet) = NickReply::from_packet(packet) {
assert_eq!(self.session.id, packet.id);
self.session.name = packet.to;
}
} }
// The who reply is broken and can't be trusted right now, so we'll // The who reply is broken and can't be trusted right now, so we'll
// not even look at it. // not even look at it.
@ -142,9 +148,9 @@ pub enum Status {
struct State { struct State {
ws_tx: SplitSink<WsStream, tungstenite::Message>, ws_tx: SplitSink<WsStream, tungstenite::Message>,
last_id: usize, last_id: usize,
replies: Replies<String, Packet>, replies: Replies<String, Result<Data, String>>,
packet_tx: mpsc::UnboundedSender<Packet>, packet_tx: mpsc::UnboundedSender<Data>,
last_ws_ping: Option<Vec<u8>>, last_ws_ping: Option<Vec<u8>>,
last_ws_pong: Option<Vec<u8>>, last_ws_pong: Option<Vec<u8>>,
@ -161,7 +167,7 @@ impl State {
rx_canary: oneshot::Receiver<Infallible>, rx_canary: oneshot::Receiver<Infallible>,
event_tx: mpsc::UnboundedSender<Event>, event_tx: mpsc::UnboundedSender<Event>,
mut event_rx: mpsc::UnboundedReceiver<Event>, mut event_rx: mpsc::UnboundedReceiver<Event>,
packet_tx: mpsc::UnboundedSender<Packet>, packet_tx: mpsc::UnboundedSender<Data>,
) { ) {
let (ws_tx, mut ws_rx) = ws.split(); let (ws_tx, mut ws_rx) = ws.split();
let state = Self { let state = Self {
@ -208,7 +214,8 @@ impl State {
while let Some(ev) = event_rx.recv().await { while let Some(ev) = event_rx.recv().await {
match ev { match ev {
Event::Message(msg) => self.on_msg(msg, event_tx)?, Event::Message(msg) => self.on_msg(msg, event_tx)?,
Event::Send(packet, reply_tx) => self.on_send(packet, reply_tx).await?, Event::SendCmd(data, reply_tx) => self.on_send_cmd(data, reply_tx).await?,
Event::SendRpl(id, data) => self.on_send_rpl(id, data).await?,
Event::Status(reply_tx) => self.on_status(reply_tx), Event::Status(reply_tx) => self.on_status(reply_tx),
Event::DoPings => self.do_pings(event_tx).await?, Event::DoPings => self.do_pings(event_tx).await?,
} }
@ -237,62 +244,84 @@ impl State {
packet: Packet, packet: Packet,
event_tx: &mpsc::UnboundedSender<Event>, event_tx: &mpsc::UnboundedSender<Event>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
if packet.r#type == PacketType::PingReply { let packet = ParsedPacket::from_packet(packet)?;
let packet = PingReply::from_packet(packet.clone())?;
self.last_euph_pong = packet.time;
} else if packet.r#type == PacketType::PingEvent {
let time = Some(PingEvent::from_packet(packet.clone())?.time);
Self::send_unconditionally(event_tx, PingReply { time }, packet.id.clone())?;
}
// Complete pending replies if the packet has an id
if let Some(id) = &packet.id { if let Some(id) = &packet.id {
self.replies.complete(id, packet.clone()); self.replies.complete(id, packet.content.clone());
} }
self.packet_tx.send(packet.clone())?; // Shovel events into self.packet_tx, assuming that no event ever
// errors. Events with errors are simply ignored.
// TODO Handle disconnect event? if let Ok(data) = &packet.content {
if data.is_event() {
match &mut self.status { self.packet_tx.send(data.clone())?;
Status::Joining(joining) => { }
joining.on_packet(packet)?; }
if let Some(joined) = joining.joined() {
self.status = Status::Joined(joined); // Play a game of table tennis
} match &packet.content {
Ok(Data::PingReply(p)) => self.last_euph_pong = p.time,
Ok(Data::PingEvent(p)) => {
let reply = PingReply { time: Some(p.time) };
event_tx.send(Event::send_rpl(packet.id.clone(), reply))?;
}
// TODO Handle disconnect event?
_ => {}
}
// Update internal state
if let Ok(data) = packet.content {
match &mut self.status {
Status::Joining(joining) => {
joining.on_data(data)?;
if let Some(joined) = joining.joined() {
self.status = Status::Joined(joined);
}
}
Status::Joined(joined) => joined.on_data(data)?,
} }
Status::Joined(joined) => joined.on_packet(packet)?,
} }
Ok(()) Ok(())
} }
async fn on_send( async fn on_send_cmd(
&mut self, &mut self,
mut packet: Packet, data: Data,
reply_tx: oneshot::Sender<Result<Packet, Error>>, reply_tx: oneshot::Sender<PendingReply<Result<Data, String>>>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let id = if let Some(id) = packet.id.clone() { // Overkill of universe-heat-death-like proportions
id self.last_id = self.last_id.wrapping_add(1);
} else { let id = format!("{}", self.last_id);
// Overkill of universe-heat-death-like proportions
self.last_id = self.last_id.wrapping_add(1);
format!("{}", self.last_id)
};
packet.id = Some(id.clone());
let pending_reply = self.replies.wait_for(id); let packet = ParsedPacket {
id: Some(id.clone()),
r#type: data.packet_type(),
content: Ok(data),
throttled: None,
}
.to_packet()?;
let msg = tungstenite::Message::Text(serde_json::to_string(&packet)?); let msg = tungstenite::Message::Text(serde_json::to_string(&packet)?);
self.ws_tx.send(msg).await?; self.ws_tx.send(msg).await?;
let reply = match pending_reply.get().await { let _ = reply_tx.send(self.replies.wait_for(id));
Ok(reply) => Ok(reply),
Err(replies::Error::TimedOut) => Err(Error::TimedOut), Ok(())
// We could also send an Error::ConnectionClosed here, but that }
// happens automatically in the send function once we drop reply_tx.
Err(replies::Error::Canceled) => return Ok(()), async fn on_send_rpl(&mut self, id: Option<String>, data: Data) -> anyhow::Result<()> {
}; let packet = ParsedPacket {
let _ = reply_tx.send(reply); id,
r#type: data.packet_type(),
content: Ok(data),
throttled: None,
}
.to_packet()?;
let msg = tungstenite::Message::Text(serde_json::to_string(&packet)?);
self.ws_tx.send(msg).await?;
Ok(()) Ok(())
} }
@ -321,18 +350,9 @@ impl State {
// Send new euph ping // Send new euph ping
let euph_payload = Time(Utc::now()); let euph_payload = Time(Utc::now());
Self::send_unconditionally(event_tx, Ping { time: euph_payload }, None)?;
Ok(())
}
fn send_unconditionally<T: ToPacket>(
event_tx: &mpsc::UnboundedSender<Event>,
packet: T,
id: Option<String>,
) -> anyhow::Result<()> {
let (tx, _) = oneshot::channel(); let (tx, _) = oneshot::channel();
event_tx.send(Event::Send(packet.to_packet(id), tx))?; event_tx.send(Event::send_cmd(Ping { time: euph_payload }, tx))?;
Ok(()) Ok(())
} }
} }
@ -343,16 +363,30 @@ pub struct ConnTx {
} }
impl ConnTx { impl ConnTx {
pub async fn send<T: ToPacket>(&self, packet: T) -> Result<Packet, Error> { pub async fn send<C>(&self, cmd: C) -> Result<C::Reply, Error>
where
C: Command + Into<Data>,
C::Reply: TryFrom<Data, Error = ()>,
{
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
let event = Event::Send(packet.to_packet(None), tx);
self.event_tx self.event_tx
.send(event) .send(Event::SendCmd(cmd.into(), tx))
.map_err(|_| Error::ConnectionClosed)?; .map_err(|_| Error::ConnectionClosed)?;
match rx.await { let pending_reply = rx
Ok(result) => result, .await
Err(_) => Err(Error::ConnectionClosed), // This should only happen if something goes wrong during encoding
} // of the packet or while sending it through the websocket. Assuming
// the first doesn't happen, the connection is probably closed.
.map_err(|_| Error::ConnectionClosed)?;
let data = pending_reply
.get()
.await
.map_err(|e| match e {
replies::Error::TimedOut => Error::TimedOut,
replies::Error::Canceled => Error::ConnectionClosed,
})?
.map_err(Error::Euph)?;
data.try_into().map_err(|_| Error::IncorrectReplyType)
} }
pub async fn status(&self) -> Result<Status, Error> { pub async fn status(&self) -> Result<Status, Error> {
@ -366,11 +400,11 @@ impl ConnTx {
pub struct ConnRx { pub struct ConnRx {
canary: oneshot::Sender<Infallible>, canary: oneshot::Sender<Infallible>,
packet_rx: mpsc::UnboundedReceiver<Packet>, packet_rx: mpsc::UnboundedReceiver<Data>,
} }
impl ConnRx { impl ConnRx {
pub async fn recv(&mut self) -> Result<Packet, Error> { pub async fn recv(&mut self) -> Result<Data, Error> {
self.packet_rx.recv().await.ok_or(Error::ConnectionClosed) self.packet_rx.recv().await.ok_or(Error::ConnectionClosed)
} }
} }

View file

@ -16,6 +16,7 @@ pub enum Error {
pub type Result<T> = result::Result<T, Error>; pub type Result<T> = result::Result<T, Error>;
#[derive(Debug)]
pub struct PendingReply<R> { pub struct PendingReply<R> {
timeout: Duration, timeout: Duration,
result: Receiver<R>, result: Receiver<R>,
@ -32,6 +33,7 @@ impl<R> PendingReply<R> {
} }
} }
#[derive(Debug)]
pub struct Replies<I, R> { pub struct Replies<I, R> {
timeout: Duration, timeout: Duration,
pending: HashMap<I, Sender<R>>, pending: HashMap<I, Sender<R>>,