Skip to main content

revolt_database/models/server_members/
model.rs

1use iso8601_timestamp::Timestamp;
2use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
3use revolt_result::{create_error, Result};
4
5use crate::{
6    events::client::EventV1, util::permissions::DatabasePermissionQuery, Channel, Database, File,
7    Server, SystemMessage, User,
8};
9
10fn default_true() -> bool {
11    true
12}
13
14fn is_true(x: &bool) -> bool {
15    *x
16}
17
18auto_derived_partial!(
19    /// Server Member
20    pub struct Member {
21        /// Unique member id
22        #[serde(rename = "_id")]
23        pub id: MemberCompositeKey,
24
25        /// Time at which this user joined the server
26        pub joined_at: Timestamp,
27
28        /// Member's nickname
29        #[serde(skip_serializing_if = "Option::is_none")]
30        pub nickname: Option<String>,
31        /// Member's pronouns
32        #[serde(skip_serializing_if = "Option::is_none")]
33        pub pronouns: Option<String>,
34        /// Avatar attachment
35        #[serde(skip_serializing_if = "Option::is_none")]
36        pub avatar: Option<File>,
37
38        /// Member's roles
39        #[serde(skip_serializing_if = "Vec::is_empty", default)]
40        pub roles: Vec<String>,
41        /// Timestamp this member is timed out until
42        #[serde(skip_serializing_if = "Option::is_none")]
43        pub timeout: Option<Timestamp>,
44
45        /// Whether the member is server-wide voice muted
46        #[serde(skip_serializing_if = "is_true", default = "default_true")]
47        pub can_publish: bool,
48        /// Whether the member is server-wide voice deafened
49        #[serde(skip_serializing_if = "is_true", default = "default_true")]
50        pub can_receive: bool,
51        // This value only exists in the database, not the models.
52        // If it is not-None, the database layer should return None to member fetching queries.
53        // pub pending_deletion_at: Option<Timestamp>
54    },
55    "PartialMember"
56);
57
58auto_derived!(
59    /// Composite primary key consisting of server and user id
60    #[derive(Hash, Default)]
61    pub struct MemberCompositeKey {
62        /// Server Id
63        pub server: String,
64        /// User Id
65        pub user: String,
66    }
67
68    /// Optional fields on server member object
69    pub enum FieldsMember {
70        Nickname,
71        Pronouns,
72        Avatar,
73        Roles,
74        Timeout,
75        CanReceive,
76        CanPublish,
77        JoinedAt,
78        VoiceChannel,
79    }
80
81    /// Member removal intention
82    pub enum RemovalIntention {
83        Leave,
84        Kick,
85        Ban,
86    }
87);
88
89impl Default for Member {
90    fn default() -> Self {
91        Self {
92            id: Default::default(),
93            joined_at: Timestamp::now_utc(),
94            nickname: None,
95            pronouns: None,
96            avatar: None,
97            roles: vec![],
98            timeout: None,
99            can_publish: true,
100            can_receive: true,
101        }
102    }
103}
104
105#[allow(clippy::disallowed_methods)]
106impl Member {
107    /// Create a new member in a server
108    pub async fn create(
109        db: &Database,
110        server: &Server,
111        user: &User,
112        channels: Option<Vec<Channel>>,
113    ) -> Result<(Member, Vec<Channel>)> {
114        if db.fetch_ban(&server.id, &user.id).await.is_ok() {
115            return Err(create_error!(Banned));
116        }
117
118        if db.fetch_member(&server.id, &user.id).await.is_ok() {
119            return Err(create_error!(AlreadyInServer));
120        }
121
122        let mut member = Member {
123            id: MemberCompositeKey {
124                server: server.id.to_string(),
125                user: user.id.to_string(),
126            },
127            ..Default::default()
128        };
129
130        if let Some(updated) = db.insert_or_merge_member(&member).await? {
131            member = updated;
132        }
133
134        let should_fetch = channels.is_none();
135        let mut channels = channels.unwrap_or_default();
136
137        if should_fetch {
138            let query = DatabasePermissionQuery::new(db, user).server(server);
139            let existing_channels = db.fetch_channels(&server.channels).await?;
140
141            for channel in existing_channels {
142                let mut channel_query = query.clone().channel(&channel);
143
144                if calculate_channel_permissions(&mut channel_query)
145                    .await
146                    .has_channel_permission(ChannelPermission::ViewChannel)
147                {
148                    channels.push(channel);
149                }
150            }
151        }
152
153        let emojis = db.fetch_emoji_by_parent_id(&server.id).await?;
154
155        #[allow(unused_mut)]
156        let mut voice_states = Vec::new();
157
158        #[cfg(feature = "voice")]
159        for channel in &channels {
160            if let Ok(Some(voice_state)) = crate::voice::get_channel_voice_state(
161                &crate::voice::UserVoiceChannel::from_channel(channel),
162            )
163            .await
164            {
165                voice_states.push(voice_state)
166            }
167        }
168
169        EventV1::ServerMemberJoin {
170            id: server.id.clone(),
171            user: user.id.clone(),
172            member: member.clone().into(),
173        }
174        .p(server.id.clone())
175        .await;
176
177        EventV1::ServerCreate {
178            id: server.id.clone(),
179            server: server.clone().into(db).await,
180            channels: channels
181                .clone()
182                .into_iter()
183                .map(|channel| channel.into())
184                .collect(),
185            emojis: emojis.into_iter().map(|emoji| emoji.into()).collect(),
186            voice_states,
187        }
188        .private(user.id.clone())
189        .await;
190
191        if let Some(id) = server
192            .system_messages
193            .as_ref()
194            .and_then(|x| x.user_joined.as_ref())
195        {
196            SystemMessage::UserJoined {
197                id: user.id.clone(),
198            }
199            .into_message(id.to_string())
200            .send_without_notifications(db, None, None, false, false, false)
201            .await
202            .ok();
203        }
204
205        Ok((member, channels))
206    }
207
208    /// Update member data
209    pub async fn update(
210        &mut self,
211        db: &Database,
212        partial: PartialMember,
213        remove: Vec<FieldsMember>,
214    ) -> Result<()> {
215        for field in &remove {
216            self.remove_field(field);
217        }
218
219        self.apply_options(partial.clone());
220
221        db.update_member(&self.id, &partial, remove.clone()).await?;
222
223        EventV1::ServerMemberUpdate {
224            id: self.id.clone().into(),
225            data: partial.into(),
226            clear: remove.into_iter().map(|field| field.into()).collect(),
227        }
228        .p(self.id.server.clone())
229        .await;
230
231        Ok(())
232    }
233
234    pub fn remove_field(&mut self, field: &FieldsMember) {
235        match field {
236            FieldsMember::JoinedAt => {}
237            FieldsMember::Avatar => self.avatar = None,
238            FieldsMember::Nickname => self.nickname = None,
239            FieldsMember::Pronouns => self.pronouns = None,
240            FieldsMember::Roles => self.roles.clear(),
241            FieldsMember::Timeout => self.timeout = None,
242            FieldsMember::CanReceive => self.can_receive = true,
243            FieldsMember::CanPublish => self.can_publish = true,
244            FieldsMember::VoiceChannel => {}
245        }
246    }
247
248    /// Generates a PartialMember containing the data which has changed in an update
249    pub fn generate_diff(&self, partial: &PartialMember, remove: &[FieldsMember]) -> PartialMember {
250        let mut before = PartialMember::default();
251
252        generate_diff!(
253            self, before, partial, remove,
254            (
255                (FieldsMember::Nickname) nickname,
256                (FieldsMember::Avatar) avatar,
257                (FieldsMember::Timeout) timeout,
258                (FieldsMember::Pronouns) pronouns,
259                ((default) FieldsMember::Roles) roles,
260                ((default) FieldsMember::CanPublish) can_publish,
261                ((default) FieldsMember::CanReceive) can_receive,
262            )
263        );
264
265        before
266    }
267
268    /// Get this user's current ranking
269    pub fn get_ranking(&self, server: &Server) -> i64 {
270        let mut value = i64::MAX;
271        for role in &self.roles {
272            if let Some(role) = server.roles.get(role) {
273                if role.rank < value {
274                    value = role.rank;
275                }
276            }
277        }
278
279        value
280    }
281
282    /// Check whether this member is in timeout
283    pub fn in_timeout(&self) -> bool {
284        if let Some(timeout) = self.timeout {
285            *timeout > *Timestamp::now_utc()
286        } else {
287            false
288        }
289    }
290
291    /// Remove member from server
292    pub async fn remove(
293        &self,
294        db: &Database,
295        server: &Server,
296        intention: RemovalIntention,
297        silent: bool,
298    ) -> Result<()> {
299        db.soft_delete_member(&self.id).await?;
300
301        EventV1::ServerMemberLeave {
302            id: self.id.server.to_string(),
303            user: self.id.user.to_string(),
304            reason: intention.clone().into(),
305        }
306        .p(self.id.server.to_string())
307        .await;
308
309        if !silent {
310            if let Some(id) = server
311                .system_messages
312                .as_ref()
313                .and_then(|x| match intention {
314                    RemovalIntention::Leave => x.user_left.as_ref(),
315                    RemovalIntention::Kick => x.user_kicked.as_ref(),
316                    RemovalIntention::Ban => x.user_banned.as_ref(),
317                })
318            {
319                match intention {
320                    RemovalIntention::Leave => SystemMessage::UserLeft {
321                        id: self.id.user.clone(),
322                    },
323                    RemovalIntention::Kick => SystemMessage::UserKicked {
324                        id: self.id.user.clone(),
325                    },
326                    RemovalIntention::Ban => SystemMessage::UserBanned {
327                        id: self.id.user.clone(),
328                    },
329                }
330                .into_message(id.to_string())
331                // TODO: support notifications here in the future?
332                .send_without_notifications(db, None, None, false, false, false)
333                .await
334                .ok();
335            }
336        }
337
338        Ok(())
339    }
340}
341
342#[cfg(test)]
343mod tests {
344    use iso8601_timestamp::{Duration, Timestamp};
345    use revolt_models::v0::DataCreateServer;
346
347    use crate::{Member, PartialMember, RemovalIntention, Server, User};
348
349    #[tokio::test]
350    async fn muted_member_rejoin() {
351        database_test!(|db| async move {
352            match db {
353                crate::Database::Reference(_) => return,
354                crate::Database::MongoDb(_) => (),
355            }
356            let owner = User::create(&db, "Server Owner".to_string(), None, None)
357                .await
358                .unwrap();
359
360            let kickable_user = User::create(&db, "Member".to_string(), None, None)
361                .await
362                .unwrap();
363
364            let server = Server::create(
365                &db,
366                DataCreateServer {
367                    name: "Server".to_string(),
368                    description: None,
369                    nsfw: None,
370                },
371                &owner,
372                false,
373            )
374            .await
375            .unwrap()
376            .0;
377
378            Member::create(&db, &server, &owner, None).await.unwrap();
379            let mut kickable_member = Member::create(&db, &server, &kickable_user, None)
380                .await
381                .unwrap()
382                .0;
383
384            kickable_member
385                .update(
386                    &db,
387                    PartialMember {
388                        timeout: Some(Timestamp::now_utc() + Duration::minutes(5)),
389                        ..Default::default()
390                    },
391                    vec![],
392                )
393                .await
394                .unwrap();
395
396            assert!(kickable_member.in_timeout());
397
398            kickable_member
399                .remove(&db, &server, RemovalIntention::Kick, false)
400                .await
401                .unwrap();
402
403            let kickable_member = Member::create(&db, &server, &kickable_user, None)
404                .await
405                .unwrap()
406                .0;
407
408            assert!(kickable_member.in_timeout())
409        });
410    }
411}