titanium_model/
member.rs

1//! Guild member types.
2//!
3//! Represents Discord guild members with roles, permissions, and presence.
4
5use crate::Snowflake;
6use crate::TitanString;
7use serde::{Deserialize, Serialize};
8
9/// A member of a Discord guild.
10#[derive(Debug, Clone, Deserialize, Serialize)]
11pub struct GuildMember<'a> {
12    /// The user this guild member represents.
13    #[serde(default)]
14    pub user: Option<super::User<'a>>,
15
16    /// This user's guild nickname.
17    #[serde(default)]
18    pub nick: Option<TitanString<'a>>,
19
20    /// The member's guild avatar hash.
21    #[serde(default)]
22    pub avatar: Option<TitanString<'a>>,
23
24    /// Array of role object IDs.
25    #[serde(default)]
26    pub roles: smallvec::SmallVec<[Snowflake; 5]>,
27
28    /// When the user joined the guild (ISO8601 timestamp).
29    pub joined_at: TitanString<'a>,
30
31    /// When the user started boosting the guild.
32    #[serde(default)]
33    pub premium_since: Option<TitanString<'a>>,
34
35    /// Whether the user is deafened in voice channels.
36    #[serde(default)]
37    pub deaf: bool,
38
39    /// Whether the user is muted in voice channels.
40    #[serde(default)]
41    pub mute: bool,
42
43    /// Guild member flags as a bitfield.
44    #[serde(default)]
45    pub flags: u64,
46
47    /// Whether the user has not yet passed the guild's Membership Screening.
48    #[serde(default)]
49    pub pending: Option<bool>,
50
51    /// Total permissions of the member in the channel (including overwrites).
52    #[serde(default)]
53    pub permissions: Option<crate::permissions::Permissions>,
54
55    /// When the user's timeout will expire (ISO8601 timestamp).
56    #[serde(default)]
57    pub communication_disabled_until: Option<TitanString<'a>>,
58}
59
60/// A Discord role.
61#[derive(Debug, Clone, Deserialize, Serialize)]
62pub struct Role<'a> {
63    /// Role ID.
64    pub id: Snowflake,
65
66    /// Role name.
67    pub name: TitanString<'a>,
68
69    /// Integer representation of hex color code.
70    pub color: u32,
71
72    /// If this role is pinned in the user listing.
73    pub hoist: bool,
74
75    /// Role icon hash.
76    #[serde(default)]
77    pub icon: Option<TitanString<'a>>,
78
79    /// Role unicode emoji.
80    #[serde(default)]
81    pub unicode_emoji: Option<TitanString<'a>>,
82
83    /// Position of this role.
84    pub position: i32,
85
86    /// Permission bit set.
87    pub permissions: crate::permissions::Permissions,
88
89    /// Whether this role is managed by an integration.
90    pub managed: bool,
91
92    /// Whether this role is mentionable.
93    pub mentionable: bool,
94
95    /// The tags this role has.
96    #[serde(default)]
97    pub tags: Option<RoleTags>,
98
99    /// Role flags as a bitfield.
100    #[serde(default)]
101    pub flags: u64,
102}
103
104/// Tags for a role.
105#[derive(Debug, Clone, Deserialize, Serialize)]
106pub struct RoleTags {
107    /// The ID of the bot this role belongs to.
108    #[serde(default)]
109    pub bot_id: Option<Snowflake>,
110
111    /// The ID of the integration this role belongs to.
112    #[serde(default)]
113    pub integration_id: Option<Snowflake>,
114
115    /// Whether this is the guild's premium subscriber role.
116    #[serde(default)]
117    pub premium_subscriber: Option<()>,
118
119    /// The ID of the subscription listing for this role.
120    #[serde(default)]
121    pub subscription_listing_id: Option<Snowflake>,
122
123    /// Whether this role is available for purchase.
124    #[serde(default)]
125    pub available_for_purchase: Option<()>,
126
127    /// Whether this role is a guild's linked role.
128    #[serde(default)]
129    pub guild_connections: Option<()>,
130}
131
132/// An emoji in a guild.
133#[derive(Debug, Clone, Deserialize, Serialize)]
134pub struct Emoji<'a> {
135    /// Emoji ID.
136    #[serde(default)]
137    pub id: Option<Snowflake>,
138
139    /// Emoji name (can be null for reaction emoji objects).
140    #[serde(default)]
141    pub name: Option<TitanString<'a>>,
142
143    /// Roles allowed to use this emoji.
144    #[serde(default)]
145    pub roles: smallvec::SmallVec<[Snowflake; 5]>,
146
147    /// User that created this emoji.
148    #[serde(default)]
149    pub user: Option<super::User<'a>>,
150
151    /// Whether this emoji must be wrapped in colons.
152    #[serde(default)]
153    pub require_colons: bool,
154
155    /// Whether this emoji is managed.
156    #[serde(default)]
157    pub managed: bool,
158
159    /// Whether this emoji is animated.
160    #[serde(default)]
161    pub animated: bool,
162
163    /// Whether this emoji can be used.
164    #[serde(default)]
165    pub available: bool,
166}
167
168impl<'a> Emoji<'a> {
169    /// Returns the URL of the emoji.
170    pub fn url(&self) -> Option<String> {
171        self.id.map(|id| {
172            let ext = if self.animated { "gif" } else { "png" };
173            format!("https://cdn.discordapp.com/emojis/{}.{}", id, ext)
174        })
175    }
176}
177
178/// A sticker in a guild.
179#[derive(Debug, Clone, Deserialize, Serialize)]
180pub struct Sticker<'a> {
181    /// ID of the sticker.
182    pub id: Snowflake,
183
184    /// ID of the pack the sticker is from.
185    #[serde(default)]
186    pub pack_id: Option<Snowflake>,
187
188    /// Name of the sticker.
189    pub name: TitanString<'a>,
190
191    /// Description of the sticker.
192    #[serde(default)]
193    pub description: Option<TitanString<'a>>,
194
195    /// Autocomplete/suggestion tags for the sticker.
196    #[serde(default)]
197    pub tags: TitanString<'a>,
198
199    /// Type of sticker.
200    #[serde(rename = "type")]
201    pub sticker_type: u8,
202
203    /// Format type of the sticker (1=PNG, 2=APNG, 3=LOTTIE, 4=GIF).
204    #[serde(rename = "format_type")]
205    pub format_type: u8,
206
207    // ...
208    /// The user that uploaded the guild sticker.
209    #[serde(default)]
210    pub user: Option<super::User<'a>>,
211
212    /// The standard sticker's sort order within its pack.
213    #[serde(default)]
214    pub sort_value: Option<u32>,
215}
216
217impl<'a> Sticker<'a> {
218    /// Returns the URL of the sticker.
219    pub fn url(&self) -> String {
220        // Sticker formats: 1 (PNG), 2 (APNG), 3 (LOTTIE), 4 (GIF)
221        // CDN: https://cdn.discordapp.com/stickers/{sticker_id}.{png|json|gif}
222        let ext = match self.format_type {
223            1 => "png",  // PNG
224            2 => "png",  // APNG -> png
225            3 => "json", // Lottie
226            4 => "gif",  // GIF
227            _ => "png",
228        };
229        format!("https://cdn.discordapp.com/stickers/{}.{}", self.id, ext)
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236
237    #[test]
238    fn test_guild_member_deserialize() {
239        let json = r#"{
240            "nick": "Test Nick",
241            "roles": ["123456789"],
242            "joined_at": "2021-01-01T00:00:00.000Z",
243            "deaf": false,
244            "mute": false,
245            "flags": 0
246        }"#;
247
248        let member: GuildMember = crate::json::from_str(json).unwrap();
249        assert_eq!(member.nick, Some(TitanString::Borrowed("Test Nick")));
250    }
251
252    #[test]
253    fn test_role_deserialize() {
254        let json = r#"{
255            "id": "123",
256            "name": "Admin",
257            "color": 16711680,
258            "hoist": true,
259            "position": 10,
260            "permissions": "8",
261            "managed": false,
262            "mentionable": true,
263            "flags": 0
264        }"#;
265
266        let role: Role = crate::json::from_str(json).unwrap();
267        assert_eq!(role.name, "Admin");
268        assert_eq!(role.color, 16711680);
269    }
270}