#[cfg(feature = "model")]
use std::cmp::Ordering;
use std::convert::TryFrom;
#[cfg(doc)]
use std::fmt::Display as _;
use std::fmt::{self, Write as _};
use std::str::FromStr;
use serde::de::{Deserialize, Error as DeError, MapAccess, Visitor};
use serde::ser::{Serialize, SerializeMap, Serializer};
#[cfg(feature = "model")]
use tracing::warn;
#[cfg(feature = "model")]
use crate::http::{CacheHttp, Http};
use crate::internal::prelude::*;
use crate::model::prelude::*;
/// An emoji reaction to a message.
#[derive(Clone, Debug, Serialize)]
#[non_exhaustive]
pub struct Reaction {
/// The [`Channel`] of the associated [`Message`].
pub channel_id: ChannelId,
/// The reactive emoji used.
pub emoji: ReactionType,
/// The Id of the [`Message`] that was reacted to.
pub message_id: MessageId,
/// The Id of the [`User`] that sent the reaction.
///
/// Set to [`None`] by [`Message::react`] when cache is not available.
pub user_id: Option<UserId>,
/// The optional Id of the [`Guild`] where the reaction was sent.
pub guild_id: Option<GuildId>,
/// The optional object of the member which added the reaction.
pub member: Option<PartialMember>,
}
impl<'de> Deserialize<'de> for Reaction {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> StdResult<Self, D::Error> {
let mut map = JsonMap::deserialize(deserializer)?;
let channel_id = map
.remove("channel_id")
.ok_or_else(|| DeError::custom("expected channel_id"))
.and_then(ChannelId::deserialize)
.map_err(DeError::custom)?;
let message_id = map
.remove("message_id")
.ok_or_else(|| DeError::custom("expected message_id"))
.and_then(MessageId::deserialize)
.map_err(DeError::custom)?;
let emoji = map
.remove("emoji")
.ok_or_else(|| DeError::custom("expected emoji"))
.and_then(ReactionType::deserialize)
.map_err(DeError::custom)?;
let user_id =
map.remove("user_id").map(UserId::deserialize).transpose().map_err(DeError::custom)?;
let guild_id = map
.remove("guild_id")
.map(GuildId::deserialize)
.transpose()
.map_err(DeError::custom)?;
if let Some(id) = guild_id {
if let Some(member) = map.get_mut("member") {
if let Some(object) = member.as_object_mut() {
object.insert("guild_id".to_owned(), Value::from(id.to_string()));
}
}
}
let member = map
.remove("member")
.map(PartialMember::deserialize)
.transpose()
.map_err(DeError::custom)?;
Ok(Self {
channel_id,
emoji,
message_id,
user_id,
guild_id,
member,
})
}
}
#[cfg(feature = "model")]
impl Reaction {
/// Retrieves the associated the reaction was made in.
///
/// If the cache is enabled, this will search for the already-cached
/// channel. If not - or the channel was not found - this will perform a
/// request over the REST API for the channel.
///
/// Requires the [Read Message History] permission.
///
/// # Errors
///
/// Returns [`Error::Http`] if the current user lacks permission,
/// or if the channel no longer exists.
///
/// [Read Message History]: Permissions::READ_MESSAGE_HISTORY
#[inline]
pub async fn channel(&self, cache_http: impl CacheHttp) -> Result<Channel> {
self.channel_id.to_channel(cache_http).await
}
/// Deletes the reaction, but only if the current user is the user who made
/// the reaction or has permission to.
///
/// Requires the [Manage Messages] permission, _if_ the current
/// user did not perform the reaction.
///
/// # Errors
///
/// If the `cache` is enabled, then returns a
/// [`ModelError::InvalidPermissions`] if the current user does not have
/// the required [permissions].
///
/// Otherwise returns [`Error::Http`] if the current user lacks permission.
///
/// [Manage Messages]: Permissions::MANAGE_MESSAGES
/// [permissions]: super::permissions
pub async fn delete(&self, cache_http: impl CacheHttp) -> Result<()> {
// Silences a warning when compiling without the `cache` feature.
#[allow(unused_mut)]
let mut user_id = self.user_id.map(|id| id.0);
#[cfg(feature = "cache")]
{
if let Some(cache) = cache_http.cache() {
if self.user_id.is_some() && self.user_id == Some(cache.current_user().id) {
user_id = None;
}
if user_id.is_some() {
utils::user_has_perms_cache(
cache,
self.channel_id,
self.guild_id,
Permissions::MANAGE_MESSAGES,
)?;
}
}
}
cache_http
.http()
.delete_reaction(self.channel_id.0, self.message_id.0, user_id, &self.emoji)
.await
}
/// Deletes all reactions from the message with this emoji.
///
/// Requires the [Manage Messages] permission
///
/// # Errors
///
/// If the `cache` is enabled, then returns a
/// [`ModelError::InvalidPermissions`] if the current user does not have
/// the required [permissions].
///
/// Otherwise returns [`Error::Http`] if the current user lacks permission.
///
/// [Manage Messages]: Permissions::MANAGE_MESSAGES
/// [permissions]: super::permissions
pub async fn delete_all(&self, cache_http: impl CacheHttp) -> Result<()> {
#[cfg(feature = "cache")]
{
if let Some(cache) = cache_http.cache() {
utils::user_has_perms_cache(
cache,
self.channel_id,
self.guild_id,
Permissions::MANAGE_MESSAGES,
)?;
}
}
cache_http
.http()
.as_ref()
.delete_message_reaction_emoji(self.channel_id.0, self.message_id.0, &self.emoji)
.await
}
/// Retrieves the [`Message`] associated with this reaction.
///
/// Requires the [Read Message History] permission.
///
/// **Note**: This will send a request to the REST API. Prefer maintaining
/// your own message cache or otherwise having the message available if
/// possible.
///
/// # Errors
///
/// Returns [`Error::Http`] if the current user lacks permission to
/// read message history, or if the message was deleted.
///
/// [Read Message History]: Permissions::READ_MESSAGE_HISTORY
#[inline]
pub async fn message(&self, http: impl AsRef<Http>) -> Result<Message> {
self.channel_id.message(&http, self.message_id).await
}
/// Retrieves the user that made the reaction.
///
/// If the cache is enabled, this will search for the already-cached user.
/// If not - or the user was not found - this will perform a request over
/// the REST API for the user.
///
/// # Errors
///
/// Returns [`Error::Http`] if the user that made the reaction is unable to be
/// retrieved from the API.
pub async fn user(&self, cache_http: impl CacheHttp) -> Result<User> {
if let Some(id) = self.user_id {
id.to_user(cache_http).await
} else {
// This can happen if only Http was passed to Message::react, even though
// "cache" was enabled.
#[cfg(feature = "cache")]
{
if let Some(cache) = cache_http.cache() {
return Ok(User::from(&cache.current_user()));
}
}
Ok(cache_http.http().get_current_user().await?.into())
}
}
/// Retrieves the list of [`User`]s who have reacted to a [`Message`] with a
/// certain [`Emoji`].
///
/// The default `limit` is `50` - specify otherwise to receive a different
/// maximum number of users. The maximum that may be retrieve at a time is
/// `100`, if a greater number is provided then it is automatically reduced.
///
/// The optional `after` attribute is to retrieve the users after a certain
/// user. This is useful for pagination.
///
/// Requires the [Read Message History] permission.
///
/// **Note**: This will send a request to the REST API.
///
/// # Errors
///
/// Returns a [`ModelError::InvalidPermissions`] if the current user does
/// not have the required [permissions].
///
/// [Read Message History]: Permissions::READ_MESSAGE_HISTORY
/// [permissions]: super::permissions
#[inline]
pub async fn users<R, U>(
&self,
http: impl AsRef<Http>,
reaction_type: R,
limit: Option<u8>,
after: Option<U>,
) -> Result<Vec<User>>
where
R: Into<ReactionType>,
U: Into<UserId>,
{
self._users(&http, &reaction_type.into(), limit, after.map(Into::into)).await
}
async fn _users(
&self,
http: impl AsRef<Http>,
reaction_type: &ReactionType,
limit: Option<u8>,
after: Option<UserId>,
) -> Result<Vec<User>> {
let mut limit = limit.unwrap_or(50);
if limit > 100 {
limit = 100;
warn!("Reaction users limit clamped to 100! (API Restriction)");
}
http.as_ref()
.get_reaction_users(
self.channel_id.0,
self.message_id.0,
reaction_type,
limit,
after.map(|u| u.0),
)
.await
}
}
/// The type of a [`Reaction`] sent.
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
#[non_exhaustive]
pub enum ReactionType {
/// A reaction with a [`Guild`]s custom [`Emoji`], which is unique to the
/// guild.
Custom {
/// Whether the emoji is animated.
animated: bool,
/// The Id of the custom [`Emoji`].
id: EmojiId,
/// The name of the custom emoji. This is primarily used for decoration
/// and distinguishing the emoji client-side.
name: Option<String>,
},
/// A reaction with a twemoji.
Unicode(String),
}
impl<'de> Deserialize<'de> for ReactionType {
#[allow(clippy::unwrap_used)] // allow unwrap here because name being none is unreachable
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> StdResult<Self, D::Error> {
#[derive(Deserialize)]
#[serde(field_identifier, rename_all = "snake_case")]
enum Field {
Animated,
Id,
Name,
}
struct ReactionTypeVisitor;
impl<'de> Visitor<'de> for ReactionTypeVisitor {
type Value = ReactionType;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("enum ReactionType")
}
fn visit_map<V: MapAccess<'de>>(self, mut map: V) -> StdResult<Self::Value, V::Error> {
let mut animated = None;
let mut id = None;
let mut name = None;
while let Some(key) = map.next_key()? {
match key {
Field::Animated => {
if animated.is_some() {
return Err(DeError::duplicate_field("animated"));
}
animated = Some(map.next_value()?);
},
Field::Id => {
if id.is_some() {
return Err(DeError::duplicate_field("id"));
}
if let Ok(emoji_id) = map.next_value::<EmojiId>() {
id = Some(emoji_id);
}
},
Field::Name => {
if name.is_some() {
return Err(DeError::duplicate_field("name"));
}
name = Some(map.next_value::<Option<String>>()?);
},
}
}
let rt = match (id, name) {
(Some(id), name) => ReactionType::Custom {
animated: animated.unwrap_or_default(),
id,
name: name.flatten(),
},
(None, Some(Some(name))) => ReactionType::Unicode(name),
_ => return Err(DeError::custom("invalid reaction type data")),
};
Ok(rt)
}
}
deserializer.deserialize_map(ReactionTypeVisitor)
}
}
impl Serialize for ReactionType {
fn serialize<S>(&self, serializer: S) -> StdResult<S::Ok, S::Error>
where
S: Serializer,
{
match *self {
ReactionType::Custom {
animated,
id,
ref name,
} => {
let mut map = serializer.serialize_map(Some(3))?;
map.serialize_entry("animated", &animated)?;
map.serialize_entry("id", &id)?;
map.serialize_entry("name", &name)?;
map.end()
},
ReactionType::Unicode(ref name) => {
let mut map = serializer.serialize_map(Some(1))?;
map.serialize_entry("name", &name)?;
map.end()
},
}
}
}
#[cfg(feature = "model")]
impl ReactionType {
/// Creates a data-esque display of the type. This is not very useful for
/// displaying, as the primary client can not render it, but can be useful
/// for debugging.
///
/// **Note**: This is mainly for use internally. There is otherwise most
/// likely little use for it.
#[inline]
#[must_use]
pub fn as_data(&self) -> String {
match *self {
ReactionType::Custom {
id,
ref name,
..
} => {
format!("{}:{}", name.as_ref().map_or("", String::as_str), id)
},
ReactionType::Unicode(ref unicode) => unicode.clone(),
}
}
/// Helper function to allow testing equality of unicode emojis without
/// having to perform any allocation.
/// Will always return false if the reaction was not a unicode reaction.
#[must_use]
pub fn unicode_eq(&self, other: &str) -> bool {
if let ReactionType::Unicode(unicode) = &self {
unicode == other
} else {
// Always return false if not a unicode reaction
false
}
}
/// Helper function to allow comparing unicode emojis without having
/// to perform any allocation.
/// Will return None if the reaction was not a unicode reaction.
#[must_use]
pub fn unicode_partial_cmp(&self, other: &str) -> Option<Ordering> {
if let ReactionType::Unicode(unicode) = &self {
Some(unicode.as_str().cmp(other))
} else {
// Always return None if not a unicode reaction
None
}
}
}
impl From<char> for ReactionType {
/// Creates a [`ReactionType`] from a `char`.
///
/// # Examples
///
/// Reacting to a message with an apple:
///
/// ```rust,no_run
/// # #[cfg(feature = "client")]
/// # use serenity::client::Context;
/// # #[cfg(feature = "framework")]
/// # use serenity::framework::standard::{CommandResult, macros::command};
/// # use serenity::model::id::ChannelId;
/// #
/// # #[cfg(all(feature = "client", feature = "framework", feature = "http"))]
/// # #[command]
/// # async fn example(ctx: &Context) -> CommandResult {
/// # let message = ChannelId(0).message(&ctx.http, 0).await?;
/// #
/// message.react(ctx, '🍎').await?;
/// # Ok(())
/// # }
/// #
/// # fn main() {}
/// ```
fn from(ch: char) -> ReactionType {
ReactionType::Unicode(ch.to_string())
}
}
impl From<Emoji> for ReactionType {
fn from(emoji: Emoji) -> ReactionType {
ReactionType::Custom {
animated: emoji.animated,
id: emoji.id,
name: Some(emoji.name),
}
}
}
impl From<EmojiId> for ReactionType {
fn from(emoji_id: EmojiId) -> ReactionType {
ReactionType::Custom {
animated: false,
id: emoji_id,
name: None,
}
}
}
impl From<EmojiIdentifier> for ReactionType {
fn from(emoji_id: EmojiIdentifier) -> ReactionType {
ReactionType::Custom {
animated: emoji_id.animated,
id: emoji_id.id,
name: Some(emoji_id.name),
}
}
}
#[derive(Debug)]
pub struct ReactionConversionError;
impl fmt::Display for ReactionConversionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("failed to convert from a string to ReactionType")
}
}
impl std::error::Error for ReactionConversionError {}
impl TryFrom<String> for ReactionType {
type Error = ReactionConversionError;
fn try_from(emoji_string: String) -> std::result::Result<Self, Self::Error> {
if emoji_string.is_empty() {
return Err(ReactionConversionError);
}
if !emoji_string.starts_with('<') {
return Ok(ReactionType::Unicode(emoji_string));
}
ReactionType::try_from(&emoji_string[..])
}
}
impl<'a> TryFrom<&'a str> for ReactionType {
/// Creates a [`ReactionType`] from a string slice.
///
/// # Examples
///
/// Creating a [`ReactionType`] from a `🍎`, modeling a similar API as the
/// rest of the library:
///
/// ```rust
/// use std::convert::TryInto;
/// use std::fmt::Debug;
///
/// use serenity::model::channel::ReactionType;
///
/// fn foo<R: TryInto<ReactionType>>(bar: R)
/// where
/// R::Error: Debug,
/// {
/// println!("{:?}", bar.try_into().unwrap());
/// }
///
/// foo("🍎");
/// ```
///
/// Creating a [`ReactionType`] from a custom emoji argument in the following format:
///
/// ```rust
/// use std::convert::TryFrom;
///
/// use serenity::model::channel::ReactionType;
/// use serenity::model::id::EmojiId;
///
/// let emoji_string = "<:customemoji:600404340292059257>";
/// let reaction = ReactionType::try_from(emoji_string).unwrap();
/// let reaction2 = ReactionType::Custom {
/// animated: false,
/// id: EmojiId(600404340292059257),
/// name: Some("customemoji".to_string()),
/// };
///
/// assert_eq!(reaction, reaction2);
/// ```
type Error = ReactionConversionError;
fn try_from(emoji_str: &str) -> std::result::Result<Self, Self::Error> {
if emoji_str.is_empty() {
return Err(ReactionConversionError);
}
if !emoji_str.starts_with('<') {
return Ok(ReactionType::Unicode(emoji_str.to_string()));
}
if !emoji_str.ends_with('>') {
return Err(ReactionConversionError);
}
let emoji_str = emoji_str.trim_matches(&['<', '>'] as &[char]);
let mut split_iter = emoji_str.split(':');
let animated = split_iter.next().ok_or(ReactionConversionError)? == "a";
let name = split_iter.next().ok_or(ReactionConversionError)?.to_string().into();
let id = split_iter
.next()
.and_then(|s| s.parse::<u64>().ok())
.ok_or(ReactionConversionError)?
.into();
Ok(ReactionType::Custom {
animated,
id,
name,
})
}
}
impl FromStr for ReactionType {
type Err = ReactionConversionError;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
ReactionType::try_from(s)
}
}
impl fmt::Display for ReactionType {
/// Formats the reaction type, displaying the associated emoji in a
/// way that clients can understand.
///
/// If the type is a [custom][`ReactionType::Custom`] emoji, then refer to
/// the documentation for [emoji's formatter][`Emoji::fmt`] on how this is
/// displayed. Otherwise, if the type is a
/// [unicode][`ReactionType::Unicode`], then the inner unicode is displayed.
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
ReactionType::Custom {
animated,
id,
ref name,
} => {
if animated {
f.write_str("<a:")?;
} else {
f.write_str("<:")?;
}
f.write_str(name.as_ref().map_or("", String::as_str))?;
f.write_char(':')?;
fmt::Display::fmt(&id, f)?;
f.write_char('>')
},
ReactionType::Unicode(ref unicode) => f.write_str(unicode),
}
}
}