Use euphoxide instead of euph module
This commit is contained in:
parent
d07b1051a9
commit
36b717ff8c
17 changed files with 83 additions and 1556 deletions
|
|
@ -1,10 +1,7 @@
|
|||
pub mod api;
|
||||
mod conn;
|
||||
mod room;
|
||||
mod small_message;
|
||||
mod util;
|
||||
|
||||
pub use conn::{Joined, Joining, Status};
|
||||
pub use room::Room;
|
||||
pub use small_message::SmallMessage;
|
||||
pub use util::{hue, nick_color, nick_style};
|
||||
pub use util::{nick_color, nick_style};
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
//! 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::*;
|
||||
|
|
@ -1,170 +0,0 @@
|
|||
//! 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>,
|
||||
}
|
||||
|
|
@ -1,192 +0,0 @@
|
|||
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,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
//! 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>,
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
//! 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>,
|
||||
}
|
||||
|
|
@ -1,391 +0,0 @@
|
|||
//! 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/euph/conn.rs
466
src/euph/conn.rs
|
|
@ -1,466 +0,0 @@
|
|||
//! 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)
|
||||
}
|
||||
|
|
@ -5,6 +5,8 @@ use std::time::Duration;
|
|||
|
||||
use anyhow::bail;
|
||||
use cookie::{Cookie, CookieJar};
|
||||
use euphoxide::api::{Data, Log, Nick, Send, Snowflake, Time, UserId};
|
||||
use euphoxide::conn::{ConnRx, ConnTx, Joining, Status};
|
||||
use log::{error, info, warn};
|
||||
use parking_lot::Mutex;
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
|
|
@ -14,15 +16,10 @@ use tokio_tungstenite::tungstenite::client::IntoClientRequest;
|
|||
use tokio_tungstenite::tungstenite::handshake::client::Response;
|
||||
use tokio_tungstenite::tungstenite::http::{header, HeaderValue};
|
||||
|
||||
use crate::euph::api::Time;
|
||||
use crate::macros::ok_or_return;
|
||||
use crate::ui::UiEvent;
|
||||
use crate::vault::{EuphVault, Vault};
|
||||
|
||||
use super::api::{Data, Log, Nick, Send, Snowflake, UserId};
|
||||
use super::conn::{self, ConnRx, ConnTx, Status};
|
||||
use super::Joining;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("room stopped")]
|
||||
|
|
@ -131,7 +128,7 @@ impl State {
|
|||
match tokio_tungstenite::connect_async(request).await {
|
||||
Ok((ws, response)) => {
|
||||
Self::update_cookies(vault.vault(), &response);
|
||||
Ok(Some(conn::wrap(ws)))
|
||||
Ok(Some(euphoxide::wrap(ws)))
|
||||
}
|
||||
Err(tungstenite::Error::Http(resp)) if resp.status().is_client_error() => {
|
||||
bail!("room {name} doesn't exist");
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
use crossterm::style::{Color, ContentStyle, Stylize};
|
||||
use euphoxide::api::{Snowflake, Time};
|
||||
use time::OffsetDateTime;
|
||||
use toss::styled::Styled;
|
||||
|
||||
use crate::store::Msg;
|
||||
use crate::ui::ChatMsg;
|
||||
|
||||
use super::api::{Snowflake, Time};
|
||||
use super::util;
|
||||
|
||||
fn nick_char(ch: char) -> bool {
|
||||
|
|
|
|||
|
|
@ -1,41 +1,8 @@
|
|||
use crossterm::style::{Color, ContentStyle, Stylize};
|
||||
use palette::{FromColor, Hsl, RgbHue, Srgb};
|
||||
|
||||
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 hue(text: &str) -> u8 {
|
||||
let normalized = normalize(text);
|
||||
if normalized.is_empty() {
|
||||
hue_hash(text, GREENIE_OFFSET)
|
||||
} else {
|
||||
hue_hash(&normalized, GREENIE_OFFSET)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn nick_color(nick: &str) -> (u8, u8, u8) {
|
||||
let hue = RgbHue::from(hue(nick) as f32);
|
||||
let hue = RgbHue::from(euphoxide::nick_hue(nick) as f32);
|
||||
let color = Hsl::new(hue, 1.0, 0.72);
|
||||
Srgb::from_color(color)
|
||||
.into_format::<u8>()
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
use std::fs::File;
|
||||
use std::io::{BufWriter, Write};
|
||||
|
||||
use euphoxide::api::Snowflake;
|
||||
use time::format_description::FormatItem;
|
||||
use time::macros::format_description;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::euph::api::Snowflake;
|
||||
use crate::euph::SmallMessage;
|
||||
use crate::store::{MsgStore, Tree};
|
||||
use crate::vault::Vault;
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ mod euph;
|
|||
mod export;
|
||||
mod logger;
|
||||
mod macros;
|
||||
mod replies;
|
||||
mod store;
|
||||
mod ui;
|
||||
mod vault;
|
||||
|
|
|
|||
|
|
@ -1,68 +0,0 @@
|
|||
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());
|
||||
}
|
||||
}
|
||||
|
|
@ -3,14 +3,15 @@ use std::sync::Arc;
|
|||
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::style::{Color, ContentStyle, Stylize};
|
||||
use euphoxide::api::{SessionType, SessionView, Snowflake};
|
||||
use euphoxide::conn::{Joined, Status};
|
||||
use parking_lot::FairMutex;
|
||||
use tokio::sync::oneshot::error::TryRecvError;
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
use toss::styled::Styled;
|
||||
use toss::terminal::Terminal;
|
||||
|
||||
use crate::euph::api::{SessionType, SessionView, Snowflake};
|
||||
use crate::euph::{self, Joined, Status};
|
||||
use crate::euph;
|
||||
use crate::store::MsgStore;
|
||||
use crate::ui::chat::{ChatState, Reaction};
|
||||
use crate::ui::input::{key, InputEvent, KeyBindingsList, KeyEvent};
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@ use std::sync::Arc;
|
|||
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::style::{ContentStyle, Stylize};
|
||||
use euphoxide::api::SessionType;
|
||||
use euphoxide::conn::{Joined, Status};
|
||||
use parking_lot::FairMutex;
|
||||
use tokio::sync::mpsc;
|
||||
use toss::styled::Styled;
|
||||
use toss::terminal::Terminal;
|
||||
|
||||
use crate::euph::api::SessionType;
|
||||
use crate::euph::{Joined, Status};
|
||||
use crate::vault::Vault;
|
||||
|
||||
use super::euph::room::EuphRoom;
|
||||
|
|
|
|||
|
|
@ -3,42 +3,48 @@ use std::str::FromStr;
|
|||
|
||||
use async_trait::async_trait;
|
||||
use cookie::{Cookie, CookieJar};
|
||||
use euphoxide::api::{Message, SessionView, Snowflake, Time, UserId};
|
||||
use rusqlite::types::{FromSql, FromSqlError, ToSqlOutput, Value, ValueRef};
|
||||
use rusqlite::{named_params, params, Connection, OptionalExtension, ToSql, Transaction};
|
||||
use time::OffsetDateTime;
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
use crate::euph::api::{Message, SessionView, Snowflake, Time, UserId};
|
||||
use crate::euph::SmallMessage;
|
||||
use crate::store::{MsgStore, Path, Tree};
|
||||
|
||||
use super::{Request, Vault};
|
||||
|
||||
impl ToSql for Snowflake {
|
||||
/// Wrapper for [`Snowflake`] that implements useful rusqlite traits.
|
||||
struct WSnowflake(Snowflake);
|
||||
|
||||
impl ToSql for WSnowflake {
|
||||
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
|
||||
self.0.to_sql()
|
||||
self.0 .0.to_sql()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromSql for Snowflake {
|
||||
impl FromSql for WSnowflake {
|
||||
fn column_result(value: ValueRef<'_>) -> Result<Self, FromSqlError> {
|
||||
u64::column_result(value).map(Self)
|
||||
u64::column_result(value).map(|v| Self(Snowflake(v)))
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSql for Time {
|
||||
/// Wrapper for [`Time`] that implements useful rusqlite traits.
|
||||
struct WTime(Time);
|
||||
|
||||
impl ToSql for WTime {
|
||||
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
|
||||
let timestamp = self.0.unix_timestamp();
|
||||
let timestamp = self.0 .0.unix_timestamp();
|
||||
Ok(ToSqlOutput::Owned(Value::Integer(timestamp)))
|
||||
}
|
||||
}
|
||||
|
||||
impl FromSql for Time {
|
||||
impl FromSql for WTime {
|
||||
fn column_result(value: ValueRef<'_>) -> rusqlite::types::FromSqlResult<Self> {
|
||||
let timestamp = i64::column_result(value)?;
|
||||
Ok(Self(
|
||||
Ok(Self(Time(
|
||||
OffsetDateTime::from_unix_timestamp(timestamp).expect("timestamp in range"),
|
||||
))
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -623,7 +629,7 @@ impl EuphRequest {
|
|||
ON CONFLICT (room) DO UPDATE
|
||||
SET last_joined = :time
|
||||
",
|
||||
named_params! {":room": room, ":time": time},
|
||||
named_params! {":room": room, ":time": WTime(time)},
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -695,14 +701,14 @@ impl EuphRequest {
|
|||
for msg in msgs {
|
||||
insert_msg.execute(named_params! {
|
||||
":room": room,
|
||||
":id": msg.id,
|
||||
":parent": msg.parent,
|
||||
":previous_edit_id": msg.previous_edit_id,
|
||||
":time": msg.time,
|
||||
":id": WSnowflake(msg.id),
|
||||
":parent": msg.parent.map(WSnowflake),
|
||||
":previous_edit_id": msg.previous_edit_id.map(WSnowflake),
|
||||
":time": WTime(msg.time),
|
||||
":content": msg.content,
|
||||
":encryption_key_id": msg.encryption_key_id,
|
||||
":edited": msg.edited,
|
||||
":deleted": msg.deleted,
|
||||
":edited": msg.edited.map(WTime),
|
||||
":deleted": msg.deleted.map(WTime),
|
||||
":truncated": msg.truncated,
|
||||
":user_id": msg.sender.id.0,
|
||||
":name": msg.sender.name,
|
||||
|
|
@ -736,8 +742,8 @@ impl EuphRequest {
|
|||
",
|
||||
)?
|
||||
.query_map([room], |row| {
|
||||
let start = row.get::<_, Option<Snowflake>>(0)?;
|
||||
let end = row.get::<_, Option<Snowflake>>(1)?;
|
||||
let start = row.get::<_, Option<WSnowflake>>(0)?.map(|s| s.0);
|
||||
let end = row.get::<_, Option<WSnowflake>>(1)?.map(|s| s.0);
|
||||
Ok((start, end))
|
||||
})?
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
|
@ -788,7 +794,7 @@ impl EuphRequest {
|
|||
",
|
||||
)?;
|
||||
for (start, end) in result {
|
||||
stmt.execute(params![room, start, end])?;
|
||||
stmt.execute(params![room, start.map(WSnowflake), end.map(WSnowflake)])?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
@ -851,7 +857,12 @@ impl EuphRequest {
|
|||
LIMIT 1
|
||||
",
|
||||
)?
|
||||
.query_row([room], |row| Ok((row.get(0)?, row.get(1)?)))
|
||||
.query_row([room], |row| {
|
||||
Ok((
|
||||
row.get::<_, Option<WSnowflake>>(0)?.map(|s| s.0),
|
||||
row.get::<_, Option<WSnowflake>>(1)?.map(|s| s.0),
|
||||
))
|
||||
})
|
||||
.optional()?;
|
||||
let _ = result.send(span);
|
||||
Ok(())
|
||||
|
|
@ -880,7 +891,9 @@ impl EuphRequest {
|
|||
ORDER BY id ASC
|
||||
",
|
||||
)?
|
||||
.query_map(params![room, id], |row| row.get(0))?
|
||||
.query_map(params![room, WSnowflake(id)], |row| {
|
||||
row.get::<_, WSnowflake>(0).map(|s| s.0)
|
||||
})?
|
||||
.collect::<rusqlite::Result<_>>()?;
|
||||
let path = Path::new(path);
|
||||
let _ = result.send(path);
|
||||
|
|
@ -912,11 +925,11 @@ impl EuphRequest {
|
|||
ORDER BY id ASC
|
||||
",
|
||||
)?
|
||||
.query_map(params![room, root], |row| {
|
||||
.query_map(params![room, WSnowflake(root)], |row| {
|
||||
Ok(SmallMessage {
|
||||
id: row.get(0)?,
|
||||
parent: row.get(1)?,
|
||||
time: row.get(2)?,
|
||||
id: row.get::<_, WSnowflake>(0)?.0,
|
||||
parent: row.get::<_, Option<WSnowflake>>(1)?.map(|s| s.0),
|
||||
time: row.get::<_, WTime>(2)?.0,
|
||||
nick: row.get(3)?,
|
||||
content: row.get(4)?,
|
||||
seen: row.get(5)?,
|
||||
|
|
@ -943,7 +956,7 @@ impl EuphRequest {
|
|||
LIMIT 1
|
||||
",
|
||||
)?
|
||||
.query_row([room], |row| row.get(0))
|
||||
.query_row([room], |row| row.get::<_, WSnowflake>(0).map(|s| s.0))
|
||||
.optional()?;
|
||||
let _ = result.send(tree);
|
||||
Ok(())
|
||||
|
|
@ -964,7 +977,7 @@ impl EuphRequest {
|
|||
LIMIT 1
|
||||
",
|
||||
)?
|
||||
.query_row([room], |row| row.get(0))
|
||||
.query_row([room], |row| row.get::<_, WSnowflake>(0).map(|s| s.0))
|
||||
.optional()?;
|
||||
let _ = result.send(tree);
|
||||
Ok(())
|
||||
|
|
@ -987,7 +1000,9 @@ impl EuphRequest {
|
|||
LIMIT 1
|
||||
",
|
||||
)?
|
||||
.query_row(params![room, root], |row| row.get(0))
|
||||
.query_row(params![room, WSnowflake(root)], |row| {
|
||||
row.get::<_, WSnowflake>(0).map(|s| s.0)
|
||||
})
|
||||
.optional()?;
|
||||
let _ = result.send(tree);
|
||||
Ok(())
|
||||
|
|
@ -1010,7 +1025,9 @@ impl EuphRequest {
|
|||
LIMIT 1
|
||||
",
|
||||
)?
|
||||
.query_row(params![room, root], |row| row.get(0))
|
||||
.query_row(params![room, WSnowflake(root)], |row| {
|
||||
row.get::<_, WSnowflake>(0).map(|s| s.0)
|
||||
})
|
||||
.optional()?;
|
||||
let _ = result.send(tree);
|
||||
Ok(())
|
||||
|
|
@ -1031,7 +1048,7 @@ impl EuphRequest {
|
|||
LIMIT 1
|
||||
",
|
||||
)?
|
||||
.query_row([room], |row| row.get(0))
|
||||
.query_row([room], |row| row.get::<_, WSnowflake>(0).map(|s| s.0))
|
||||
.optional()?;
|
||||
let _ = result.send(tree);
|
||||
Ok(())
|
||||
|
|
@ -1052,7 +1069,7 @@ impl EuphRequest {
|
|||
LIMIT 1
|
||||
",
|
||||
)?
|
||||
.query_row([room], |row| row.get(0))
|
||||
.query_row([room], |row| row.get::<_, WSnowflake>(0).map(|s| s.0))
|
||||
.optional()?;
|
||||
let _ = result.send(tree);
|
||||
Ok(())
|
||||
|
|
@ -1075,7 +1092,9 @@ impl EuphRequest {
|
|||
LIMIT 1
|
||||
",
|
||||
)?
|
||||
.query_row(params![room, id], |row| row.get(0))
|
||||
.query_row(params![room, WSnowflake(id)], |row| {
|
||||
row.get::<_, WSnowflake>(0).map(|s| s.0)
|
||||
})
|
||||
.optional()?;
|
||||
let _ = result.send(tree);
|
||||
Ok(())
|
||||
|
|
@ -1098,7 +1117,9 @@ impl EuphRequest {
|
|||
LIMIT 1
|
||||
",
|
||||
)?
|
||||
.query_row(params![room, id], |row| row.get(0))
|
||||
.query_row(params![room, WSnowflake(id)], |row| {
|
||||
row.get::<_, WSnowflake>(0).map(|s| s.0)
|
||||
})
|
||||
.optional()?;
|
||||
let _ = result.send(tree);
|
||||
Ok(())
|
||||
|
|
@ -1120,7 +1141,7 @@ impl EuphRequest {
|
|||
LIMIT 1
|
||||
",
|
||||
)?
|
||||
.query_row([room], |row| row.get(0))
|
||||
.query_row([room], |row| row.get::<_, WSnowflake>(0).map(|s| s.0))
|
||||
.optional()?;
|
||||
let _ = result.send(tree);
|
||||
Ok(())
|
||||
|
|
@ -1142,7 +1163,7 @@ impl EuphRequest {
|
|||
LIMIT 1
|
||||
",
|
||||
)?
|
||||
.query_row([room], |row| row.get(0))
|
||||
.query_row([room], |row| row.get::<_, WSnowflake>(0).map(|s| s.0))
|
||||
.optional()?;
|
||||
let _ = result.send(tree);
|
||||
Ok(())
|
||||
|
|
@ -1166,7 +1187,9 @@ impl EuphRequest {
|
|||
LIMIT 1
|
||||
",
|
||||
)?
|
||||
.query_row(params![room, id], |row| row.get(0))
|
||||
.query_row(params![room, WSnowflake(id)], |row| {
|
||||
row.get::<_, WSnowflake>(0).map(|s| s.0)
|
||||
})
|
||||
.optional()?;
|
||||
let _ = result.send(tree);
|
||||
Ok(())
|
||||
|
|
@ -1190,7 +1213,9 @@ impl EuphRequest {
|
|||
LIMIT 1
|
||||
",
|
||||
)?
|
||||
.query_row(params![room, id], |row| row.get(0))
|
||||
.query_row(params![room, WSnowflake(id)], |row| {
|
||||
row.get::<_, WSnowflake>(0).map(|s| s.0)
|
||||
})
|
||||
.optional()?;
|
||||
let _ = result.send(tree);
|
||||
Ok(())
|
||||
|
|
@ -1229,7 +1254,7 @@ impl EuphRequest {
|
|||
WHERE room = :room
|
||||
AND id = :id
|
||||
",
|
||||
named_params! { ":room": room, ":id": id, ":seen": seen },
|
||||
named_params! { ":room": room, ":id": WSnowflake(id), ":seen": seen },
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1248,7 +1273,7 @@ impl EuphRequest {
|
|||
AND id <= :id
|
||||
AND seen != :seen
|
||||
",
|
||||
named_params! { ":room": room, ":id": id, ":seen": seen },
|
||||
named_params! { ":room": room, ":id": WSnowflake(id), ":seen": seen },
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1276,14 +1301,14 @@ impl EuphRequest {
|
|||
let messages = query
|
||||
.query_map(params![room, amount, offset], |row| {
|
||||
Ok(Message {
|
||||
id: row.get(0)?,
|
||||
parent: row.get(1)?,
|
||||
previous_edit_id: row.get(2)?,
|
||||
time: row.get(3)?,
|
||||
id: row.get::<_, WSnowflake>(0)?.0,
|
||||
parent: row.get::<_, Option<WSnowflake>>(1)?.map(|s| s.0),
|
||||
previous_edit_id: row.get::<_, Option<WSnowflake>>(2)?.map(|s| s.0),
|
||||
time: row.get::<_, WTime>(3)?.0,
|
||||
content: row.get(4)?,
|
||||
encryption_key_id: row.get(5)?,
|
||||
edited: row.get(6)?,
|
||||
deleted: row.get(7)?,
|
||||
edited: row.get::<_, Option<WTime>>(6)?.map(|t| t.0),
|
||||
deleted: row.get::<_, Option<WTime>>(7)?.map(|t| t.0),
|
||||
truncated: row.get(8)?,
|
||||
sender: SessionView {
|
||||
id: UserId(row.get(9)?),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue