From 1495095fa868fde4a80cff1275ff138404c9efd2 Mon Sep 17 00:00:00 2001 From: Joscha Date: Thu, 18 Aug 2022 00:46:16 +0200 Subject: [PATCH] Copy euph-api-related parts from cove Also includes the required dependencies and some minor changes to the module structure, as well as all the clippy and rustc warnings. --- Cargo.toml | 13 ++ src/api.rs | 13 ++ src/api/events.rs | 170 +++++++++++++++ src/api/packet.rs | 192 +++++++++++++++++ src/api/room_cmds.rs | 116 ++++++++++ src/api/session_cmds.rs | 43 ++++ src/api/types.rs | 391 +++++++++++++++++++++++++++++++++ src/conn.rs | 466 ++++++++++++++++++++++++++++++++++++++++ src/huehash.rs | 32 +++ src/lib.rs | 32 +-- src/replies.rs | 68 ++++++ 11 files changed, 1524 insertions(+), 12 deletions(-) create mode 100644 src/api.rs create mode 100644 src/api/events.rs create mode 100644 src/api/packet.rs create mode 100644 src/api/room_cmds.rs create mode 100644 src/api/session_cmds.rs create mode 100644 src/api/types.rs create mode 100644 src/conn.rs create mode 100644 src/huehash.rs create mode 100644 src/replies.rs diff --git a/Cargo.toml b/Cargo.toml index 3dcef6f..64caf79 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,3 +4,16 @@ version = "0.1.0" edition = "2021" [dependencies] +anyhow = "1.0.62" # TODO Remove +futures = "0.3.23" +log = "0.4.17" # TODO Remove +rand = "0.8.5" +serde = { version = "1.0.143", features = ["derive"] } +serde_json = "1.0.83" +thiserror = "1.0.32" +time = { version = "0.3.13", features = ["serde"] } +tokio = { version = "1.20.1", features = ["time", "sync", "macros", "rt"] } + +[dependencies.tokio-tungstenite] +version = "0.17.2" +features = ["rustls-tls-native-roots"] diff --git a/src/api.rs b/src/api.rs new file mode 100644 index 0000000..95f76c7 --- /dev/null +++ b/src/api.rs @@ -0,0 +1,13 @@ +//! Models the euphoria API at . + +mod events; +pub mod packet; +mod room_cmds; +mod session_cmds; +mod types; + +pub use events::*; +pub use packet::Data; +pub use room_cmds::*; +pub use session_cmds::*; +pub use types::*; diff --git a/src/api/events.rs b/src/api/events.rs new file mode 100644 index 0000000..1d79b03 --- /dev/null +++ b/src/api/events.rs @@ -0,0 +1,170 @@ +//! Asynchronous events. + +use serde::{Deserialize, Serialize}; + +use super::{AuthOption, Message, PersonalAccountView, SessionView, Snowflake, Time, UserId}; + +/// Indicates that access to a room is denied. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BounceEvent { + /// The reason why access was denied. + pub reason: Option, + /// Authentication options that may be used. + pub auth_options: Option>, + /// Internal use only. + pub agent_id: Option, + /// Internal use only. + pub ip: Option, +} + +/// Indicates that the session is being closed. The client will subsequently be +/// disconnected. +/// +/// If the disconnect reason is `authentication changed`, the client should +/// immediately reconnect. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DisconnectEvent { + /// The reason for disconnection. + pub reason: String, +} + +/// Sent by the server to the client when a session is started. +/// +/// It includes information about the client's authentication and associated +/// identity. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct HelloEvent { + /// The id of the agent or account logged into this session. + pub id: UserId, + /// Details about the user's account, if the session is logged in. + pub account: Option, + /// Details about the session. + pub session: SessionView, + /// If true, then the account has an explicit access grant to the current + /// room. + pub account_has_access: Option, + /// Whether the account's email address has been verified. + pub account_email_verified: Option, + /// If true, the session is connected to a private room. + pub room_is_private: bool, + /// The version of the code being run and served by the server. + pub version: String, +} + +/// Indicates a session just joined the room. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct JoinEvent(pub SessionView); + +/// 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)] +pub struct LoginEvent { + pub account_id: Snowflake, +} + +/// 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; + +/// Indicates some server-side event that impacts the presence of sessions in a +/// room. +/// +/// If the network event type is `partition`, then this should be treated as a +/// [`PartEvent`] for all sessions connected to the same server id/era combo. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NetworkEvent { + /// The type of network event; for now, always `partition`. + pub r#type: String, + /// The id of the affected server. + pub server_id: String, + /// The era of the affected server. + pub server_era: String, +} + +/// Announces a nick change by another session in the room. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NickEvent { + /// The id of the session this name applies to. + pub session_id: String, + /// The id of the agent or account logged into the session. + pub id: UserId, + /// The previous name associated with the session. + pub from: String, + /// The name associated with the session henceforth. + pub to: String, +} + +/// 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 +/// displayed, it should update its display accordingly. +/// +/// The event packet includes a snapshot of the message post-edit. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EditMessageEvent { + /// The id of the edit. + pub edit_id: Snowflake, + /// The snapshot of the message post-edit. + #[serde(flatten)] + pub message: Message, +} + +/// Indicates a session just disconnected from the room. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PartEvent(pub SessionView); + +/// Represents a server-to-client ping. +/// +/// The client should send back a ping-reply with the same value for the time +/// field as soon as possible (or risk disconnection). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PingEvent { + /// A unix timestamp according to the server's clock. + pub time: Time, + /// The expected time of the next ping event, according to the server's + /// clock. + pub next: Time, +} + +/// Informs the client that another user wants to chat with them privately. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PmInitiateEvent { + /// The id of the user inviting the client to chat privately. + pub from: UserId, + /// The nick of the inviting user. + pub from_nick: String, + /// The room where the invitation was sent from. + pub from_room: String, + /// The private chat can be accessed at `/room/pm:`. + pub pm_id: Snowflake, +} + +/// Indicates a message received by the room from another session. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SendEvent(pub Message); + +/// Indicates that a session has successfully joined a room. +/// +/// It also offers a snapshot of the room’s state and recent history. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SnapshotEvent { + /// The id of the agent or account logged into this session. + pub identity: UserId, + /// The globally unique id of this session. + pub session_id: String, + /// The server’s version identifier. + pub version: String, + /// The list of all other sessions joined to the room (excluding this + /// session). + pub listing: Vec, + /// The most recent messages posted to the room (currently up to 100). + pub log: Vec, + /// The acting nick of the session; if omitted, client set nick before + /// speaking. + pub nick: Option, + /// If given, this room is for private chat with the given nick. + pub pm_with_nick: Option, + /// If given, this room is for private chat with the given user. + pub pm_with_user_id: Option, +} diff --git a/src/api/packet.rs b/src/api/packet.rs new file mode 100644 index 0000000..e69de56 --- /dev/null +++ b/src/api/packet.rs @@ -0,0 +1,192 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use super::PacketType; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Packet { + pub id: Option, + pub r#type: PacketType, + pub data: Option, + #[serde(skip_serializing)] + pub error: Option, + #[serde(default, skip_serializing)] + pub throttled: bool, + #[serde(skip_serializing)] + pub throttled_reason: Option, +} + +pub trait Command { + type Reply; +} + +macro_rules! packets { + ( $( $name:ident, )*) => { + #[derive(Debug, Clone)] + #[non_exhaustive] + pub enum Data { + $( $name(super::$name), )* + Unimplemented, + } + + impl Data { + pub fn from_value(ptype: PacketType, value: Value) -> serde_json::Result { + Ok(match ptype { + $( PacketType::$name => Self::$name(serde_json::from_value(value)?), )* + _ => Self::Unimplemented, + }) + } + + pub fn into_value(self) -> serde_json::Result { + 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(_) => PacketType::$name, )* + Self::Unimplemented => panic!("using unimplemented data"), + } + } + } + + $( + impl From for Data { + fn from(p: super::$name) -> Self { + Self::$name(p) + } + } + + impl TryFrom for super::$name{ + type Error = (); + + fn try_from(value: Data) -> Result { + match value { + Data::$name(p) => Ok(p), + _ => Err(()) + } + } + } + )* + }; +} + +macro_rules! commands { + ( $( $cmd:ident => $rpl:ident, )* ) => { + $( + impl Command for super::$cmd { + type Reply = super::$rpl; + } + )* + }; +} + +packets! { + BounceEvent, + DisconnectEvent, + HelloEvent, + JoinEvent, + LoginEvent, + LogoutEvent, + NetworkEvent, + NickEvent, + EditMessageEvent, + PartEvent, + PingEvent, + PmInitiateEvent, + SendEvent, + SnapshotEvent, + Auth, + AuthReply, + Ping, + PingReply, + GetMessage, + GetMessageReply, + Log, + LogReply, + Nick, + NickReply, + PmInitiate, + PmInitiateReply, + Send, + SendReply, + Who, + WhoReply, +} + +commands! { + Auth => AuthReply, + Ping => PingReply, + GetMessage => GetMessageReply, + Log => LogReply, + Nick => NickReply, + PmInitiate => PmInitiateReply, + Send => SendReply, + Who => WhoReply, +} + +#[derive(Debug, Clone)] +pub struct ParsedPacket { + pub id: Option, + pub r#type: PacketType, + pub content: Result, + pub throttled: Option, +} + +impl ParsedPacket { + pub fn from_packet(packet: Packet) -> serde_json::Result { + let id = packet.id; + let r#type = packet.r#type; + + let content = if let Some(error) = packet.error { + Err(error) + } else { + let data = packet.data.unwrap_or_default(); + 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 into_packet(self) -> serde_json::Result { + 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.into_value()?), + error: None, + throttled, + throttled_reason, + }, + Err(error) => Packet { + id, + r#type, + data: None, + error: Some(error), + throttled, + throttled_reason, + }, + }) + } +} diff --git a/src/api/room_cmds.rs b/src/api/room_cmds.rs new file mode 100644 index 0000000..02dddd4 --- /dev/null +++ b/src/api/room_cmds.rs @@ -0,0 +1,116 @@ +//! Chat room commands. + +use serde::{Deserialize, Serialize}; + +use super::{Message, SessionView, Snowflake, UserId}; + +/// Retrieve the full content of a single message in the room. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetMessage { + /// The id of the message to retrieve. + pub id: Snowflake, +} + +/// The message retrieved by [`GetMessage`]. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetMessageReply(pub Message); + +/// Request messages from the room's message log. +/// +/// This can be used to supplement the log provided by snapshot-event (for +/// example, when scrolling back further in history). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Log { + /// Maximum number of messages to return (up to 1000). + pub n: usize, + /// Return messages prior to this snowflake. + pub before: Option, +} + +/// List of messages from the room's message log. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LogReply { + /// List of messages returned. + pub log: Vec, + /// Messages prior to this snowflake were returned. + pub before: Option, +} + +/// Set the name you present to the room. +/// +/// This name applies to all messages sent during this session, until the nick +/// command is called again. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Nick { + /// The requested name (maximum length 36 bytes). + pub name: String, +} + +/// Confirms the [`Nick`] command. +/// +/// Returns the session's former and new names (the server may modify the +/// requested nick). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NickReply { + /// The id of the session this name applies to. + pub session_id: String, + /// The id of the agent or account logged into the session. + pub id: UserId, + /// The previous name associated with the session. + pub from: String, + /// The name associated with the session henceforth. + pub to: String, +} + +/// Constructs a virtual room for private messaging between the client and the +/// given [`UserId`]. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PmInitiate { + /// The id of the user to invite to chat privately. + pub user_id: UserId, +} + +/// Provides the PMID for the requested private messaging room. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PmInitiateReply { + /// The private chat can be accessed at `/room/pm:`. + pub pm_id: Snowflake, + /// The nickname of the recipient of the invitation. + pub to_nick: String, +} + +/// Send a message to a room. +/// +/// The session must be successfully joined with the room. This message will be +/// broadcast to all sessions joined with the room. +/// +/// If the room is private, then the message content will be encrypted before it +/// is stored and broadcast to the rest of the room. +/// +/// The caller of this command will not receive the corresponding +/// [`SendEvent`](super::SendEvent), but will receive the same information in +/// the [`SendReply`]. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Send { + /// The content of the message (client-defined). + pub content: String, + /// The id of the parent message, if any. + pub parent: Option, +} + +/// 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); + +/// Request a list of sessions currently joined in the room. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Who; + +/// Lists the sessions currently joined in the room. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WhoReply { + /// A list of session views. + listing: Vec, +} diff --git a/src/api/session_cmds.rs b/src/api/session_cmds.rs new file mode 100644 index 0000000..e169f7d --- /dev/null +++ b/src/api/session_cmds.rs @@ -0,0 +1,43 @@ +//! Session commands. + +use serde::{Deserialize, Serialize}; + +use super::{AuthOption, Time}; + +/// Attempt to join a private room. +/// +/// This should be sent in response to a bounce event at the beginning of a +/// session. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Auth { + /// The method of authentication. + pub r#type: AuthOption, + /// Use this field for [`AuthOption::Passcode`] authentication. + pub passcode: Option, +} + +/// Reports whether the [`Auth`] command succeeded. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AuthReply { + /// True if authentication succeeded. + pub success: bool, + /// If [`Self::success`] was false, the reason for failure. + pub reason: Option, +} + +/// Initiate a client-to-server ping. +/// +/// The server will send back a [`PingReply`] with the same timestamp as soon as +/// possible. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Ping { + /// An arbitrary value, intended to be a unix timestamp. + pub time: Time, +} + +/// 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