euphoxide/src/api/types.rs

487 lines
15 KiB
Rust

//! 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 jiff::Timestamp;
use serde::{de, ser, Deserialize, Serialize};
use serde_json::Value;
/// Describes an account and its preferred name.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AccountView {
/// The id of the account.
pub id: AccountId,
/// 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: MessageId,
/// The id of the message's parent, or null if top-level.
#[serde(skip_serializing_if = "Option::is_none")]
pub parent: Option<MessageId>,
/// 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: AccountId,
/// 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: SessionId,
/// 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 fmt::Display for Snowflake {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// 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;
}
f.write_str(&result)
}
}
#[derive(Debug)]
pub enum ParseSnowflakeError {
InvalidLength(usize),
ParseIntError(ParseIntError),
}
impl fmt::Display for ParseSnowflakeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidLength(l) => {
write!(f, "invalid length: expected 13 bytes, got {l}")
}
Self::ParseIntError(from) => write!(f, "{from}"),
}
}
}
impl error::Error for ParseSnowflakeError {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match self {
Self::InvalidLength(_) => None,
Self::ParseIntError(from) => Some(from),
}
}
}
impl From<ParseIntError> for ParseSnowflakeError {
fn from(err: ParseIntError) -> Self {
Self::ParseIntError(err)
}
}
impl FromStr for Snowflake {
type Err = ParseSnowflakeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
// Convert base36 string to u64
if s.len() != 13 {
return Err(ParseSnowflakeError::InvalidLength(s.len()));
}
let n = u64::from_str_radix(s, 36)?;
Ok(Snowflake(n))
}
}
impl Serialize for Snowflake {
fn serialize<S: ser::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
format!("{self}").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> {
v.parse().map_err(|e| match e {
ParseSnowflakeError::InvalidLength(len) => E::invalid_length(len, &self),
ParseSnowflakeError::ParseIntError(_) => {
E::invalid_value(de::Unexpected::Str(v), &self)
}
})
}
}
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(pub i64);
impl Time {
pub fn from_timestamp(time: Timestamp) -> Self {
Self(time.as_second())
}
pub fn as_timestamp(&self) -> Timestamp {
Timestamp::from_second(self.0).unwrap()
}
pub fn now() -> Self {
Self::from_timestamp(Timestamp::now())
}
}
/// 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, PartialOrd, Ord, Serialize, Deserialize)]
pub struct UserId(pub String);
impl fmt::Display for UserId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[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
}
}
}
/// Identifies an account.
///
/// This type is a wrapper around [`Snowflake`] meant for type safety. It is not
/// specified in the euphoria API itself.
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct AccountId(pub Snowflake);
/// Identifies a message.
///
/// This type is a wrapper around [`Snowflake`] meant for type safety. It is not
/// specified in the euphoria API itself.
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct MessageId(pub Snowflake);
/// Identifies a private room.
///
/// This type is a wrapper around [`Snowflake`] meant for type safety. It is not
/// specified in the euphoria API itself.
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct PmId(pub Snowflake);
/// Identifies a session.
///
/// This type is a wrapper around [`String`] meant for type safety. It is not
/// specified in the euphoria API itself.
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct SessionId(pub String);
// TODO Find out if an edit id is a MessageId or if it deserves a wrapper