use serde::{Deserialize, Serialize};
use crate::envelope::{Ack, ServerPush, WsCall};
use crate::error::{ApiError, ValidationDetails};
use crate::ids::{MessageId, RoomId, UserId};
use crate::kinds;
use crate::page::{Page, PageRequest};
use crate::text;
use crate::time::UnixMillis;
pub const DEFAULT_MAX_TEXT_CHARS: usize = 500;
pub const DEFAULT_RATE_MESSAGES: u32 = 5;
pub const DEFAULT_RATE_WINDOW_SECS: u32 = 10;
pub const DEFAULT_HISTORY_RETENTION_DAYS: u32 = 30;
pub const DEFAULT_MAX_ROOM_MEMBERS: u32 = 200;
pub const DEFAULT_MAX_JOINED_ROOMS: u32 = 16;
pub const MAX_ROOM_KEY_BYTES: usize = 64;
pub const MAX_NONCE_BYTES: usize = 64;
pub fn is_valid_room_key(key: &str) -> bool {
let bytes = key.as_bytes();
bytes.len() <= MAX_ROOM_KEY_BYTES
&& bytes.first().is_some_and(|b| b.is_ascii_lowercase() || b.is_ascii_digit())
&& bytes.iter().all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || matches!(b, b'_' | b'-' | b'.'))
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(untagged)]
#[non_exhaustive]
pub enum RoomRef {
Id(RoomId),
Key(String),
}
impl From<RoomId> for RoomRef {
fn from(id: RoomId) -> Self {
RoomRef::Id(id)
}
}
impl From<&str> for RoomRef {
fn from(key: &str) -> Self {
RoomRef::Key(key.to_string())
}
}
impl From<String> for RoomRef {
fn from(key: String) -> Self {
RoomRef::Key(key)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum RoomKind {
Room,
Dm,
Group,
Player,
#[serde(other)]
Unknown,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct RoomInfo {
pub id: RoomId,
pub kind: RoomKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub key: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub member_count: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_members: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub peer: Option<UserId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub visibility: Option<RoomVisibility>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub owner: Option<UserId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub role: Option<RoomRole>,
}
impl RoomInfo {
pub fn new(id: RoomId, kind: RoomKind) -> Self {
Self { id, kind, key: None, name: None, member_count: None, max_members: None, peer: None, visibility: None, owner: None, role: None }
}
pub fn with_visibility(mut self, visibility: RoomVisibility) -> Self {
self.visibility = Some(visibility);
self
}
pub fn with_owner(mut self, owner: UserId) -> Self {
self.owner = Some(owner);
self
}
pub fn with_role(mut self, role: RoomRole) -> Self {
self.role = Some(role);
self
}
pub fn with_peer(mut self, peer: UserId) -> Self {
self.peer = Some(peer);
self
}
pub fn with_key(mut self, key: impl Into<String>) -> Self {
self.key = Some(key.into());
self
}
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn with_members(mut self, member_count: Option<u32>, max_members: Option<u32>) -> Self {
self.member_count = member_count;
self.max_members = max_members;
self
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct JoinRoom {
pub room: RoomRef,
}
impl JoinRoom {
pub fn new(room: impl Into<RoomRef>) -> Self {
Self { room: room.into() }
}
pub fn validate(&self) -> Result<(), ApiError> {
let mut details = ValidationDetails::new();
if let RoomRef::Key(key) = &self.room {
if !is_valid_room_key(key) {
details.add("room", format!("is not a room key (1 to {MAX_ROOM_KEY_BYTES} bytes of a-z, 0-9, _ - .)"));
}
}
details.into_result()
}
}
impl WsCall for JoinRoom {
type Response = RoomInfo;
const KIND: &'static str = kinds::CHAT_JOIN;
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct LeaveRoom {
pub room: RoomId,
}
impl LeaveRoom {
pub fn new(room: RoomId) -> Self {
Self { room }
}
}
impl WsCall for LeaveRoom {
type Response = Ack;
const KIND: &'static str = kinds::CHAT_LEAVE;
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct SendMessage {
pub room: RoomId,
pub text: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub nonce: Option<String>,
}
impl SendMessage {
pub fn new(room: RoomId, text: impl Into<String>) -> Self {
Self { room, text: text.into(), nonce: None }
}
pub fn with_nonce(mut self, nonce: impl Into<String>) -> Self {
self.nonce = Some(nonce.into());
self
}
pub fn validate(&self, max_chars: usize) -> Result<(), ApiError> {
let mut details = ValidationDetails::new();
if self.text.trim().is_empty() {
details.add("text", "is empty");
}
if self.text.chars().count() > max_chars {
details.add("text", format!("is longer than {max_chars} characters"));
}
if let Some(problem) = text::message_problem(&self.text) {
details.add("text", problem);
}
if let Some(nonce) = &self.nonce {
if nonce.is_empty() || nonce.len() > MAX_NONCE_BYTES || !nonce.bytes().all(|b| b.is_ascii_graphic()) {
details.add("nonce", format!("must be 1 to {MAX_NONCE_BYTES} visible ASCII characters"));
}
}
details.into_result()
}
}
impl WsCall for SendMessage {
type Response = SendAck;
const KIND: &'static str = kinds::CHAT_SEND;
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct SendAck {
pub message_id: MessageId,
pub sent_at: UnixMillis,
}
impl SendAck {
pub fn new(message_id: MessageId, sent_at: UnixMillis) -> Self {
Self { message_id, sent_at }
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ChatHistory {
pub room: RoomId,
#[serde(flatten)]
pub page: PageRequest,
}
impl ChatHistory {
pub fn new(room: RoomId) -> Self {
Self { room, page: PageRequest::first() }
}
pub fn with_page(mut self, page: PageRequest) -> Self {
self.page = page;
self
}
}
impl WsCall for ChatHistory {
type Response = Page<ChatMessage>;
const KIND: &'static str = kinds::CHAT_HISTORY;
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ChatMessage {
pub id: MessageId,
pub room: RoomId,
pub sender: UserId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sender_name: Option<String>,
pub text: String,
pub sent_at: UnixMillis,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub nonce: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub edited_at: Option<UnixMillis>,
}
impl ChatMessage {
pub fn new(id: MessageId, room: RoomId, sender: UserId, text: impl Into<String>, sent_at: UnixMillis) -> Self {
Self { id, room, sender, sender_name: None, text: text.into(), sent_at, nonce: None, edited_at: None }
}
pub fn with_edited_at(mut self, at: UnixMillis) -> Self {
self.edited_at = Some(at);
self
}
pub fn with_nonce(mut self, nonce: impl Into<String>) -> Self {
self.nonce = Some(nonce.into());
self
}
pub fn with_sender_name(mut self, name: impl Into<String>) -> Self {
self.sender_name = Some(name.into());
self
}
}
impl ServerPush for ChatMessage {
const KIND: &'static str = kinds::CHAT_MESSAGE;
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct MessageDeleted {
pub id: MessageId,
pub room: RoomId,
}
impl MessageDeleted {
pub fn new(id: MessageId, room: RoomId) -> Self {
Self { id, room }
}
}
impl ServerPush for MessageDeleted {
const KIND: &'static str = kinds::CHAT_DELETED;
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct OpenDirect {
pub user: UserId,
}
impl OpenDirect {
pub fn new(user: UserId) -> Self {
Self { user }
}
}
pub const DEFAULT_PRESENCE_MAX_MEMBERS: u32 = 100;
pub const DEFAULT_PRESENCE_PER_SECOND: u32 = 10;
pub const MAX_LISTED_MEMBERS: u32 = 200;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ListMembers {
pub room: RoomId,
}
impl ListMembers {
pub fn new(room: RoomId) -> Self {
Self { room }
}
}
impl WsCall for ListMembers {
type Response = RoomMembers;
const KIND: &'static str = kinds::CHAT_MEMBERS;
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct RoomMember {
pub user: UserId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
impl RoomMember {
pub fn new(user: UserId) -> Self {
Self { user, name: None }
}
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct RoomMembers {
pub room: RoomId,
pub members: Vec<RoomMember>,
pub count: u32,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub truncated: bool,
}
impl RoomMembers {
pub fn new(room: RoomId, members: Vec<RoomMember>, count: u32) -> Self {
Self { room, members, count, truncated: false }
}
pub fn truncated(mut self) -> Self {
self.truncated = true;
self
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum PresenceEvent {
Joined,
Left,
#[serde(other)]
Unknown,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Presence {
pub room: RoomId,
pub user: UserId,
pub event: PresenceEvent,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub count: Option<u32>,
}
impl Presence {
pub fn new(room: RoomId, user: UserId, event: PresenceEvent) -> Self {
Self { room, user, event, name: None, count: None }
}
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn with_count(mut self, count: u32) -> Self {
self.count = Some(count);
self
}
}
impl ServerPush for Presence {
const KIND: &'static str = kinds::CHAT_PRESENCE;
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct MessageEdit {
pub text: String,
}
impl MessageEdit {
pub fn new(text: impl Into<String>) -> Self {
Self { text: text.into() }
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct EditMessage {
pub room: RoomId,
pub message: MessageId,
#[serde(flatten)]
pub edit: MessageEdit,
}
impl EditMessage {
pub fn new(room: RoomId, message: MessageId, text: impl Into<String>) -> Self {
Self { room, message, edit: MessageEdit::new(text) }
}
pub fn validate(&self, max_chars: usize) -> Result<(), ApiError> {
SendMessage::new(self.room, self.edit.text.clone()).validate(max_chars)
}
}
impl WsCall for EditMessage {
type Response = ChatMessage;
const KIND: &'static str = kinds::CHAT_EDIT;
}
pub const DEFAULT_EDIT_WINDOW_SECS: u32 = 900;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct MessageEdited {
pub id: MessageId,
pub room: RoomId,
pub text: String,
pub edited_at: UnixMillis,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub edited_by: Option<UserId>,
}
impl MessageEdited {
pub fn new(id: MessageId, room: RoomId, text: impl Into<String>, edited_at: UnixMillis) -> Self {
Self { id, room, text: text.into(), edited_at, edited_by: None }
}
pub fn by(mut self, user: UserId) -> Self {
self.edited_by = Some(user);
self
}
}
impl ServerPush for MessageEdited {
const KIND: &'static str = kinds::CHAT_EDITED;
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ReadUpTo {
pub message: MessageId,
}
impl ReadUpTo {
pub fn new(message: MessageId) -> Self {
Self { message }
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct MarkRead {
pub room: RoomId,
#[serde(flatten)]
pub read: ReadUpTo,
}
impl MarkRead {
pub fn new(room: RoomId, message: MessageId) -> Self {
Self { room, read: ReadUpTo::new(message) }
}
}
impl WsCall for MarkRead {
type Response = Ack;
const KIND: &'static str = kinds::CHAT_MARK_READ;
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ReadReceipt {
pub room: RoomId,
pub user: UserId,
pub message: MessageId,
pub read_at: UnixMillis,
}
impl ReadReceipt {
pub fn new(room: RoomId, user: UserId, message: MessageId, read_at: UnixMillis) -> Self {
Self { room, user, message, read_at }
}
}
impl ServerPush for ReadReceipt {
const KIND: &'static str = kinds::CHAT_READ;
}
pub const MAX_LISTED_RECEIPTS: u32 = 200;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ListReceipts {
pub room: RoomId,
}
impl ListReceipts {
pub fn new(room: RoomId) -> Self {
Self { room }
}
}
impl WsCall for ListReceipts {
type Response = ReadReceipts;
const KIND: &'static str = kinds::CHAT_RECEIPTS;
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ReadReceipts {
pub room: RoomId,
pub receipts: Vec<ReadReceipt>,
}
impl ReadReceipts {
pub fn new(room: RoomId, receipts: Vec<ReadReceipt>) -> Self {
Self { room, receipts }
}
}
pub const MAX_UNREAD_ROOMS: usize = 100;
pub const MAX_UNREAD_COUNT: u32 = 1000;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct UnreadQuery {
pub rooms: Vec<RoomId>,
}
impl UnreadQuery {
pub fn new(rooms: Vec<RoomId>) -> Self {
Self { rooms }
}
pub fn validate(&self) -> Result<(), ApiError> {
let mut details = ValidationDetails::new();
if self.rooms.is_empty() || self.rooms.len() > MAX_UNREAD_ROOMS {
details.add("rooms", format!("must name 1 to {MAX_UNREAD_ROOMS} rooms"));
}
details.into_result()
}
}
impl WsCall for UnreadQuery {
type Response = UnreadCounts;
const KIND: &'static str = kinds::CHAT_UNREAD;
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct UnreadCount {
pub room: RoomId,
pub unread: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_read: Option<MessageId>,
}
impl UnreadCount {
pub fn new(room: RoomId, unread: u32, last_read: Option<MessageId>) -> Self {
Self { room, unread, last_read }
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct UnreadCounts {
pub rooms: Vec<UnreadCount>,
}
impl UnreadCounts {
pub fn new(rooms: Vec<UnreadCount>) -> Self {
Self { rooms }
}
}
pub const DEFAULT_TYPING_TTL_MS: u32 = 6000;
pub const DEFAULT_TYPING_INTERVAL_MS: u32 = 3000;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct SetTyping {
pub room: RoomId,
pub typing: bool,
}
impl SetTyping {
pub fn started(room: RoomId) -> Self {
Self { room, typing: true }
}
pub fn stopped(room: RoomId) -> Self {
Self { room, typing: false }
}
}
impl WsCall for SetTyping {
type Response = Ack;
const KIND: &'static str = kinds::CHAT_SET_TYPING;
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct TypingUpdate {
pub room: RoomId,
pub user: UserId,
pub typing: bool,
pub expires_in_ms: u32,
}
impl TypingUpdate {
pub fn new(room: RoomId, user: UserId, typing: bool, expires_in_ms: u32) -> Self {
Self { room, user, typing, expires_in_ms }
}
}
impl ServerPush for TypingUpdate {
const KIND: &'static str = kinds::CHAT_TYPING;
}
pub const MAX_ROOM_NAME_CHARS: usize = 64;
pub fn room_name_problem(name: &str) -> Option<String> {
if name.trim().is_empty() {
return Some("is empty".into());
}
if name.chars().count() > MAX_ROOM_NAME_CHARS {
return Some(format!("is longer than {MAX_ROOM_NAME_CHARS} characters"));
}
text::name_problem(name).map(str::to_string)
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum RoomVisibility {
Public,
#[default]
Private,
#[serde(other)]
Unknown,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum RoomRole {
Owner,
Moderator,
Member,
Invited,
Banned,
#[serde(other)]
Unknown,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct CreateRoom {
pub name: String,
#[serde(default)]
pub visibility: RoomVisibility,
}
impl CreateRoom {
pub fn new(name: impl Into<String>) -> Self {
Self { name: name.into(), visibility: RoomVisibility::Private }
}
pub fn public(mut self) -> Self {
self.visibility = RoomVisibility::Public;
self
}
pub fn validate(&self) -> Result<(), ApiError> {
let mut details = ValidationDetails::new();
if let Some(problem) = room_name_problem(&self.name) {
details.add("name", problem);
}
if self.visibility == RoomVisibility::Unknown {
details.add("visibility", "must be public or private");
}
details.into_result()
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct UpdateRoom {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub visibility: Option<RoomVisibility>,
}
impl UpdateRoom {
pub fn new() -> Self {
Self::default()
}
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn with_visibility(mut self, visibility: RoomVisibility) -> Self {
self.visibility = Some(visibility);
self
}
pub fn validate(&self) -> Result<(), ApiError> {
let mut details = ValidationDetails::new();
if self.name.is_none() && self.visibility.is_none() {
details.add("name", "give a name or a visibility");
}
if let Some(problem) = self.name.as_deref().and_then(room_name_problem) {
details.add("name", problem);
}
if self.visibility == Some(RoomVisibility::Unknown) {
details.add("visibility", "must be public or private");
}
details.into_result()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct RoomUser {
pub user: UserId,
}
impl RoomUser {
pub fn new(user: UserId) -> Self {
Self { user }
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct RoomRoleChange {
pub role: RoomRole,
}
impl RoomRoleChange {
pub fn new(role: RoomRole) -> Self {
Self { role }
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct RoomMembership {
pub user: UserId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
pub role: RoomRole,
pub since: UnixMillis,
}
impl RoomMembership {
pub fn new(user: UserId, role: RoomRole, since: UnixMillis) -> Self {
Self { user, name: None, role, since }
}
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum RoomChange {
Updated,
Deleted,
Invited,
Joined,
Left,
Kicked,
Role,
Owner,
#[serde(other)]
Unknown,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct RoomUpdate {
pub room: RoomId,
pub change: RoomChange,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub user: Option<UserId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub by: Option<UserId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub role: Option<RoomRole>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub info: Option<RoomInfo>,
}
impl RoomUpdate {
pub fn new(room: RoomId, change: RoomChange) -> Self {
Self { room, change, user: None, by: None, role: None, info: None }
}
pub fn about(mut self, user: UserId) -> Self {
self.user = Some(user);
self
}
pub fn by(mut self, user: UserId) -> Self {
self.by = Some(user);
self
}
pub fn with_role(mut self, role: RoomRole) -> Self {
self.role = Some(role);
self
}
pub fn with_info(mut self, info: RoomInfo) -> Self {
self.info = Some(info);
self
}
}
impl ServerPush for RoomUpdate {
const KIND: &'static str = kinds::CHAT_ROOM;
}
mod calls {
use super::*;
use crate::http_call::{payload_call, HttpCall, NoPayload, PathParams, PayloadKind, NO_PAYLOAD};
use crate::routes::{self, HttpMethod, Route};
payload_call!(OpenDirect, Post, routes::chat::DM, true, Json, RoomInfo);
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct ListRooms {
pub page: PageRequest,
}
impl ListRooms {
pub fn new() -> Self {
Self::default()
}
pub fn with_page(mut self, page: PageRequest) -> Self {
self.page = page;
self
}
}
impl HttpCall for ListRooms {
type Payload = PageRequest;
type Response = Page<RoomInfo>;
const ROUTE: Route = Route::new(HttpMethod::Get, routes::chat::ROOMS, true);
const PAYLOAD: PayloadKind = PayloadKind::Query;
fn payload(&self) -> &PageRequest {
&self.page
}
fn from_parts(_params: &PathParams, page: PageRequest) -> Result<Self, ApiError> {
Ok(Self::new().with_page(page))
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct ListDirects {
pub page: PageRequest,
}
impl ListDirects {
pub fn new() -> Self {
Self::default()
}
pub fn with_page(mut self, page: PageRequest) -> Self {
self.page = page;
self
}
}
impl HttpCall for ListDirects {
type Payload = PageRequest;
type Response = Page<RoomInfo>;
const ROUTE: Route = Route::new(HttpMethod::Get, routes::chat::DMS, true);
const PAYLOAD: PayloadKind = PayloadKind::Query;
fn payload(&self) -> &PageRequest {
&self.page
}
fn from_parts(_params: &PathParams, page: PageRequest) -> Result<Self, ApiError> {
Ok(Self::new().with_page(page))
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct ListMessages {
pub room: RoomId,
pub page: PageRequest,
}
impl ListMessages {
pub fn new(room: RoomId) -> Self {
Self { room, page: PageRequest::first() }
}
pub fn with_page(mut self, page: PageRequest) -> Self {
self.page = page;
self
}
}
impl HttpCall for ListMessages {
type Payload = PageRequest;
type Response = Page<ChatMessage>;
const ROUTE: Route = Route::new(HttpMethod::Get, routes::chat::HISTORY, true);
const PAYLOAD: PayloadKind = PayloadKind::Query;
fn payload(&self) -> &PageRequest {
&self.page
}
fn path_params(&self) -> PathParams {
PathParams::new().with("room", self.room)
}
fn from_parts(params: &PathParams, page: PageRequest) -> Result<Self, ApiError> {
Ok(Self::new(params.id("room")?).with_page(page))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct DeleteMessage {
pub room: RoomId,
pub message: MessageId,
}
impl DeleteMessage {
pub fn new(room: RoomId, message: MessageId) -> Self {
Self { room, message }
}
}
impl HttpCall for DeleteMessage {
type Payload = NoPayload;
type Response = Ack;
const ROUTE: Route = Route::new(HttpMethod::Delete, routes::chat::MESSAGE, true);
const PAYLOAD: PayloadKind = PayloadKind::Empty;
fn payload(&self) -> &NoPayload {
&NO_PAYLOAD
}
fn path_params(&self) -> PathParams {
PathParams::new().with("room", self.room).with("message", self.message)
}
fn from_parts(params: &PathParams, _payload: NoPayload) -> Result<Self, ApiError> {
Ok(Self::new(params.id("room")?, params.id("message")?))
}
}
payload_call!(CreateRoom, Post, routes::chat::ROOMS, true, Json, RoomInfo);
payload_call!(UnreadQuery, Post, routes::chat::UNREAD, true, Json, UnreadCounts);
impl HttpCall for EditMessage {
type Payload = MessageEdit;
type Response = ChatMessage;
const ROUTE: Route = Route::new(HttpMethod::Patch, routes::chat::MESSAGE, true);
const PAYLOAD: PayloadKind = PayloadKind::Json;
fn payload(&self) -> &MessageEdit {
&self.edit
}
fn path_params(&self) -> PathParams {
PathParams::new().with("room", self.room).with("message", self.message)
}
fn from_parts(params: &PathParams, edit: MessageEdit) -> Result<Self, ApiError> {
Ok(Self::new(params.id("room")?, params.id("message")?, edit.text))
}
}
impl HttpCall for MarkRead {
type Payload = ReadUpTo;
type Response = Ack;
const ROUTE: Route = Route::new(HttpMethod::Put, routes::chat::READ, true);
const PAYLOAD: PayloadKind = PayloadKind::Json;
fn payload(&self) -> &ReadUpTo {
&self.read
}
fn path_params(&self) -> PathParams {
PathParams::new().with("room", self.room)
}
fn from_parts(params: &PathParams, read: ReadUpTo) -> Result<Self, ApiError> {
Ok(Self::new(params.id("room")?, read.message))
}
}
impl HttpCall for ListReceipts {
type Payload = NoPayload;
type Response = ReadReceipts;
const ROUTE: Route = Route::new(HttpMethod::Get, routes::chat::RECEIPTS, true);
const PAYLOAD: PayloadKind = PayloadKind::Empty;
fn payload(&self) -> &NoPayload {
&NO_PAYLOAD
}
fn path_params(&self) -> PathParams {
PathParams::new().with("room", self.room)
}
fn from_parts(params: &PathParams, _payload: NoPayload) -> Result<Self, ApiError> {
Ok(Self::new(params.id("room")?))
}
}
macro_rules! page_call {
($(#[$meta:meta])* $name:ident, $path:expr, $item:ty) => {
$(#[$meta])*
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct $name {
/// Which page.
pub page: PageRequest,
}
impl $name {
pub fn new() -> Self {
Self::default()
}
pub fn with_page(mut self, page: PageRequest) -> Self {
self.page = page;
self
}
}
impl HttpCall for $name {
type Payload = PageRequest;
type Response = Page<$item>;
const ROUTE: Route = Route::new(HttpMethod::Get, $path, true);
const PAYLOAD: PayloadKind = PayloadKind::Query;
fn payload(&self) -> &PageRequest {
&self.page
}
fn from_parts(_params: &PathParams, page: PageRequest) -> Result<Self, ApiError> {
Ok(Self::new().with_page(page))
}
}
};
}
page_call!(
MyRooms,
routes::chat::ROOMS_MINE,
RoomInfo
);
page_call!(
PublicRooms,
routes::chat::ROOMS_PUBLIC,
RoomInfo
);
macro_rules! room_call {
($(#[$meta:meta])* $name:ident, $method:ident, $path:expr, $response:ty) => {
$(#[$meta])*
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct $name {
/// The room.
pub room: RoomId,
}
impl $name {
pub fn new(room: RoomId) -> Self {
Self { room }
}
}
impl HttpCall for $name {
type Payload = NoPayload;
type Response = $response;
const ROUTE: Route = Route::new(HttpMethod::$method, $path, true);
const PAYLOAD: PayloadKind = PayloadKind::Empty;
fn payload(&self) -> &NoPayload {
&NO_PAYLOAD
}
fn path_params(&self) -> PathParams {
PathParams::new().with("room", self.room)
}
fn from_parts(params: &PathParams, _payload: NoPayload) -> Result<Self, ApiError> {
Ok(Self::new(params.id("room")?))
}
}
};
}
room_call!(
GetRoom,
Get,
routes::chat::ROOM,
RoomInfo
);
room_call!(
DeleteRoom,
Delete,
routes::chat::ROOM,
Ack
);
room_call!(
JoinChatRoom,
Post,
routes::chat::ROOM_JOIN,
RoomInfo
);
room_call!(
LeaveChatRoom,
Post,
routes::chat::ROOM_LEAVE,
Ack
);
macro_rules! room_body_call {
($(#[$meta:meta])* $name:ident, $method:ident, $path:expr, $body:ident, $field:ident, $response:ty) => {
$(#[$meta])*
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct $name {
/// The room.
pub room: RoomId,
pub $field: $body,
}
impl $name {
pub fn new(room: RoomId, $field: $body) -> Self {
Self { room, $field }
}
}
impl HttpCall for $name {
type Payload = $body;
type Response = $response;
const ROUTE: Route = Route::new(HttpMethod::$method, $path, true);
const PAYLOAD: PayloadKind = PayloadKind::Json;
fn payload(&self) -> &$body {
&self.$field
}
fn path_params(&self) -> PathParams {
PathParams::new().with("room", self.room)
}
fn from_parts(params: &PathParams, $field: $body) -> Result<Self, ApiError> {
Ok(Self::new(params.id("room")?, $field))
}
}
};
}
room_body_call!(
EditRoom,
Patch,
routes::chat::ROOM,
UpdateRoom,
update,
RoomInfo
);
room_body_call!(
InviteToRoom,
Post,
routes::chat::ROOM_INVITES,
RoomUser,
invitee,
Ack
);
room_body_call!(
TransferRoom,
Post,
routes::chat::ROOM_OWNER,
RoomUser,
to,
Ack
);
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct ListRoomMembers {
pub room: RoomId,
pub page: PageRequest,
}
impl ListRoomMembers {
pub fn new(room: RoomId) -> Self {
Self { room, page: PageRequest::first() }
}
pub fn with_page(mut self, page: PageRequest) -> Self {
self.page = page;
self
}
}
impl HttpCall for ListRoomMembers {
type Payload = PageRequest;
type Response = Page<RoomMembership>;
const ROUTE: Route = Route::new(HttpMethod::Get, routes::chat::ROOM_MEMBERS, true);
const PAYLOAD: PayloadKind = PayloadKind::Query;
fn payload(&self) -> &PageRequest {
&self.page
}
fn path_params(&self) -> PathParams {
PathParams::new().with("room", self.room)
}
fn from_parts(params: &PathParams, page: PageRequest) -> Result<Self, ApiError> {
Ok(Self::new(params.id("room")?).with_page(page))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct KickFromRoom {
pub room: RoomId,
pub user: UserId,
}
impl KickFromRoom {
pub fn new(room: RoomId, user: UserId) -> Self {
Self { room, user }
}
}
impl HttpCall for KickFromRoom {
type Payload = NoPayload;
type Response = Ack;
const ROUTE: Route = Route::new(HttpMethod::Delete, routes::chat::ROOM_MEMBER, true);
const PAYLOAD: PayloadKind = PayloadKind::Empty;
fn payload(&self) -> &NoPayload {
&NO_PAYLOAD
}
fn path_params(&self) -> PathParams {
PathParams::new().with("room", self.room).with("user", self.user)
}
fn from_parts(params: &PathParams, _payload: NoPayload) -> Result<Self, ApiError> {
Ok(Self::new(params.id("room")?, params.id("user")?))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct SetRoomRole {
pub room: RoomId,
pub user: UserId,
pub change: RoomRoleChange,
}
impl SetRoomRole {
pub fn new(room: RoomId, user: UserId, role: RoomRole) -> Self {
Self { room, user, change: RoomRoleChange::new(role) }
}
}
impl HttpCall for SetRoomRole {
type Payload = RoomRoleChange;
type Response = Ack;
const ROUTE: Route = Route::new(HttpMethod::Put, routes::chat::ROOM_ROLE, true);
const PAYLOAD: PayloadKind = PayloadKind::Json;
fn payload(&self) -> &RoomRoleChange {
&self.change
}
fn path_params(&self) -> PathParams {
PathParams::new().with("room", self.room).with("user", self.user)
}
fn from_parts(params: &PathParams, change: RoomRoleChange) -> Result<Self, ApiError> {
Ok(Self::new(params.id("room")?, params.id("user")?, change.role))
}
}
}
pub use calls::{
DeleteMessage, DeleteRoom, EditRoom, GetRoom, InviteToRoom, JoinChatRoom, KickFromRoom, LeaveChatRoom, ListDirects, ListMessages, ListRoomMembers,
ListRooms, MyRooms, PublicRooms, SetRoomRole, TransferRoom,
};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn room_ref_json() {
assert_eq!(serde_json::to_string(&JoinRoom::new("world")).ok().as_deref(), Some(r#"{"room":"world"}"#));
assert_eq!(serde_json::to_string(&JoinRoom::new(RoomId(12))).ok().as_deref(), Some(r#"{"room":12}"#));
assert_eq!(serde_json::from_str::<JoinRoom>(r#"{"room":12}"#).ok(), Some(JoinRoom::new(RoomId(12))));
assert_eq!(serde_json::from_str::<JoinRoom>(r#"{"room":"trade"}"#).ok(), Some(JoinRoom::new("trade")));
assert!(serde_json::from_str::<JoinRoom>(r#"{"room":true}"#).is_err());
assert!(JoinRoom::new("").validate().is_err());
assert!(JoinRoom::new("k".repeat(MAX_ROOM_KEY_BYTES + 1)).validate().is_err());
assert!(JoinRoom::new("world").validate().is_ok());
for bad in ["a b/c", "\0", "World", "-x", "w\u{200B}"] {
assert!(JoinRoom::new(bad).validate().is_err(), "{bad:?}");
}
}
#[test]
fn send_rules() {
assert!(SendMessage::new(RoomId(1), "hi").validate(DEFAULT_MAX_TEXT_CHARS).is_ok());
assert!(SendMessage::new(RoomId(1), " ").validate(DEFAULT_MAX_TEXT_CHARS).is_err());
assert!(SendMessage::new(RoomId(1), "é".repeat(DEFAULT_MAX_TEXT_CHARS)).validate(DEFAULT_MAX_TEXT_CHARS).is_ok());
assert!(SendMessage::new(RoomId(1), "é".repeat(DEFAULT_MAX_TEXT_CHARS + 1)).validate(DEFAULT_MAX_TEXT_CHARS).is_err());
assert!(SendMessage::new(RoomId(1), "two\nlines").validate(DEFAULT_MAX_TEXT_CHARS).is_ok());
assert!(SendMessage::new(RoomId(1), "x").with_nonce("n-1").validate(DEFAULT_MAX_TEXT_CHARS).is_ok());
for nonce in ["", "has space", &"n".repeat(MAX_NONCE_BYTES + 1)] {
assert!(SendMessage::new(RoomId(1), "x").with_nonce(nonce).validate(DEFAULT_MAX_TEXT_CHARS).is_err(), "{nonce:?}");
}
}
#[test]
fn history_flattens_the_page() {
let request = ChatHistory::new(RoomId(4)).with_page(PageRequest::after(crate::Cursor::new("c1")).with_limit(20));
assert_eq!(serde_json::to_string(&request).ok().as_deref(), Some(r#"{"room":4,"cursor":"c1","limit":20}"#));
assert_eq!(serde_json::to_string(&ChatHistory::new(RoomId(4))).ok().as_deref(), Some(r#"{"room":4}"#));
assert_eq!(serde_json::from_str::<RoomKind>("\"guild_hall\"").ok(), Some(RoomKind::Unknown));
}
#[test]
fn extras_json_and_rules() {
let edit = EditMessage::new(RoomId(12), MessageId(981), "hello again");
assert_eq!(serde_json::to_string(&edit).ok().as_deref(), Some(r#"{"room":12,"message":981,"text":"hello again"}"#));
assert_eq!(serde_json::from_str::<EditMessage>(r#"{"room":12,"message":981,"text":"hello again"}"#).ok(), Some(edit.clone()));
assert!(edit.validate(DEFAULT_MAX_TEXT_CHARS).is_ok() && EditMessage::new(RoomId(1), MessageId(1), " ").validate(DEFAULT_MAX_TEXT_CHARS).is_err());
assert_eq!(serde_json::to_string(&MarkRead::new(RoomId(12), MessageId(9))).ok().as_deref(), Some(r#"{"room":12,"message":9}"#));
assert!(UnreadQuery::new(vec![]).validate().is_err());
assert!(UnreadQuery::new(vec![RoomId(1); MAX_UNREAD_ROOMS + 1]).validate().is_err());
assert!(UnreadQuery::new(vec![RoomId(1)]).validate().is_ok());
assert_eq!(serde_json::to_string(&UnreadCount::new(RoomId(1), 3, None)).ok().as_deref(), Some(r#"{"room":1,"unread":3}"#));
assert_eq!(serde_json::to_string(&SetTyping::stopped(RoomId(4))).ok().as_deref(), Some(r#"{"room":4,"typing":false}"#));
let message = ChatMessage::new(MessageId(1), RoomId(2), UserId(3), "x", UnixMillis(5));
assert!(!serde_json::to_string(&message).unwrap_or_default().contains("edited_at"));
assert!(serde_json::to_string(&message.with_edited_at(UnixMillis(6))).unwrap_or_default().contains(r#""edited_at":6"#));
assert_eq!(serde_json::from_str::<CreateRoom>(r#"{"name":"Den"}"#).ok(), Some(CreateRoom::new("Den")));
assert_eq!(serde_json::to_string(&CreateRoom::new("Den").public()).ok().as_deref(), Some(r#"{"name":"Den","visibility":"public"}"#));
for bad in ["", " ", &"x".repeat(MAX_ROOM_NAME_CHARS + 1), "a\u{202E}b"] {
assert!(CreateRoom::new(bad).validate().is_err(), "{bad:?}");
}
let unknown: CreateRoom = serde_json::from_str(r#"{"name":"Den","visibility":"secret"}"#).unwrap_or_else(|_| CreateRoom::new("x"));
assert_eq!(unknown.visibility, RoomVisibility::Unknown);
assert!(unknown.validate().is_err());
assert!(UpdateRoom::new().validate().is_err());
assert!(UpdateRoom::new().with_visibility(RoomVisibility::Public).validate().is_ok());
assert_eq!(serde_json::from_str::<RoomKind>("\"player\"").ok(), Some(RoomKind::Player));
assert_eq!(serde_json::from_str::<RoomRole>("\"chief\"").ok(), Some(RoomRole::Unknown));
assert_eq!(serde_json::from_str::<RoomChange>("\"renamed\"").ok(), Some(RoomChange::Unknown));
let update = RoomUpdate::new(RoomId(12), RoomChange::Joined).about(UserId(7)).by(UserId(7));
assert_eq!(serde_json::to_string(&update).ok().as_deref(), Some(r#"{"room":12,"change":"joined","user":7,"by":7}"#));
let info = RoomInfo::new(RoomId(12), RoomKind::Player).with_visibility(RoomVisibility::Private).with_owner(UserId(1)).with_role(RoomRole::Invited);
assert_eq!(serde_json::to_string(&info).ok().as_deref(), Some(r#"{"id":12,"kind":"player","visibility":"private","owner":1,"role":"invited"}"#));
}
}