Skip to main content

serenity/model/guild/
role.rs

1use std::cmp::Ordering;
2use std::fmt;
3
4#[cfg(feature = "model")]
5use crate::builder::EditRole;
6#[cfg(all(feature = "cache", feature = "model"))]
7use crate::cache::Cache;
8#[cfg(feature = "model")]
9use crate::http::Http;
10#[cfg(all(feature = "cache", feature = "model"))]
11use crate::internal::prelude::*;
12use crate::model::prelude::*;
13use crate::model::utils::is_false;
14
15fn minus1_as_0<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<u16, D::Error> {
16    i16::deserialize(deserializer).map(|val| if val == -1 { 0 } else { val as u16 })
17}
18
19/// Information about a role within a guild.
20///
21/// A role represents a set of permissions, and can be attached to one or multiple users. A role has
22/// various miscellaneous configurations, such as being assigned a colour. Roles are unique per
23/// guild and do not cross over to other guilds in any way, and can have channel-specific permission
24/// overrides in addition to guild-level permissions.
25///
26/// [Discord docs](https://discord.com/developers/docs/topics/permissions#role-object).
27#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
28#[derive(Clone, Debug, Default, Deserialize, Serialize)]
29#[non_exhaustive]
30pub struct Role {
31    /// The Id of the role. Can be used to calculate the role's creation date.
32    pub id: RoleId,
33    /// The Id of the Guild the Role is in.
34    #[serde(default)]
35    pub guild_id: GuildId,
36    /// The colour of the role.
37    #[serde(rename = "color")]
38    pub colour: Colour,
39    #[serde(rename = "colors")]
40    pub colours: RoleColours,
41    /// Indicator of whether the role is pinned above lesser roles.
42    ///
43    /// In the client, this causes [`Member`]s in the role to be seen above those in roles with a
44    /// lower [`Self::position`].
45    pub hoist: bool,
46    /// Indicator of whether the role is managed by an integration service.
47    pub managed: bool,
48    /// Indicator of whether the role can be mentioned, similar to mentioning a specific member or
49    /// `@everyone`.
50    ///
51    /// Only members of the role will be notified if a role is mentioned with this set to `true`.
52    #[serde(default)]
53    pub mentionable: bool,
54    /// The name of the role.
55    pub name: String,
56    /// A set of permissions that the role has been assigned.
57    ///
58    /// See the [`permissions`] module for more information.
59    ///
60    /// [`permissions`]: crate::model::permissions
61    pub permissions: Permissions,
62    /// The role's position in the position list. Roles are considered higher in hierarchy if their
63    /// position is higher.
64    ///
65    /// The `@everyone` role is usually either `-1` or `0`.
66    #[serde(deserialize_with = "minus1_as_0")]
67    pub position: u16,
68    /// The tags this role has. It can be used to determine if this role is a special role in this
69    /// guild such as guild subscriber role, or if the role is linked to an [`Integration`] or a
70    /// bot.
71    ///
72    /// [`Integration`]: super::Integration
73    #[serde(default)]
74    pub tags: RoleTags,
75    /// Role icon image hash.
76    pub icon: Option<ImageHash>,
77    /// Role unicoded image.
78    pub unicode_emoji: Option<String>,
79}
80
81/// The colours of a Discord role, secondary_colour and tertiary_colour may only be set if
82/// the [Guild] has the `ENHANCED_ROLE_COLORS` feature.
83#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
84#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize)]
85#[non_exhaustive]
86pub struct RoleColours {
87    /// the primary color for the role
88    #[serde(rename = "primary_color")]
89    pub primary_colour: Colour,
90    /// the secondary color for the role, this will make the role a gradient between the other
91    /// provided colors
92    #[serde(rename = "secondary_color")]
93    pub secondary_colour: Option<Colour>,
94    /// the tertiary color for the role, this will turn the gradient into a holographic style
95    #[serde(rename = "tertiary_color")]
96    pub tertiary_colour: Option<Colour>,
97}
98
99#[cfg(feature = "model")]
100impl Role {
101    /// Deletes the role.
102    ///
103    /// **Note** Requires the [Manage Roles] permission.
104    ///
105    /// # Errors
106    ///
107    /// Returns [`Error::Http`] if the current user lacks permission to delete this role.
108    ///
109    /// [Manage Roles]: Permissions::MANAGE_ROLES
110    #[inline]
111    pub async fn delete(&mut self, http: impl AsRef<Http>) -> Result<()> {
112        http.as_ref().delete_role(self.guild_id, self.id, None).await
113    }
114
115    /// Edits a [`Role`], optionally setting its new fields.
116    ///
117    /// Requires the [Manage Roles] permission.
118    ///
119    /// # Examples
120    ///
121    /// See the documentation of [`EditRole`] for details.
122    ///
123    /// # Errors
124    ///
125    /// Returns [`Error::Http`] if the current user does not have permission to Manage Roles.
126    ///
127    /// [Manage Roles]: Permissions::MANAGE_ROLES
128    #[inline]
129    pub async fn edit(&mut self, http: impl AsRef<Http>, builder: EditRole<'_>) -> Result<()> {
130        *self = self.guild_id.edit_role(http.as_ref(), self.id, builder).await?;
131        Ok(())
132    }
133
134    /// Check that the role has the given permission.
135    #[inline]
136    #[must_use]
137    pub fn has_permission(&self, permission: Permissions) -> bool {
138        self.permissions.contains(permission)
139    }
140
141    /// Checks whether the role has all of the given permissions.
142    ///
143    /// The 'precise' argument is used to check if the role's permissions are precisely equivalent
144    /// to the given permissions. If you need only check that the role has at least the given
145    /// permissions, pass `false`.
146    #[inline]
147    #[must_use]
148    pub fn has_permissions(&self, permissions: Permissions, precise: bool) -> bool {
149        if precise {
150            self.permissions == permissions
151        } else {
152            self.permissions.contains(permissions)
153        }
154    }
155
156    #[inline]
157    #[must_use]
158    /// Generates a URL to the Role icon's image.
159    pub fn icon_url(&self) -> Option<String> {
160        self.icon.map(|icon| {
161            let ext = if icon.is_animated() { "gif" } else { "webp" };
162
163            cdn!("/role-icons/{}/{}.{}", self.id, icon, ext)
164        })
165    }
166}
167
168impl fmt::Display for Role {
169    /// Format a mention for the role, pinging its members.
170    // This is in the format of: `<@&ROLE_ID>`.
171    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172        fmt::Display::fmt(&self.mention(), f)
173    }
174}
175
176impl Eq for Role {}
177
178impl Ord for Role {
179    fn cmp(&self, other: &Role) -> Ordering {
180        // Discord does position DESC, id ASC so:
181        if self.position == other.position {
182            other.id.cmp(&self.id)
183        } else {
184            self.position.cmp(&other.position)
185        }
186    }
187}
188
189impl PartialEq for Role {
190    fn eq(&self, other: &Role) -> bool {
191        self.id == other.id
192    }
193}
194
195impl PartialOrd for Role {
196    fn partial_cmp(&self, other: &Role) -> Option<Ordering> {
197        Some(self.cmp(other))
198    }
199}
200
201#[cfg(feature = "model")]
202impl RoleId {
203    /// Tries to find the [`Role`] by its Id in the cache.
204    #[cfg(feature = "cache")]
205    #[deprecated = "Use Guild::roles. This performs a loop over the entire cache!"]
206    pub fn to_role_cached(self, cache: impl AsRef<Cache>) -> Option<Role> {
207        for guild_entry in cache.as_ref().guilds.iter() {
208            let guild = guild_entry.value();
209
210            if !guild.roles.contains_key(&self) {
211                continue;
212            }
213
214            if let Some(role) = guild.roles.get(&self) {
215                return Some(role.clone());
216            }
217        }
218
219        None
220    }
221}
222
223impl From<Role> for RoleId {
224    /// Gets the Id of a role.
225    fn from(role: Role) -> RoleId {
226        role.id
227    }
228}
229
230impl From<&Role> for RoleId {
231    /// Gets the Id of a role.
232    fn from(role: &Role) -> RoleId {
233        role.id
234    }
235}
236
237/// The tags of a [`Role`].
238///
239/// [Discord docs](https://discord.com/developers/docs/topics/permissions#role-object-role-tags-structure).
240#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
241#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
242#[non_exhaustive]
243pub struct RoleTags {
244    /// The Id of the bot the [`Role`] belongs to.
245    pub bot_id: Option<UserId>,
246    /// The Id of the integration the [`Role`] belongs to.
247    pub integration_id: Option<IntegrationId>,
248    /// Whether this is the guild's premium subscriber role.
249    #[serde(default, skip_serializing_if = "is_false", with = "bool_as_option_unit")]
250    pub premium_subscriber: bool,
251    /// The id of this role's subscription sku and listing.
252    pub subscription_listing_id: Option<SkuId>,
253    /// Whether this role is available for purchase.
254    #[serde(default, skip_serializing_if = "is_false", with = "bool_as_option_unit")]
255    pub available_for_purchase: bool,
256    /// Whether this role is a guild's linked role.
257    #[serde(default, skip_serializing_if = "is_false", with = "bool_as_option_unit")]
258    pub guild_connections: bool,
259}
260
261/// A premium subscriber role is reported with the field present and the value `null`.
262mod bool_as_option_unit {
263    use std::fmt;
264
265    use serde::de::{Error, Visitor};
266    use serde::{Deserializer, Serializer};
267
268    pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<bool, D::Error> {
269        deserializer.deserialize_option(NullValueVisitor)
270    }
271
272    #[allow(clippy::trivially_copy_pass_by_ref)]
273    pub fn serialize<S: Serializer>(_: &bool, serializer: S) -> Result<S::Ok, S::Error> {
274        serializer.serialize_none()
275    }
276
277    struct NullValueVisitor;
278
279    impl Visitor<'_> for NullValueVisitor {
280        type Value = bool;
281
282        fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
283            f.write_str("null value")
284        }
285
286        fn visit_none<E: Error>(self) -> Result<Self::Value, E> {
287            Ok(true)
288        }
289
290        /// Called by the `simd_json` crate
291        fn visit_unit<E: Error>(self) -> Result<Self::Value, E> {
292            Ok(true)
293        }
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    use super::RoleTags;
300    use crate::json::{assert_json, json};
301
302    #[test]
303    fn premium_subscriber_role_serde() {
304        let value = RoleTags {
305            bot_id: None,
306            integration_id: None,
307            premium_subscriber: true,
308            subscription_listing_id: None,
309            available_for_purchase: false,
310            guild_connections: false,
311        };
312
313        assert_json(
314            &value,
315            json!({"bot_id": null, "integration_id": null, "premium_subscriber": null, "subscription_listing_id": null}),
316        );
317    }
318
319    #[test]
320    fn non_premium_subscriber_role_serde() {
321        let value = RoleTags {
322            bot_id: None,
323            integration_id: None,
324            premium_subscriber: false,
325            subscription_listing_id: None,
326            available_for_purchase: false,
327            guild_connections: false,
328        };
329
330        assert_json(
331            &value,
332            json!({"bot_id": null, "integration_id": null, "subscription_listing_id": null}),
333        );
334    }
335}