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.
This commit is contained in:
parent
629ce3ceb9
commit
1495095fa8
11 changed files with 1524 additions and 12 deletions
13
Cargo.toml
13
Cargo.toml
|
|
@ -4,3 +4,16 @@ version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[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"]
|
||||||
|
|
|
||||||
13
src/api.rs
Normal file
13
src/api.rs
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
//! Models the euphoria API at <http://api.euphoria.io/>.
|
||||||
|
|
||||||
|
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::*;
|
||||||
170
src/api/events.rs
Normal file
170
src/api/events.rs
Normal file
|
|
@ -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<String>,
|
||||||
|
/// Authentication options that may be used.
|
||||||
|
pub auth_options: Option<Vec<AuthOption>>,
|
||||||
|
/// Internal use only.
|
||||||
|
pub agent_id: Option<UserId>,
|
||||||
|
/// Internal use only.
|
||||||
|
pub ip: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<PersonalAccountView>,
|
||||||
|
/// 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<bool>,
|
||||||
|
/// Whether the account's email address has been verified.
|
||||||
|
pub account_email_verified: Option<bool>,
|
||||||
|
/// 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:<pm_id>`.
|
||||||
|
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<SessionView>,
|
||||||
|
/// The most recent messages posted to the room (currently up to 100).
|
||||||
|
pub log: Vec<Message>,
|
||||||
|
/// The acting nick of the session; if omitted, client set nick before
|
||||||
|
/// speaking.
|
||||||
|
pub nick: Option<String>,
|
||||||
|
/// If given, this room is for private chat with the given nick.
|
||||||
|
pub pm_with_nick: Option<String>,
|
||||||
|
/// If given, this room is for private chat with the given user.
|
||||||
|
pub pm_with_user_id: Option<String>,
|
||||||
|
}
|
||||||
192
src/api/packet.rs
Normal file
192
src/api/packet.rs
Normal file
|
|
@ -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<String>,
|
||||||
|
pub r#type: PacketType,
|
||||||
|
pub data: Option<Value>,
|
||||||
|
#[serde(skip_serializing)]
|
||||||
|
pub error: Option<String>,
|
||||||
|
#[serde(default, skip_serializing)]
|
||||||
|
pub throttled: bool,
|
||||||
|
#[serde(skip_serializing)]
|
||||||
|
pub throttled_reason: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Self> {
|
||||||
|
Ok(match ptype {
|
||||||
|
$( PacketType::$name => Self::$name(serde_json::from_value(value)?), )*
|
||||||
|
_ => Self::Unimplemented,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_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(_) => 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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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<String>,
|
||||||
|
pub r#type: PacketType,
|
||||||
|
pub content: Result<Data, String>,
|
||||||
|
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 {
|
||||||
|
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<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.into_value()?),
|
||||||
|
error: None,
|
||||||
|
throttled,
|
||||||
|
throttled_reason,
|
||||||
|
},
|
||||||
|
Err(error) => Packet {
|
||||||
|
id,
|
||||||
|
r#type,
|
||||||
|
data: None,
|
||||||
|
error: Some(error),
|
||||||
|
throttled,
|
||||||
|
throttled_reason,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
116
src/api/room_cmds.rs
Normal file
116
src/api/room_cmds.rs
Normal file
|
|
@ -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<Snowflake>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List of messages from the room's message log.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct LogReply {
|
||||||
|
/// List of messages returned.
|
||||||
|
pub log: Vec<Message>,
|
||||||
|
/// Messages prior to this snowflake were returned.
|
||||||
|
pub before: Option<Snowflake>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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:<pm_id>`.
|
||||||
|
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<Snowflake>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<SessionView>,
|
||||||
|
}
|
||||||
43
src/api/session_cmds.rs
Normal file
43
src/api/session_cmds.rs
Normal file
|
|
@ -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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<Time>,
|
||||||
|
}
|
||||||
391
src/api/types.rs
Normal file
391
src/api/types.rs
Normal file
|
|
@ -0,0 +1,391 @@
|
||||||
|
//! Field types.
|
||||||
|
|
||||||
|
// TODO Add newtype wrappers for different kinds of IDs?
|
||||||
|
|
||||||
|
// Serde's derive macros generate this warning and I can't turn it off locally,
|
||||||
|
// so I'm turning it off for the entire module.
|
||||||
|
#![allow(clippy::use_self)]
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use serde::{de, ser, Deserialize, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
|
/// Describes an account and its preferred name.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct AccountView {
|
||||||
|
/// The id of the account.
|
||||||
|
pub id: Snowflake,
|
||||||
|
/// The name that the holder of the account goes by.
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mode of authentication.
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub enum AuthOption {
|
||||||
|
/// Authentication with a passcode, where a key is derived from the passcode
|
||||||
|
/// to unlock an access grant.
|
||||||
|
Passcode,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A node in a room's log.
|
||||||
|
///
|
||||||
|
/// It corresponds to a chat message, or a post, or any broadcasted event in a
|
||||||
|
/// room that should appear in the log.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Message {
|
||||||
|
/// The id of the message (unique within a room).
|
||||||
|
pub id: Snowflake,
|
||||||
|
/// The id of the message's parent, or null if top-level.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub parent: Option<Snowflake>,
|
||||||
|
/// The edit id of the most recent edit of this message, or null if it's
|
||||||
|
/// never been edited.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub previous_edit_id: Option<Snowflake>,
|
||||||
|
/// The unix timestamp of when the message was posted.
|
||||||
|
pub time: Time,
|
||||||
|
/// The view of the sender's session.
|
||||||
|
pub sender: SessionView,
|
||||||
|
/// The content of the message (client-defined).
|
||||||
|
pub content: String,
|
||||||
|
/// The id of the key that encrypts the message in storage.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub encryption_key_id: Option<String>,
|
||||||
|
/// The unix timestamp of when the message was last edited.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub edited: Option<Time>,
|
||||||
|
/// The unix timestamp of when the message was deleted.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub deleted: Option<Time>,
|
||||||
|
/// If true, then the full content of this message is not included (see
|
||||||
|
/// [`GetMessage`](super::GetMessage) to obtain the message with full
|
||||||
|
/// content).
|
||||||
|
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||||
|
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.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct PersonalAccountView {
|
||||||
|
/// The id of the account.
|
||||||
|
pub id: Snowflake,
|
||||||
|
/// The name that the holder of the account goes by.
|
||||||
|
pub name: String,
|
||||||
|
/// The account's email address.
|
||||||
|
pub email: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Describes a session and its identity.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct SessionView {
|
||||||
|
/// The id of an agent or account (or bot).
|
||||||
|
pub id: UserId,
|
||||||
|
/// The name-in-use at the time this view was captured.
|
||||||
|
pub name: String,
|
||||||
|
/// The id of the server that captured this view.
|
||||||
|
pub server_id: String,
|
||||||
|
/// The era of the server that captured this view.
|
||||||
|
pub server_era: String,
|
||||||
|
/// Id of the session, unique across all sessions globally.
|
||||||
|
pub session_id: String,
|
||||||
|
/// If true, this session belongs to a member of staff.
|
||||||
|
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||||
|
pub is_staff: bool,
|
||||||
|
/// If true, this session belongs to a manager of the room.
|
||||||
|
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||||
|
pub is_manager: bool,
|
||||||
|
/// For hosts and staff, the virtual address of the client.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub client_address: Option<String>,
|
||||||
|
/// For staff, the real address of the client.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub real_client_address: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A 13-character string, usually used as aunique identifier for some type of object.
|
||||||
|
///
|
||||||
|
/// It is the base-36 encoding of an unsigned, 64-bit integer.
|
||||||
|
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct Snowflake(pub u64);
|
||||||
|
|
||||||
|
impl Snowflake {
|
||||||
|
/// Maximum possible snowflake that can be safely handled by all of cove's
|
||||||
|
/// parts.
|
||||||
|
///
|
||||||
|
/// In theory, euphoria's snowflakes are 64-bit values and can take
|
||||||
|
/// advantage of the full range. However, sqlite always stores integers as
|
||||||
|
/// signed, and uses a maximum of 8 bytes (64 bits). Because of this, using
|
||||||
|
/// [`u64::MAX`] here would lead to errors in some database interactions.
|
||||||
|
///
|
||||||
|
/// For this reason, I'm limiting snowflakes to the range from `0` to
|
||||||
|
/// [`i64::MAX`]. The euphoria backend isn't likely to change its
|
||||||
|
/// representation of message ids to suddenly use the upper parts of the
|
||||||
|
/// range, and since message ids mostly consist of a timestamp, this
|
||||||
|
/// approach should last until at least 2075.
|
||||||
|
pub const MAX: Self = Snowflake(i64::MAX as u64);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for Snowflake {
|
||||||
|
fn serialize<S: ser::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||||
|
// Convert u64 to base36 string
|
||||||
|
let mut n = self.0;
|
||||||
|
let mut result = String::with_capacity(13);
|
||||||
|
for _ in 0..13 {
|
||||||
|
let c = char::from_digit((n % 36) as u32, 36).unwrap();
|
||||||
|
result.insert(0, c);
|
||||||
|
n /= 36;
|
||||||
|
}
|
||||||
|
result.serialize(serializer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SnowflakeVisitor;
|
||||||
|
|
||||||
|
impl de::Visitor<'_> for SnowflakeVisitor {
|
||||||
|
type Value = Snowflake;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(formatter, "a base36 string of length 13")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
|
||||||
|
// Convert base36 string to u64
|
||||||
|
if v.len() != 13 {
|
||||||
|
return Err(E::invalid_length(v.len(), &self));
|
||||||
|
}
|
||||||
|
let n = u64::from_str_radix(v, 36)
|
||||||
|
.map_err(|_| E::invalid_value(de::Unexpected::Str(v), &self))?;
|
||||||
|
Ok(Snowflake(n))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for Snowflake {
|
||||||
|
fn deserialize<D: de::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||||
|
deserializer.deserialize_str(SnowflakeVisitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Time is specified as a signed 64-bit integer, giving the number of seconds
|
||||||
|
/// since the Unix Epoch.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct Time(#[serde(with = "time::serde::timestamp")] pub OffsetDateTime);
|
||||||
|
|
||||||
|
impl Time {
|
||||||
|
pub fn now() -> Self {
|
||||||
|
Self(OffsetDateTime::now_utc().replace_millisecond(0).unwrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Identifies a user.
|
||||||
|
///
|
||||||
|
/// The prefix of this value (up to the colon) indicates a type of session,
|
||||||
|
/// while the suffix is a unique value for that type of session.
|
||||||
|
///
|
||||||
|
/// It is possible for this value to have no prefix and colon, and there is no
|
||||||
|
/// fixed format for the unique value.
|
||||||
|
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct UserId(pub String);
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub enum SessionType {
|
||||||
|
Agent,
|
||||||
|
Account,
|
||||||
|
Bot,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UserId {
|
||||||
|
pub fn session_type(&self) -> Option<SessionType> {
|
||||||
|
if self.0.starts_with("agent:") {
|
||||||
|
Some(SessionType::Agent)
|
||||||
|
} else if self.0.starts_with("account:") {
|
||||||
|
Some(SessionType::Account)
|
||||||
|
} else if self.0.starts_with("bot:") {
|
||||||
|
Some(SessionType::Bot)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
466
src/conn.rs
Normal file
466
src/conn.rs
Normal file
|
|
@ -0,0 +1,466 @@
|
||||||
|
//! Connection state modeling.
|
||||||
|
|
||||||
|
// TODO Catch errors differently when sending into mpsc/oneshot
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::convert::Infallible;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use anyhow::bail;
|
||||||
|
use futures::channel::oneshot;
|
||||||
|
use futures::stream::{SplitSink, SplitStream};
|
||||||
|
use futures::{SinkExt, StreamExt};
|
||||||
|
use log::warn;
|
||||||
|
use rand::Rng;
|
||||||
|
use tokio::net::TcpStream;
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
use tokio::{select, task, time};
|
||||||
|
use tokio_tungstenite::{tungstenite, MaybeTlsStream, WebSocketStream};
|
||||||
|
|
||||||
|
use crate::replies::{self, PendingReply, Replies};
|
||||||
|
|
||||||
|
use super::api::packet::{Command, Packet, ParsedPacket};
|
||||||
|
use super::api::{
|
||||||
|
BounceEvent, Data, HelloEvent, PersonalAccountView, Ping, PingReply, SessionView,
|
||||||
|
SnapshotEvent, Time, UserId,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub type WsStream = WebSocketStream<MaybeTlsStream<TcpStream>>;
|
||||||
|
|
||||||
|
/// Timeout used for any kind of reply from the server, including to ws and euph
|
||||||
|
/// pings. Also used as the time in-between pings.
|
||||||
|
const TIMEOUT: Duration = Duration::from_secs(30); // TODO Make configurable
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("connection closed")]
|
||||||
|
ConnectionClosed,
|
||||||
|
#[error("packet timed out")]
|
||||||
|
TimedOut,
|
||||||
|
#[error("incorrect reply type")]
|
||||||
|
IncorrectReplyType,
|
||||||
|
#[error("{0}")]
|
||||||
|
Euph(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum Event {
|
||||||
|
Message(tungstenite::Message),
|
||||||
|
SendCmd(Data, oneshot::Sender<PendingReply<Result<Data, String>>>),
|
||||||
|
SendRpl(Option<String>, Data),
|
||||||
|
Status(oneshot::Sender<Status>),
|
||||||
|
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)]
|
||||||
|
pub struct Joining {
|
||||||
|
pub hello: Option<HelloEvent>,
|
||||||
|
pub snapshot: Option<SnapshotEvent>,
|
||||||
|
pub bounce: Option<BounceEvent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Joining {
|
||||||
|
fn on_data(&mut self, data: &Data) -> anyhow::Result<()> {
|
||||||
|
match data {
|
||||||
|
Data::BounceEvent(p) => self.bounce = Some(p.clone()),
|
||||||
|
Data::HelloEvent(p) => self.hello = Some(p.clone()),
|
||||||
|
Data::SnapshotEvent(p) => self.snapshot = Some(p.clone()),
|
||||||
|
d @ (Data::JoinEvent(_)
|
||||||
|
| Data::NetworkEvent(_)
|
||||||
|
| Data::NickEvent(_)
|
||||||
|
| Data::EditMessageEvent(_)
|
||||||
|
| Data::PartEvent(_)
|
||||||
|
| Data::PmInitiateEvent(_)
|
||||||
|
| Data::SendEvent(_)) => bail!("unexpected {}", d.packet_type()),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn joined(&self) -> Option<Joined> {
|
||||||
|
if let (Some(hello), Some(snapshot)) = (&self.hello, &self.snapshot) {
|
||||||
|
let mut session = hello.session.clone();
|
||||||
|
if let Some(nick) = &snapshot.nick {
|
||||||
|
session.name = nick.clone();
|
||||||
|
}
|
||||||
|
let listing = snapshot
|
||||||
|
.listing
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(|s| (s.id.clone(), s))
|
||||||
|
.collect::<HashMap<_, _>>();
|
||||||
|
Some(Joined {
|
||||||
|
session,
|
||||||
|
account: hello.account.clone(),
|
||||||
|
listing,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Joined {
|
||||||
|
pub session: SessionView,
|
||||||
|
pub account: Option<PersonalAccountView>,
|
||||||
|
pub listing: HashMap<UserId, SessionView>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Joined {
|
||||||
|
fn on_data(&mut self, data: &Data) {
|
||||||
|
match data {
|
||||||
|
Data::JoinEvent(p) => {
|
||||||
|
self.listing.insert(p.0.id.clone(), p.0.clone());
|
||||||
|
}
|
||||||
|
Data::SendEvent(p) => {
|
||||||
|
self.listing
|
||||||
|
.insert(p.0.sender.id.clone(), p.0.sender.clone());
|
||||||
|
}
|
||||||
|
Data::PartEvent(p) => {
|
||||||
|
self.listing.remove(&p.0.id);
|
||||||
|
}
|
||||||
|
Data::NetworkEvent(p) => {
|
||||||
|
if p.r#type == "partition" {
|
||||||
|
self.listing.retain(|_, s| {
|
||||||
|
!(s.server_id == p.server_id && s.server_era == p.server_era)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Data::NickEvent(p) => {
|
||||||
|
if let Some(session) = self.listing.get_mut(&p.id) {
|
||||||
|
session.name = p.to.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Data::NickReply(p) => {
|
||||||
|
assert_eq!(self.session.id, p.id);
|
||||||
|
self.session.name = p.to.clone();
|
||||||
|
}
|
||||||
|
// The who reply is broken and can't be trusted right now, so we'll
|
||||||
|
// not even look at it.
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[allow(clippy::large_enum_variant)]
|
||||||
|
pub enum Status {
|
||||||
|
Joining(Joining),
|
||||||
|
Joined(Joined),
|
||||||
|
}
|
||||||
|
|
||||||
|
struct State {
|
||||||
|
ws_tx: SplitSink<WsStream, tungstenite::Message>,
|
||||||
|
last_id: usize,
|
||||||
|
replies: Replies<String, Result<Data, String>>,
|
||||||
|
|
||||||
|
packet_tx: mpsc::UnboundedSender<Data>,
|
||||||
|
|
||||||
|
last_ws_ping: Option<Vec<u8>>,
|
||||||
|
last_ws_pong: Option<Vec<u8>>,
|
||||||
|
last_euph_ping: Option<Time>,
|
||||||
|
last_euph_pong: Option<Time>,
|
||||||
|
|
||||||
|
status: Status,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl State {
|
||||||
|
async fn run(
|
||||||
|
ws: WsStream,
|
||||||
|
mut tx_canary: mpsc::UnboundedReceiver<Infallible>,
|
||||||
|
rx_canary: oneshot::Receiver<Infallible>,
|
||||||
|
event_tx: mpsc::UnboundedSender<Event>,
|
||||||
|
mut event_rx: mpsc::UnboundedReceiver<Event>,
|
||||||
|
packet_tx: mpsc::UnboundedSender<Data>,
|
||||||
|
) {
|
||||||
|
let (ws_tx, mut ws_rx) = ws.split();
|
||||||
|
let mut state = Self {
|
||||||
|
ws_tx,
|
||||||
|
last_id: 0,
|
||||||
|
replies: Replies::new(TIMEOUT),
|
||||||
|
packet_tx,
|
||||||
|
last_ws_ping: None,
|
||||||
|
last_ws_pong: None,
|
||||||
|
last_euph_ping: None,
|
||||||
|
last_euph_pong: None,
|
||||||
|
status: Status::Joining(Joining::default()),
|
||||||
|
};
|
||||||
|
|
||||||
|
select! {
|
||||||
|
_ = tx_canary.recv() => (),
|
||||||
|
_ = rx_canary => (),
|
||||||
|
_ = Self::listen(&mut ws_rx, &event_tx) => (),
|
||||||
|
_ = Self::send_ping_events(&event_tx) => (),
|
||||||
|
_ = state.handle_events(&event_tx, &mut event_rx) => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn listen(
|
||||||
|
ws_rx: &mut SplitStream<WsStream>,
|
||||||
|
event_tx: &mpsc::UnboundedSender<Event>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
while let Some(msg) = ws_rx.next().await {
|
||||||
|
event_tx.send(Event::Message(msg?))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_ping_events(event_tx: &mpsc::UnboundedSender<Event>) -> anyhow::Result<()> {
|
||||||
|
loop {
|
||||||
|
event_tx.send(Event::DoPings)?;
|
||||||
|
time::sleep(TIMEOUT).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_events(
|
||||||
|
&mut self,
|
||||||
|
event_tx: &mpsc::UnboundedSender<Event>,
|
||||||
|
event_rx: &mut mpsc::UnboundedReceiver<Event>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
while let Some(ev) = event_rx.recv().await {
|
||||||
|
self.replies.purge();
|
||||||
|
match ev {
|
||||||
|
Event::Message(msg) => self.on_msg(msg, event_tx)?,
|
||||||
|
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::DoPings => self.do_pings(event_tx).await?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_msg(
|
||||||
|
&mut self,
|
||||||
|
msg: tungstenite::Message,
|
||||||
|
event_tx: &mpsc::UnboundedSender<Event>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
match msg {
|
||||||
|
tungstenite::Message::Text(t) => self.on_packet(serde_json::from_str(&t)?, event_tx)?,
|
||||||
|
tungstenite::Message::Binary(_) => bail!("unexpected binary message"),
|
||||||
|
tungstenite::Message::Ping(_) => {}
|
||||||
|
tungstenite::Message::Pong(p) => self.last_ws_pong = Some(p),
|
||||||
|
tungstenite::Message::Close(_) => {}
|
||||||
|
tungstenite::Message::Frame(_) => {}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_packet(
|
||||||
|
&mut self,
|
||||||
|
packet: Packet,
|
||||||
|
event_tx: &mpsc::UnboundedSender<Event>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let packet = ParsedPacket::from_packet(packet)?;
|
||||||
|
|
||||||
|
// Complete pending replies if the packet has an id
|
||||||
|
if let Some(id) = &packet.id {
|
||||||
|
self.replies.complete(id, packet.content.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shovel events and successful replies into self.packet_tx. Assumes
|
||||||
|
// that no even ever errors and that erroring replies are not
|
||||||
|
// interesting.
|
||||||
|
if let Ok(data) = packet.content {
|
||||||
|
self.packet_tx.send(data)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn on_send_cmd(
|
||||||
|
&mut self,
|
||||||
|
data: Data,
|
||||||
|
reply_tx: oneshot::Sender<PendingReply<Result<Data, String>>>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
// Overkill of universe-heat-death-like proportions
|
||||||
|
self.last_id = self.last_id.wrapping_add(1);
|
||||||
|
let id = format!("{}", self.last_id);
|
||||||
|
|
||||||
|
let packet = ParsedPacket {
|
||||||
|
id: Some(id.clone()),
|
||||||
|
r#type: data.packet_type(),
|
||||||
|
content: Ok(data),
|
||||||
|
throttled: None,
|
||||||
|
}
|
||||||
|
.into_packet()?;
|
||||||
|
|
||||||
|
let msg = tungstenite::Message::Text(serde_json::to_string(&packet)?);
|
||||||
|
self.ws_tx.send(msg).await?;
|
||||||
|
|
||||||
|
let _ = reply_tx.send(self.replies.wait_for(id));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn on_send_rpl(&mut self, id: Option<String>, data: Data) -> anyhow::Result<()> {
|
||||||
|
let packet = ParsedPacket {
|
||||||
|
id,
|
||||||
|
r#type: data.packet_type(),
|
||||||
|
content: Ok(data),
|
||||||
|
throttled: None,
|
||||||
|
}
|
||||||
|
.into_packet()?;
|
||||||
|
|
||||||
|
let msg = tungstenite::Message::Text(serde_json::to_string(&packet)?);
|
||||||
|
self.ws_tx.send(msg).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_status(&mut self, reply_tx: oneshot::Sender<Status>) {
|
||||||
|
let _ = reply_tx.send(self.status.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn do_pings(&mut self, event_tx: &mpsc::UnboundedSender<Event>) -> anyhow::Result<()> {
|
||||||
|
// Check old ws ping
|
||||||
|
if self.last_ws_ping.is_some() && self.last_ws_ping != self.last_ws_pong {
|
||||||
|
warn!("server missed ws ping");
|
||||||
|
bail!("server missed ws ping")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send new ws ping
|
||||||
|
let mut ws_payload = [0_u8; 8];
|
||||||
|
rand::thread_rng().fill(&mut ws_payload);
|
||||||
|
self.last_ws_ping = Some(ws_payload.to_vec());
|
||||||
|
self.ws_tx
|
||||||
|
.send(tungstenite::Message::Ping(ws_payload.to_vec()))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Check old euph ping
|
||||||
|
if self.last_euph_ping.is_some() && self.last_euph_ping != self.last_euph_pong {
|
||||||
|
warn!("server missed euph ping");
|
||||||
|
bail!("server missed euph ping")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send new euph ping
|
||||||
|
let euph_payload = Time::now();
|
||||||
|
self.last_euph_ping = Some(euph_payload);
|
||||||
|
let (tx, _) = oneshot::channel();
|
||||||
|
event_tx.send(Event::send_cmd(Ping { time: euph_payload }, tx))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ConnTx {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
canary: mpsc::UnboundedSender<Infallible>,
|
||||||
|
event_tx: mpsc::UnboundedSender<Event>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConnTx {
|
||||||
|
pub async fn send<C>(&self, cmd: C) -> Result<C::Reply, Error>
|
||||||
|
where
|
||||||
|
C: Command + Into<Data>,
|
||||||
|
C::Reply: TryFrom<Data>,
|
||||||
|
{
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
self.event_tx
|
||||||
|
.send(Event::SendCmd(cmd.into(), tx))
|
||||||
|
.map_err(|_| Error::ConnectionClosed)?;
|
||||||
|
let pending_reply = rx
|
||||||
|
.await
|
||||||
|
// 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> {
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
self.event_tx
|
||||||
|
.send(Event::Status(tx))
|
||||||
|
.map_err(|_| Error::ConnectionClosed)?;
|
||||||
|
rx.await.map_err(|_| Error::ConnectionClosed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ConnRx {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
canary: oneshot::Sender<Infallible>,
|
||||||
|
packet_rx: mpsc::UnboundedReceiver<Data>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConnRx {
|
||||||
|
pub async fn recv(&mut self) -> Result<Data, Error> {
|
||||||
|
self.packet_rx.recv().await.ok_or(Error::ConnectionClosed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Combine ConnTx and ConnRx and implement Stream + Sink?
|
||||||
|
|
||||||
|
pub fn wrap(ws: WsStream) -> (ConnTx, ConnRx) {
|
||||||
|
let (tx_canary_tx, tx_canary_rx) = mpsc::unbounded_channel();
|
||||||
|
let (rx_canary_tx, rx_canary_rx) = oneshot::channel();
|
||||||
|
let (event_tx, event_rx) = mpsc::unbounded_channel();
|
||||||
|
let (packet_tx, packet_rx) = mpsc::unbounded_channel();
|
||||||
|
|
||||||
|
task::spawn(State::run(
|
||||||
|
ws,
|
||||||
|
tx_canary_rx,
|
||||||
|
rx_canary_rx,
|
||||||
|
event_tx.clone(),
|
||||||
|
event_rx,
|
||||||
|
packet_tx,
|
||||||
|
));
|
||||||
|
|
||||||
|
let tx = ConnTx {
|
||||||
|
canary: tx_canary_tx,
|
||||||
|
event_tx,
|
||||||
|
};
|
||||||
|
let rx = ConnRx {
|
||||||
|
canary: rx_canary_tx,
|
||||||
|
packet_rx,
|
||||||
|
};
|
||||||
|
(tx, rx)
|
||||||
|
}
|
||||||
32
src/huehash.rs
Normal file
32
src/huehash.rs
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
fn normalize(text: &str) -> String {
|
||||||
|
// TODO Remove emoji names?
|
||||||
|
text.chars()
|
||||||
|
.filter(|&c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
|
||||||
|
.map(|c| c.to_ascii_lowercase())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A re-implementation of [euphoria's nick hue hashing algorithm][0].
|
||||||
|
///
|
||||||
|
/// [0]: https://github.com/euphoria-io/heim/blob/master/client/lib/hueHash.js
|
||||||
|
fn hue_hash(text: &str, offset: i64) -> u8 {
|
||||||
|
let mut val = 0_i32;
|
||||||
|
for bibyte in text.encode_utf16() {
|
||||||
|
let char_val = (bibyte as i32).wrapping_mul(439) % 256;
|
||||||
|
val = val.wrapping_mul(33).wrapping_add(char_val);
|
||||||
|
}
|
||||||
|
|
||||||
|
let val: i64 = val as i64 + 2_i64.pow(31);
|
||||||
|
((val + offset) % 255) as u8
|
||||||
|
}
|
||||||
|
|
||||||
|
const GREENIE_OFFSET: i64 = 148 - 192; // 148 - hue_hash("greenie", 0)
|
||||||
|
|
||||||
|
pub fn nick_hue(nick: &str) -> u8 {
|
||||||
|
let normalized = normalize(nick);
|
||||||
|
if normalized.is_empty() {
|
||||||
|
hue_hash(nick, GREENIE_OFFSET)
|
||||||
|
} else {
|
||||||
|
hue_hash(&normalized, GREENIE_OFFSET)
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/lib.rs
32
src/lib.rs
|
|
@ -1,14 +1,22 @@
|
||||||
pub fn add(left: usize, right: usize) -> usize {
|
#![deny(unsafe_code)]
|
||||||
left + right
|
// Rustc lint groups
|
||||||
}
|
#![warn(future_incompatible)]
|
||||||
|
#![warn(rust_2018_idioms)]
|
||||||
|
// Rustc lints
|
||||||
|
#![warn(noop_method_call)]
|
||||||
|
#![warn(single_use_lifetimes)]
|
||||||
|
#![warn(trivial_numeric_casts)]
|
||||||
|
#![warn(unused_crate_dependencies)]
|
||||||
|
#![warn(unused_extern_crates)]
|
||||||
|
#![warn(unused_import_braces)]
|
||||||
|
#![warn(unused_lifetimes)]
|
||||||
|
#![warn(unused_qualifications)]
|
||||||
|
// Clippy lints
|
||||||
|
#![warn(clippy::use_self)]
|
||||||
|
|
||||||
#[cfg(test)]
|
pub mod api;
|
||||||
mod tests {
|
pub mod conn;
|
||||||
use super::*;
|
mod huehash;
|
||||||
|
pub mod replies;
|
||||||
|
|
||||||
#[test]
|
pub use huehash::nick_hue;
|
||||||
fn it_works() {
|
|
||||||
let result = add(2, 2);
|
|
||||||
assert_eq!(result, 4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
68
src/replies.rs
Normal file
68
src/replies.rs
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::hash::Hash;
|
||||||
|
use std::result;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use tokio::sync::oneshot::{self, Receiver, Sender};
|
||||||
|
use tokio::time;
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("timed out")]
|
||||||
|
TimedOut,
|
||||||
|
#[error("canceled")]
|
||||||
|
Canceled,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Result<T> = result::Result<T, Error>;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct PendingReply<R> {
|
||||||
|
timeout: Duration,
|
||||||
|
result: Receiver<R>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R> PendingReply<R> {
|
||||||
|
pub async fn get(self) -> Result<R> {
|
||||||
|
let result = time::timeout(self.timeout, self.result).await;
|
||||||
|
match result {
|
||||||
|
Err(_) => Err(Error::TimedOut),
|
||||||
|
Ok(Err(_)) => Err(Error::Canceled),
|
||||||
|
Ok(Ok(value)) => Ok(value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Replies<I, R> {
|
||||||
|
timeout: Duration,
|
||||||
|
pending: HashMap<I, Sender<R>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<I: Eq + Hash, R> Replies<I, R> {
|
||||||
|
pub fn new(timeout: Duration) -> Self {
|
||||||
|
Self {
|
||||||
|
timeout,
|
||||||
|
pending: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn wait_for(&mut self, id: I) -> PendingReply<R> {
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
self.pending.insert(id, tx);
|
||||||
|
PendingReply {
|
||||||
|
timeout: self.timeout,
|
||||||
|
result: rx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn complete(&mut self, id: &I, result: R) {
|
||||||
|
if let Some(tx) = self.pending.remove(id) {
|
||||||
|
let _ = tx.send(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn purge(&mut self) {
|
||||||
|
self.pending.retain(|_, tx| !tx.is_closed());
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue