Refactor and document api module

This commit is contained in:
Joscha 2024-12-05 12:37:24 +01:00
parent 5ddffb510d
commit 92ea7f0aa0
10 changed files with 428 additions and 272 deletions

View file

@ -1,17 +1,12 @@
//! Models the [euphoria API][0].
//! Models the [euphoria.leet.nu API][0].
//!
//! [0]: https://euphoria.leet.nu/heim/api
mod account_cmds;
mod events;
pub mod packet;
mod room_cmds;
mod session_cmds;
mod types;
pub mod account_cmds;
pub mod events;
pub mod packets;
pub mod room_cmds;
pub mod session_cmds;
pub mod types;
pub use account_cmds::*;
pub use events::*;
pub use packet::Data;
pub use room_cmds::*;
pub use session_cmds::*;
pub use types::*;
pub use self::{account_cmds::*, events::*, packets::*, room_cmds::*, session_cmds::*, types::*};

View file

@ -1,8 +1,10 @@
//! Account commands.
//! Models [account commands][0] and their replies.
//!
//! These commands enable a client to register, associate, and dissociate with
//! an account. An account allows an identity to be shared across browsers and
//! devices, and is a prerequisite for room management
//!
//! [0]: https://euphoria.leet.nu/heim/api#account-commands
use serde::{Deserialize, Serialize};
@ -11,6 +13,8 @@ use super::AccountId;
/// Change the primary email address associated with the signed in account.
///
/// The email address may need to be verified before the change is fully applied.
///
/// <https://euphoria.leet.nu/heim/api#change-email>
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChangeEmail {
/// The new primary email address for the account.
@ -32,6 +36,8 @@ pub struct ChangeEmailReply {
}
/// Change the name associated with the signed in account.
///
/// <https://euphoria.leet.nu/heim/api#change-name>
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChangeName {
/// The name to associate with the account.
@ -46,6 +52,8 @@ pub struct ChangeNameReply {
}
/// Change the password of the signed in account.
///
/// <https://euphoria.leet.nu/heim/api#change-password>
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChangePassword {
/// The current (and soon-to-be former) password.
@ -65,6 +73,8 @@ pub struct ChangePasswordReply {}
/// If the login succeeds, the client should expect to receive a
/// [`DisconnectEvent`](super::DisconnectEvent) shortly after. The next
/// connection the client makes will be a logged in session.
///
/// <https://euphoria.leet.nu/heim/api#login>
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Login {
/// The namespace of a personal identifier.
@ -98,6 +108,8 @@ pub struct LoginReply {
/// If the logout is successful, the client should expect to receive a
/// [`DisconnectEvent`](super::DisconnectEvent) shortly after. The next
/// connection the client makes will be a logged out session.
///
/// <https://euphoria.leet.nu/heim/api#logout>
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Logout {}
@ -113,6 +125,8 @@ pub struct LogoutReply {}
/// [`DisconnectEvent`](super::DisconnectEvent) shortly after. The next
/// connection the client makes will be a logged in session using the new
/// account.
///
/// <https://euphoria.leet.nu/heim/api#register-account>
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegisterAccount {
/// The namespace of a personal identifier.
@ -145,6 +159,8 @@ pub struct RegisterAccountReply {
///
/// An error will be returned if the account has no unverified email addresses
/// associated with it.
///
/// <https://euphoria.leet.nu/heim/api#resend-verification-email>
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResendVerificationEmail {}
@ -156,6 +172,8 @@ pub struct ResendVerificationEmailReply {}
///
/// An email will be sent to the owner of the given personal identifier, with
/// instructions and a confirmation code for resetting the password.
///
/// <https://euphoria.leet.nu/heim/api#reset-password>
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResetPassword {
pub namespace: String,

View file

@ -1,4 +1,6 @@
//! Asynchronous events.
//! Models [asynchronous events][0].
//!
//! [0]: https://euphoria.leet.nu/heim/api#asynchronous-events
use serde::{Deserialize, Serialize};
@ -8,6 +10,8 @@ use super::{
};
/// Indicates that access to a room is denied.
///
/// <https://euphoria.leet.nu/heim/api#bounce-event>
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BounceEvent {
/// The reason why access was denied.
@ -25,16 +29,37 @@ pub struct BounceEvent {
///
/// If the disconnect reason is `authentication changed`, the client should
/// immediately reconnect.
///
/// <https://euphoria.leet.nu/heim/api#disconnect-event>
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DisconnectEvent {
/// The reason for disconnection.
pub reason: 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.
///
/// <https://euphoria.leet.nu/heim/api#edit-message-event>
#[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,
}
/// Sent by the server to the client when a session is started.
///
/// It includes information about the client's authentication and associated
/// identity.
///
/// <https://euphoria.leet.nu/heim/api#hello-event>
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HelloEvent {
/// The id of the agent or account logged into this session.
@ -55,11 +80,15 @@ pub struct HelloEvent {
}
/// Indicates a session just joined the room.
///
/// <https://euphoria.leet.nu/heim/api#join-event>
#[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).
///
/// <https://euphoria.leet.nu/heim/api#login-event>
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoginEvent {
pub account_id: AccountId,
@ -67,6 +96,8 @@ pub struct LoginEvent {
/// Sent to all sessions of an agent when that agent is logged out (except for
/// the session that issued the logout command).
///
/// <https://euphoria.leet.nu/heim/api#logout-event>
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogoutEvent {}
@ -75,6 +106,8 @@ pub struct LogoutEvent {}
///
/// 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.
///
/// <https://euphoria.leet.nu/heim/api#network-event>
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetworkEvent {
/// The type of network event; for now, always `partition`.
@ -86,6 +119,8 @@ pub struct NetworkEvent {
}
/// Announces a nick change by another session in the room.
///
/// <https://euphoria.leet.nu/heim/api#nick-event>
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NickEvent {
/// The id of the session this name applies to.
@ -98,22 +133,9 @@ pub struct NickEvent {
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.
///
/// <https://euphoria.leet.nu/heim/api#part-event>
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PartEvent(pub SessionView);
@ -121,6 +143,8 @@ pub struct PartEvent(pub SessionView);
///
/// The client should send back a ping-reply with the same value for the time
/// field as soon as possible (or risk disconnection).
///
/// <https://euphoria.leet.nu/heim/api#ping-event>
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PingEvent {
/// A unix timestamp according to the server's clock.
@ -131,6 +155,8 @@ pub struct PingEvent {
}
/// Informs the client that another user wants to chat with them privately.
///
/// <https://euphoria.leet.nu/heim/api#pm-initiate-event>
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PmInitiateEvent {
/// The id of the user inviting the client to chat privately.
@ -144,12 +170,16 @@ pub struct PmInitiateEvent {
}
/// Indicates a message received by the room from another session.
///
/// <https://euphoria.leet.nu/heim/api#send-event>
#[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.
///
/// <https://euphoria.leet.nu/heim/api#snapshot-event>
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SnapshotEvent {
/// The id of the agent or account logged into this session.

View file

@ -1,223 +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_if = "Option::is_none")]
pub error: Option<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub throttled: bool,
#[serde(skip_serializing_if = "Option::is_none")]
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! {
// Events
BounceEvent,
DisconnectEvent,
HelloEvent,
JoinEvent,
LoginEvent,
LogoutEvent,
NetworkEvent,
NickEvent,
EditMessageEvent,
PartEvent,
PingEvent,
PmInitiateEvent,
SendEvent,
SnapshotEvent,
// Session commands
Auth,
AuthReply,
Ping,
PingReply,
// Chat room commands
GetMessage,
GetMessageReply,
Log,
LogReply,
Nick,
NickReply,
PmInitiate,
PmInitiateReply,
Send,
SendReply,
Who,
WhoReply,
// Account commands
ChangeEmail,
ChangeEmailReply,
ChangeName,
ChangeNameReply,
ChangePassword,
ChangePasswordReply,
Login,
LoginReply,
Logout,
LogoutReply,
RegisterAccount,
RegisterAccountReply,
ResendVerificationEmail,
ResendVerificationEmailReply,
ResetPassword,
ResetPasswordReply,
}
commands! {
// Session commands
Auth => AuthReply,
Ping => PingReply,
// Chat room commands
GetMessage => GetMessageReply,
Log => LogReply,
Nick => NickReply,
PmInitiate => PmInitiateReply,
Send => SendReply,
Who => WhoReply,
// Account commands
ChangeEmail => ChangeEmailReply,
ChangeName => ChangeNameReply,
ChangePassword => ChangePasswordReply,
Login => LoginReply,
Logout => LogoutReply,
RegisterAccount => RegisterAccountReply,
ResendVerificationEmail => ResendVerificationEmailReply,
ResetPassword => ResetPasswordReply,
}
#[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,
},
})
}
}

294
src/api/packets.rs Normal file
View file

@ -0,0 +1,294 @@
//! Models the [packets][0] sent between the server and client.
//!
//! [0]: https://euphoria.leet.nu/heim/api#packets
use serde::{Deserialize, Serialize};
use serde_json::Value;
use super::PacketType;
/// A "raw" packet.
///
/// This packet closely matches the [packet representation defined in the
/// API][0]. It can contain arbitrary data in the form of a JSON [`Value`]. It
/// can also contain both data and an error at the same time.
///
/// In order to interpret this packet, you probably want to convert it to a
/// [`ParsedPacket`] using [`ParsedPacket::from_packet`].
///
/// [0]: https://euphoria.leet.nu/heim/api#packets
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Packet {
/// Client-generated id for associating replies with commands.
pub id: Option<String>,
/// The type of the command, reply, or event.
pub r#type: PacketType,
/// The payload of the command, reply, or event.
pub data: Option<Value>,
/// This field appears in replies if a command fails.
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
/// This field appears in replies to warn the client that it may be
/// flooding.
///
/// The client should slow down its command rate.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub throttled: bool,
/// If throttled is true, this field describes why.
#[serde(skip_serializing_if = "Option::is_none")]
pub throttled_reason: Option<String>,
}
/// Models the relationship between command and reply types.
///
/// This trait is useful for type-safe command-reply APIs.
pub trait Command {
/// The type of reply one can expect from the server when sending this
/// command.
type Reply;
}
macro_rules! packets {
( $( $mod:ident::$name:ident, )*) => {
/// A big enum containing most types of packet data.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum Data {
$( $name(super::$mod::$name), )*
/// A valid type of packet data that this library does not model as
/// a struct.
Unimplemented(PacketType, Value),
}
impl Data {
/// Interpret a JSON [`Value`] as packet data of a specific [`PacketType`].
///
/// This method may fail if the data is invalid.
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(ptype, value),
})
}
/// Convert the packet data into a JSON [`Value`].
///
/// This method may fail if the data fails to serialize.
pub fn into_value(self) -> serde_json::Result<Value> {
Ok(match self {
$( Self::$name(p) => serde_json::to_value(p)?, )*
Self::Unimplemented(_, value) => value,
})
}
/// The [`PacketType`] of this packet data.
pub fn packet_type(&self) -> PacketType {
match self {
$( Self::$name(_) => PacketType::$name, )*
Self::Unimplemented(ptype, _) => *ptype,
}
}
}
$(
impl From<super::$mod::$name> for Data {
fn from(p: super::$mod::$name) -> Self {
Self::$name(p)
}
}
impl TryFrom<Data> for super::$mod::$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! {
// Events
events::BounceEvent,
events::DisconnectEvent,
events::EditMessageEvent,
events::HelloEvent,
events::JoinEvent,
events::LoginEvent,
events::LogoutEvent,
events::NetworkEvent,
events::NickEvent,
events::PartEvent,
events::PingEvent,
events::PmInitiateEvent,
events::SendEvent,
events::SnapshotEvent,
// Session commands
session_cmds::Auth,
session_cmds::AuthReply,
session_cmds::Ping,
session_cmds::PingReply,
// Chat room commands
room_cmds::GetMessage,
room_cmds::GetMessageReply,
room_cmds::Log,
room_cmds::LogReply,
room_cmds::Nick,
room_cmds::NickReply,
room_cmds::PmInitiate,
room_cmds::PmInitiateReply,
room_cmds::Send,
room_cmds::SendReply,
room_cmds::Who,
room_cmds::WhoReply,
// Account commands
account_cmds::ChangeEmail,
account_cmds::ChangeEmailReply,
account_cmds::ChangeName,
account_cmds::ChangeNameReply,
account_cmds::ChangePassword,
account_cmds::ChangePasswordReply,
account_cmds::Login,
account_cmds::LoginReply,
account_cmds::Logout,
account_cmds::LogoutReply,
account_cmds::RegisterAccount,
account_cmds::RegisterAccountReply,
account_cmds::ResendVerificationEmail,
account_cmds::ResendVerificationEmailReply,
account_cmds::ResetPassword,
account_cmds::ResetPasswordReply,
}
commands! {
// Session commands
Auth => AuthReply,
Ping => PingReply,
// Chat room commands
GetMessage => GetMessageReply,
Log => LogReply,
Nick => NickReply,
PmInitiate => PmInitiateReply,
Send => SendReply,
Who => WhoReply,
// Account commands
ChangeEmail => ChangeEmailReply,
ChangeName => ChangeNameReply,
ChangePassword => ChangePasswordReply,
Login => LoginReply,
Logout => LogoutReply,
RegisterAccount => RegisterAccountReply,
ResendVerificationEmail => ResendVerificationEmailReply,
ResetPassword => ResetPasswordReply,
}
/// A fully parsed and interpreted packet.
///
/// Compared to [`Packet`], this packet's representation more closely matches
/// the actual use of packets.
#[derive(Debug, Clone)]
pub struct ParsedPacket {
/// Client-generated id for associating replies with commands.
pub id: Option<String>,
/// The type of the command, reply, or event.
pub r#type: PacketType,
/// The payload of the command, reply, or event, or an error message if the
/// command failed.
pub content: Result<Data, String>,
/// A warning to the client that it may be flooding.
///
/// The client should slow down its command rate.
pub throttled: Option<String>,
}
impl ParsedPacket {
/// Convert a [`Packet`] into a [`ParsedPacket`].
///
/// This method may fail if the packet data is invalid.
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,
})
}
/// Convert a [`ParsedPacket`] into a [`Packet`].
///
/// This method may fail if the packet data fails to serialize.
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,
},
})
}
}
impl TryFrom<Packet> for ParsedPacket {
type Error = serde_json::Error;
fn try_from(value: Packet) -> Result<Self, Self::Error> {
Self::from_packet(value)
}
}
impl TryFrom<ParsedPacket> for Packet {
type Error = serde_json::Error;
fn try_from(value: ParsedPacket) -> Result<Self, Self::Error> {
value.into_packet()
}
}

View file

@ -1,13 +1,17 @@
//! Chat room commands.
//! Models [chat room commands][0] and their replies.
//!
//! These commands are available to the client once a session successfully joins
//! a room.
//!
//! [0]: https://euphoria.leet.nu/heim/api#chat-room-commands
use serde::{Deserialize, Serialize};
use super::{Message, MessageId, PmId, SessionId, SessionView, UserId};
/// Retrieve the full content of a single message in the room.
///
/// <https://euphoria.leet.nu/heim/api#get-message>
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetMessage {
/// The id of the message to retrieve.
@ -23,6 +27,8 @@ pub struct GetMessageReply(pub Message);
/// This can be used to supplement the log provided by
/// [`SnapshotEvent`](super::SnapshotEvent) (for example, when scrolling back
/// further in history).
///
/// <https://euphoria.leet.nu/heim/api#log>
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Log {
/// Maximum number of messages to return (up to 1000).
@ -44,6 +50,8 @@ pub struct LogReply {
///
/// This name applies to all messages sent during this session, until the nick
/// command is called again.
///
/// <https://euphoria.leet.nu/heim/api#nick>
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Nick {
/// The requested name (maximum length 36 bytes).
@ -68,6 +76,8 @@ pub struct NickReply {
/// Constructs a virtual room for private messaging between the client and the
/// given [`UserId`].
///
/// <https://euphoria.leet.nu/heim/api#pm-initiate>
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PmInitiate {
/// The id of the user to invite to chat privately.
@ -94,6 +104,8 @@ pub struct PmInitiateReply {
/// The caller of this command will not receive the corresponding
/// [`SendEvent`](super::SendEvent), but will receive the same information in
/// the [`SendReply`].
///
/// <https://euphoria.leet.nu/heim/api#send>
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Send {
/// The content of the message (client-defined).
@ -109,6 +121,8 @@ pub struct Send {
pub struct SendReply(pub Message);
/// Request a list of sessions currently joined in the room.
///
/// <https://euphoria.leet.nu/heim/api#who>
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Who {}

View file

@ -1,7 +1,9 @@
//! Session commands.
//! Models [session commands][0] and their replies.
//!
//! Session management commands are involved in the initial handshake and
//! maintenance of a session.
//!
//! [0]: https://euphoria.leet.nu/heim/api#session-commands
use serde::{Deserialize, Serialize};
@ -11,6 +13,8 @@ use super::{AuthOption, Time};
///
/// This should be sent in response to a [`BounceEvent`](super::BounceEvent) at
/// the beginning of a session.
///
/// <https://euphoria.leet.nu/heim/api#auth>
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Auth {
/// The method of authentication.
@ -32,6 +36,8 @@ pub struct AuthReply {
///
/// The server will send back a [`PingReply`] with the same timestamp as soon as
/// possible.
///
/// <https://euphoria.leet.nu/heim/api#ping>
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Ping {
/// An arbitrary value, intended to be a unix timestamp.

View file

@ -1,20 +1,16 @@
//! Field types.
//! Models the [field types][0].
//!
//! [0]: https://euphoria.leet.nu/heim/api#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::num::ParseIntError;
use std::str::FromStr;
use std::{error, fmt};
use std::{error, fmt, num::ParseIntError, str::FromStr};
use jiff::Timestamp;
use serde::{de, ser, Deserialize, Serialize};
use serde_json::Value;
/// Describes an account and its preferred name.
///
/// <https://euphoria.leet.nu/heim/api#accountview>
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AccountView {
/// The id of the account.
@ -24,6 +20,8 @@ pub struct AccountView {
}
/// Mode of authentication.
///
/// <https://euphoria.leet.nu/heim/api#authoption>
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum AuthOption {
@ -36,6 +34,8 @@ pub enum AuthOption {
///
/// It corresponds to a chat message, or a post, or any broadcasted event in a
/// room that should appear in the log.
///
/// <https://euphoria.leet.nu/heim/api#message>
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Message {
/// The id of the message (unique within a room).
@ -72,6 +72,8 @@ pub struct Message {
/// The type of a packet.
///
/// Not all of these types have their corresponding data modeled as a struct.
///
/// <https://euphoria.leet.nu/heim/api#packettype>
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum PacketType {
@ -250,6 +252,8 @@ impl fmt::Display for PacketType {
}
/// Describes an account to its owner.
///
/// <https://euphoria.leet.nu/heim/api#personalaccountview>
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PersonalAccountView {
/// The id of the account.
@ -261,6 +265,8 @@ pub struct PersonalAccountView {
}
/// Describes a session and its identity.
///
/// <https://euphoria.leet.nu/heim/api#sessionview>
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionView {
/// The id of an agent or account (or bot).
@ -290,6 +296,8 @@ pub struct SessionView {
/// 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.
///
/// <https://euphoria.leet.nu/heim/api#snowflake>
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct Snowflake(pub u64);
@ -307,7 +315,7 @@ impl Snowflake {
/// 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);
pub const MAX: Self = Self(i64::MAX as u64);
}
impl fmt::Display for Snowflake {
@ -324,6 +332,7 @@ impl fmt::Display for Snowflake {
}
}
/// An error that occurred while parsing a [`Snowflake`].
#[derive(Debug)]
pub enum ParseSnowflakeError {
InvalidLength(usize),
@ -365,7 +374,7 @@ impl FromStr for Snowflake {
return Err(ParseSnowflakeError::InvalidLength(s.len()));
}
let n = u64::from_str_radix(s, 36)?;
Ok(Snowflake(n))
Ok(Self(n))
}
}
@ -402,6 +411,8 @@ impl<'de> Deserialize<'de> for Snowflake {
/// Time is specified as a signed 64-bit integer, giving the number of seconds
/// since the Unix Epoch.
///
/// <https://euphoria.leet.nu/heim/api#time>
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Time(pub i64);
@ -426,6 +437,8 @@ impl Time {
///
/// It is possible for this value to have no prefix and colon, and there is no
/// fixed format for the unique value.
///
/// <https://euphoria.leet.nu/heim/api#userid>
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct UserId(pub String);
@ -435,21 +448,27 @@ impl fmt::Display for UserId {
}
}
/// What kind of user a [`UserId`] is.
#[derive(Debug, PartialEq, Eq)]
pub enum SessionType {
pub enum UserType {
Agent,
Account,
Bot,
}
impl UserId {
pub fn session_type(&self) -> Option<SessionType> {
/// Retrieve the [`UserType`] of this user.
///
/// This method can return [`None`] because user IDs used to have no
/// associated type. Such user IDs can still occur in old room logs, so
/// euphoxide supports them.
pub fn user_type(&self) -> Option<UserType> {
if self.0.starts_with("agent:") {
Some(SessionType::Agent)
Some(UserType::Agent)
} else if self.0.starts_with("account:") {
Some(SessionType::Account)
Some(UserType::Account)
} else if self.0.starts_with("bot:") {
Some(SessionType::Bot)
Some(UserType::Bot)
} else {
None
}

View file

@ -1,3 +1,4 @@
pub mod api;
mod emoji;
pub use crate::emoji::Emoji;