Use euphoxide instead of euph module

This commit is contained in:
Joscha 2022-08-18 18:13:49 +02:00
parent d07b1051a9
commit 36b717ff8c
17 changed files with 83 additions and 1556 deletions

View file

@ -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};

View file

@ -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::*;

View file

@ -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 rooms 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 servers 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>,
}

View file

@ -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,
},
})
}
}

View file

@ -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>,
}

View file

@ -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>,
}

View file

@ -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
}
}
}

View file

@ -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)
}

View file

@ -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");

View file

@ -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 {

View file

@ -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>()

View file

@ -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;

View file

@ -21,7 +21,6 @@ mod euph;
mod export;
mod logger;
mod macros;
mod replies;
mod store;
mod ui;
mod vault;

View file

@ -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());
}
}

View file

@ -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};

View file

@ -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;

View file

@ -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)?),