use std::time::SystemTime; use indexmap::{IndexMap, IndexSet}; use revolt_config::config; #[cfg(feature = "validator")] use validator::Validate; #[cfg(feature = "rocket")] use rocket::{FromForm, FromFormField}; use iso8601_timestamp::Timestamp; use super::{Channel, Embed, File, Member, MessageWebhook, User, Webhook, RE_COLOUR}; auto_derived_partial!( /// Message pub struct Message { /// Unique Id #[serde(rename = "_id")] pub id: String, /// Unique value generated by client sending this message #[serde(skip_serializing_if = "Option::is_none")] pub nonce: Option, /// Id of the channel this message was sent in pub channel: String, /// Id of the user or webhook that sent this message pub author: String, /// The user that sent this message #[serde(skip_serializing_if = "Option::is_none")] pub user: Option, /// The member that sent this message #[serde(skip_serializing_if = "Option::is_none")] pub member: Option, /// The webhook that sent this message #[serde(skip_serializing_if = "Option::is_none")] pub webhook: Option, /// Message content #[serde(skip_serializing_if = "Option::is_none")] pub content: Option, /// System message #[serde(skip_serializing_if = "Option::is_none")] pub system: Option, /// Array of attachments #[serde(skip_serializing_if = "Option::is_none")] pub attachments: Option>, /// Time at which this message was last edited #[serde(skip_serializing_if = "Option::is_none")] pub edited: Option, /// Attached embeds to this message #[serde(skip_serializing_if = "Option::is_none")] pub embeds: Option>, /// Array of user ids mentioned in this message #[serde(skip_serializing_if = "Option::is_none")] pub mentions: Option>, /// Array of role ids mentioned in this message #[serde(skip_serializing_if = "Option::is_none")] pub role_mentions: Option>, /// Array of message ids this message is replying to #[serde(skip_serializing_if = "Option::is_none")] pub replies: Option>, /// Hashmap of emoji IDs to array of user IDs #[serde(skip_serializing_if = "IndexMap::is_empty", default)] pub reactions: IndexMap>, /// Information about how this message should be interacted with #[serde(skip_serializing_if = "Interactions::is_default", default)] pub interactions: Interactions, /// Name and / or avatar overrides for this message #[serde(skip_serializing_if = "Option::is_none")] pub masquerade: Option, /// Whether or not the message in pinned #[serde(skip_serializing_if = "crate::if_option_false")] pub pinned: Option, /// Bitfield of message flags /// /// https://docs.rs/revolt-models/latest/revolt_models/v0/enum.MessageFlags.html #[cfg_attr( feature = "serde", serde(skip_serializing_if = "crate::if_zero_u32", default) )] pub flags: u32, }, "PartialMessage" ); auto_derived!( /// Bulk Message Response #[serde(untagged)] pub enum BulkMessageResponse { JustMessages( /// List of messages Vec, ), MessagesAndUsers { /// List of messages messages: Vec, /// List of users users: Vec, /// List of members #[serde(skip_serializing_if = "Option::is_none")] members: Option>, }, } /// System Event #[serde(tag = "type")] pub enum SystemMessage { #[serde(rename = "text")] Text { content: String }, #[serde(rename = "user_added")] UserAdded { id: String, by: String }, #[serde(rename = "user_remove")] UserRemove { id: String, by: String }, #[serde(rename = "user_joined")] UserJoined { id: String }, #[serde(rename = "user_left")] UserLeft { id: String }, #[serde(rename = "user_kicked")] UserKicked { id: String }, #[serde(rename = "user_banned")] UserBanned { id: String }, #[serde(rename = "channel_renamed")] ChannelRenamed { name: String, by: String }, #[serde(rename = "channel_description_changed")] ChannelDescriptionChanged { by: String }, #[serde(rename = "channel_icon_changed")] ChannelIconChanged { by: String }, #[serde(rename = "channel_ownership_changed")] ChannelOwnershipChanged { from: String, to: String }, #[serde(rename = "message_pinned")] MessagePinned { id: String, by: String }, #[serde(rename = "message_unpinned")] MessageUnpinned { id: String, by: String }, #[serde(rename = "call_started")] CallStarted { by: String, finished_at: Option, }, } /// Name and / or avatar override information #[cfg_attr(feature = "validator", derive(Validate))] pub struct Masquerade { /// Replace the display name shown on this message #[serde(skip_serializing_if = "Option::is_none")] #[validate(length(min = 1, max = 32))] pub name: Option, /// Replace the avatar shown on this message (URL to image file) #[serde(skip_serializing_if = "Option::is_none")] #[validate(length(min = 1, max = 256))] pub avatar: Option, /// Replace the display role colour shown on this message /// /// Must have `ManageRole` permission to use #[serde(skip_serializing_if = "Option::is_none")] #[validate(length(min = 1, max = 128), regex = "RE_COLOUR")] pub colour: Option, } /// Information to guide interactions on this message #[derive(Default)] pub struct Interactions { /// Reactions which should always appear and be distinct #[serde(skip_serializing_if = "Option::is_none", default)] pub reactions: Option>, /// Whether reactions should be restricted to the given list /// /// Can only be set to true if reactions list is of at least length 1 #[serde(skip_serializing_if = "crate::if_false", default)] pub restrict_reactions: bool, } /// Appended Information pub struct AppendMessage { /// Additional embeds to include in this message #[serde(skip_serializing_if = "Option::is_none")] pub embeds: Option>, } /// Message Sort /// /// Sort used for retrieving messages #[derive(Default)] #[cfg_attr(feature = "rocket", derive(FromFormField))] pub enum MessageSort { /// Sort by the most relevant messages #[default] Relevance, /// Sort by the newest messages first Latest, /// Sort by the oldest messages first Oldest, } /// Push Notification pub struct PushNotification { /// Known author name pub author: String, /// URL to author avatar pub icon: String, /// URL to first matching attachment #[serde(skip_serializing_if = "Option::is_none")] pub image: Option, /// Message content or system message information pub body: String, /// The raw body, if the body has been rendered #[serde(skip_serializing_if = "Option::is_none")] pub raw_body: Option, /// Unique tag, usually the channel ID pub tag: String, /// Timestamp at which this notification was created pub timestamp: u64, /// URL to open when clicking notification pub url: String, /// The message object itself, to send to clients for processing pub message: Message, /// The channel object itself, for clients to process pub channel: Channel, } /// Representation of a text embed before it is sent. #[derive(Default)] #[cfg_attr(feature = "validator", derive(Validate))] pub struct SendableEmbed { #[cfg_attr(feature = "validator", validate(length(min = 1, max = 256)))] pub icon_url: Option, #[cfg_attr(feature = "validator", validate(length(min = 1, max = 256)))] pub url: Option, #[cfg_attr(feature = "validator", validate(length(min = 1, max = 100)))] pub title: Option, #[cfg_attr(feature = "validator", validate(length(min = 1, max = 2000)))] pub description: Option, pub media: Option, #[cfg_attr( feature = "validator", validate(length(min = 1, max = 128), regex = "RE_COLOUR") )] pub colour: Option, } /// What this message should reply to and how pub struct ReplyIntent { /// Message Id pub id: String, /// Whether this reply should mention the message's author pub mention: bool, /// Whether to error if the referenced message doesn't exist. /// Otherwise, send a message without this reply. /// Default is true. pub fail_if_not_exists: Option, } /// Message to send #[cfg_attr(feature = "validator", derive(Validate))] pub struct DataMessageSend { /// Unique token to prevent duplicate message sending /// /// **This is deprecated and replaced by `Idempotency-Key`!** #[cfg_attr(feature = "validator", validate(length(min = 1, max = 64)))] pub nonce: Option, /// Message content to send #[cfg_attr(feature = "validator", validate(length(min = 0)))] pub content: Option, /// Attachments to include in message pub attachments: Option>, /// Messages to reply to pub replies: Option>, /// Embeds to include in message /// /// Text embed content contributes to the content length cap #[cfg_attr(feature = "validator", validate)] pub embeds: Option>, /// Masquerade to apply to this message #[cfg_attr(feature = "validator", validate)] pub masquerade: Option, /// Information about how this message should be interacted with pub interactions: Option, /// Bitfield of message flags /// /// https://docs.rs/revolt-models/latest/revolt_models/v0/enum.MessageFlags.html pub flags: Option, } /// Options for querying messages #[cfg_attr(feature = "validator", derive(Validate))] #[cfg_attr(feature = "rocket", derive(FromForm))] pub struct OptionsQueryMessages { /// Maximum number of messages to fetch /// /// For fetching nearby messages, this is \`(limit + 2)\`. #[cfg_attr(feature = "validator", validate(range(min = 1, max = 100)))] pub limit: Option, /// Message id before which messages should be fetched #[cfg_attr(feature = "validator", validate(length(min = 26, max = 26)))] pub before: Option, /// Message id after which messages should be fetched #[cfg_attr(feature = "validator", validate(length(min = 26, max = 26)))] pub after: Option, /// Message sort direction pub sort: Option, /// Message id to search around /// /// Specifying 'nearby' ignores 'before', 'after' and 'sort'. /// It will also take half of limit rounded as the limits to each side. /// It also fetches the message ID specified. #[cfg_attr(feature = "validator", validate(length(min = 26, max = 26)))] pub nearby: Option, /// Whether to include user (and member, if server channel) objects pub include_users: Option, } /// Options for searching for messages #[cfg_attr(feature = "validator", derive(Validate))] pub struct DataMessageSearch { /// Full-text search query /// /// See [MongoDB documentation](https://docs.mongodb.com/manual/text-search/#-text-operator) for more information. #[cfg_attr(feature = "validator", validate(length(min = 1, max = 64)))] pub query: Option, /// Whether to only search for pinned messages, cannot be sent with `query`. pub pinned: Option, /// Maximum number of messages to fetch #[cfg_attr(feature = "validator", validate(range(min = 1, max = 100)))] pub limit: Option, /// Message id before which messages should be fetched #[cfg_attr(feature = "validator", validate(length(min = 26, max = 26)))] pub before: Option, /// Message id after which messages should be fetched #[cfg_attr(feature = "validator", validate(length(min = 26, max = 26)))] pub after: Option, /// Message sort direction /// /// By default, it will be sorted by latest. #[cfg_attr(feature = "serde", serde(default = "MessageSort::default"))] pub sort: MessageSort, /// Whether to include user (and member, if server channel) objects pub include_users: Option, } /// Changes to make to message #[cfg_attr(feature = "validator", derive(Validate))] pub struct DataEditMessage { /// New message content #[cfg_attr(feature = "validator", validate(length(min = 1)))] pub content: Option, /// Embeds to include in the message #[cfg_attr(feature = "validator", validate(length(min = 0, max = 10)))] pub embeds: Option>, } /// Options for bulk deleting messages #[cfg_attr( feature = "validator", cfg_attr(feature = "validator", derive(Validate)) )] pub struct OptionsBulkDelete { /// Message IDs #[validate(length(min = 1, max = 100))] pub ids: Vec, } /// Options for removing reaction #[cfg_attr(feature = "rocket", derive(FromForm))] pub struct OptionsUnreact { /// Remove a specific user's reaction pub user_id: Option, /// Remove all reactions pub remove_all: Option, } /// Message flag bitfield #[repr(u32)] pub enum MessageFlags { /// Message will not send push / desktop notifications SuppressNotifications = 1, /// Message will mention all users who can see the channel MentionsEveryone = 2, /// Message will mention all users who are online and can see the channel. /// This cannot be true if MentionsEveryone is true MentionsOnline = 3, } /// Optional fields on message pub enum FieldsMessage { Pinned, } ); /// Message Author Abstraction #[derive(Clone)] pub enum MessageAuthor<'a> { User(&'a User), Webhook(&'a Webhook), System { username: &'a str, avatar: Option<&'a str>, }, } impl Interactions { /// Check if default initialisation of fields pub fn is_default(&self) -> bool { !self.restrict_reactions && self.reactions.is_none() } } impl MessageAuthor<'_> { pub fn id(&self) -> &str { match self { MessageAuthor::User(user) => &user.id, MessageAuthor::Webhook(webhook) => &webhook.id, MessageAuthor::System { .. } => "00000000000000000000000000", } } pub fn avatar(&self) -> Option<&str> { match self { MessageAuthor::User(user) => user.avatar.as_ref().map(|file| file.id.as_str()), MessageAuthor::Webhook(webhook) => webhook.avatar.as_ref().map(|file| file.id.as_str()), MessageAuthor::System { avatar, .. } => *avatar, } } pub fn username(&self) -> &str { match self { MessageAuthor::User(user) => &user.username, MessageAuthor::Webhook(webhook) => &webhook.name, MessageAuthor::System { username, .. } => username, } } } impl From for String { fn from(s: SystemMessage) -> String { match s { SystemMessage::Text { content } => content, SystemMessage::UserAdded { .. } => "User added to the channel.".to_string(), SystemMessage::UserRemove { .. } => "User removed from the channel.".to_string(), SystemMessage::UserJoined { .. } => "User joined the channel.".to_string(), SystemMessage::UserLeft { .. } => "User left the channel.".to_string(), SystemMessage::UserKicked { .. } => "User kicked from the channel.".to_string(), SystemMessage::UserBanned { .. } => "User banned from the channel.".to_string(), SystemMessage::ChannelRenamed { .. } => "Channel renamed.".to_string(), SystemMessage::ChannelDescriptionChanged { .. } => { "Channel description changed.".to_string() } SystemMessage::ChannelIconChanged { .. } => "Channel icon changed.".to_string(), SystemMessage::ChannelOwnershipChanged { .. } => { "Channel ownership changed.".to_string() } SystemMessage::MessagePinned { .. } => "Message pinned.".to_string(), SystemMessage::MessageUnpinned { .. } => "Message unpinned.".to_string(), SystemMessage::CallStarted { .. } => "Call started.".to_string(), } } } impl PushNotification { /// Create a new notification from a given message, author and channel ID pub async fn from(msg: Message, author: Option>, channel: Channel) -> Self { let config = config().await; let icon = if let Some(author) = &author { if let Some(avatar) = author.avatar() { format!("{}/avatars/{}", config.hosts.autumn, avatar) } else { format!("{}/users/{}/default_avatar", config.hosts.api, author.id()) } } else { format!("{}/assets/logo.png", config.hosts.app) }; let image = msg.attachments.as_ref().and_then(|attachments| { attachments .first() .map(|v| format!("{}/attachments/{}", config.hosts.autumn, v.id)) }); let body = if let Some(ref sys) = msg.system { sys.clone().into() } else if let Some(ref text) = msg.content { text.clone() } else if let Some(text) = msg.embeds.as_ref().and_then(|embeds| match embeds.first() { Some(Embed::Image(_)) => Some("Sent an image".to_string()), Some(Embed::Video(_)) => Some("Sent a video".to_string()), Some(Embed::Text(e)) => e .description .clone() .or(e.title.clone().or(Some("Empty Embed".to_string()))), Some(Embed::Website(e)) => e.title.clone().or(e .description .clone() .or(e.site_name.clone().or(Some("Empty Embed".to_string())))), Some(Embed::None) => Some("Empty Message".to_string()), // ??? None => Some("Empty Message".to_string()), // ?? }) { text } else { "Empty Message".to_string() }; let timestamp = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .expect("Time went backwards") .as_secs(); Self { author: author .map(|x| x.username().to_string()) .unwrap_or_else(|| "Revolt".to_string()), icon, image, body, raw_body: None, tag: channel.id().to_string(), timestamp, url: format!("{}/channel/{}/{}", config.hosts.app, channel.id(), msg.id), message: msg, channel, } } }