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