Skip to main content

revolt_database/models/users/ops/
mongodb.rs

1use ::mongodb::options::{Collation, CollationStrength, FindOneOptions, FindOptions};
2use futures::StreamExt;
3use revolt_result::Result;
4
5use crate::DocumentId;
6use crate::IntoDocumentPath;
7use crate::MongoDb;
8use crate::{FieldsUser, PartialUser, RelationshipStatus, User};
9
10use super::AbstractUsers;
11
12static COL: &str = "users";
13
14#[async_trait]
15impl AbstractUsers for MongoDb {
16    /// Insert a new user into the database
17    async fn insert_user(&self, user: &User) -> Result<()> {
18        query!(self, insert_one, COL, &user).map(|_| ())
19    }
20
21    /// Fetch a user from the database
22    async fn fetch_user(&self, id: &str) -> Result<User> {
23        query!(self, find_one_by_id, COL, id)?.ok_or_else(|| create_error!(NotFound))
24    }
25
26    /// Fetch a user from the database by their username
27    async fn fetch_user_by_username(&self, username: &str, discriminator: &str) -> Result<User> {
28        query!(
29            self,
30            find_one_with_options,
31            COL,
32            doc! {
33                "username": username,
34                "discriminator": discriminator
35            },
36            FindOneOptions::builder()
37                .collation(
38                    Collation::builder()
39                        .locale("en")
40                        .strength(CollationStrength::Secondary)
41                        .build(),
42                )
43                .build()
44        )?
45        .ok_or_else(|| create_error!(NotFound))
46    }
47
48    /// Fetch multiple users by their ids
49    async fn fetch_users<'a>(&self, ids: &'a [String]) -> Result<Vec<User>> {
50        Ok(self
51            .col::<User>(COL)
52            .find(doc! {
53                "_id": {
54                    "$in": ids
55                }
56            })
57            .await
58            .map_err(|_| create_database_error!("find", COL))?
59            .filter_map(|s| async {
60                if cfg!(debug_assertions) {
61                    Some(s.unwrap())
62                } else {
63                    s.ok()
64                }
65            })
66            .collect()
67            .await)
68    }
69
70    /// Fetch all discriminators in use for a username
71    async fn fetch_discriminators_in_use(&self, username: &str) -> Result<Vec<String>> {
72        #[derive(Deserialize)]
73        struct UserDocument {
74            discriminator: String,
75        }
76
77        Ok(self
78            .col::<UserDocument>(COL)
79            .find(doc! {
80                "username": username
81            })
82            .with_options(
83                FindOptions::builder()
84                    .collation(
85                        Collation::builder()
86                            .locale("en")
87                            .strength(CollationStrength::Secondary)
88                            .build(),
89                    )
90                    .projection(doc! { "_id": 0, "discriminator": 1 })
91                    .build(),
92            )
93            .await
94            .map_err(|_| create_database_error!("find", COL))?
95            .filter_map(|s| async { s.ok() })
96            .collect::<Vec<UserDocument>>()
97            .await
98            .into_iter()
99            .map(|user| user.discriminator)
100            .collect::<Vec<String>>())
101    }
102
103    /// Fetch ids of users that both users are friends with
104    async fn fetch_mutual_user_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>> {
105        Ok(self
106            .col::<DocumentId>(COL)
107            .find(doc! {
108                "$and": [
109                    { "relations": { "$elemMatch": { "_id": &user_a, "status": "Friend" } } },
110                    { "relations": { "$elemMatch": { "_id": &user_b, "status": "Friend" } } }
111                ]
112            })
113            .with_options(FindOptions::builder().projection(doc! { "_id": 1 }).build())
114            .await
115            .map_err(|_| create_database_error!("find", COL))?
116            .filter_map(|s| async { s.ok() })
117            .map(|user| user.id)
118            .collect()
119            .await)
120    }
121
122    /// Fetch ids of channels that both users are in
123    async fn fetch_mutual_channel_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>> {
124        Ok(self
125            .col::<DocumentId>("channels")
126            .find(doc! {
127                "channel_type": {
128                    "$in": ["Group", "DirectMessage"]
129                },
130                "recipients": {
131                    "$all": [ user_a, user_b ]
132                }
133            })
134            .with_options(FindOptions::builder().projection(doc! { "_id": 1 }).build())
135            .await
136            .map_err(|_| create_database_error!("find", "channels"))?
137            .filter_map(|s| async { s.ok() })
138            .map(|user| user.id)
139            .collect()
140            .await)
141    }
142
143    /// Fetch ids of servers that both users share
144    async fn fetch_mutual_server_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>> {
145        Ok(self
146            .col::<DocumentId>("server_members")
147            .aggregate(vec![
148                doc! {
149                    "$match": {
150                        "_id.user": user_a
151                    }
152                },
153                doc! {
154                    "$lookup": {
155                        "from": "server_members",
156                        "as": "members",
157                        "let": {
158                            "server": "$_id.server"
159                        },
160                        "pipeline": [
161                            {
162                                "$match": {
163                                    "$expr": {
164                                        "$and": [
165                                            { "$eq": [ "$_id.user", user_b ] },
166                                            { "$eq": [ "$_id.server", "$$server" ] }
167                                        ]
168                                    }
169                                }
170                            }
171                        ]
172                    }
173                },
174                doc! {
175                    "$match": {
176                        "members": {
177                            "$size": 1_i32
178                        }
179                    }
180                },
181                doc! {
182                    "$project": {
183                        "_id": "$_id.server"
184                    }
185                },
186            ])
187            .await
188            .map_err(|_| create_database_error!("aggregate", "server_members"))?
189            .filter_map(|s| async { s.ok() })
190            .filter_map(|doc| async move { doc.get_str("_id").map(|id| id.to_string()).ok() })
191            .collect()
192            .await)
193    }
194
195    /// Update a user by their id given some data
196    async fn update_user(
197        &self,
198        id: &str,
199        partial: &PartialUser,
200        remove: Vec<FieldsUser>,
201    ) -> Result<()> {
202        if remove.contains(&FieldsUser::StatusText) && partial.status.is_some() {
203            // stupid-ass workaround to fix mongo conflicting the same item
204            let _: Result<()> = query!(
205                self,
206                update_one_by_id,
207                COL,
208                id,
209                PartialUser {
210                    ..Default::default()
211                },
212                remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
213                None
214            )
215            .map(|_| ());
216
217            query!(self, update_one_by_id, COL, id, partial, vec![], None).map(|_| ())
218        } else {
219            query!(
220                self,
221                update_one_by_id,
222                COL,
223                id,
224                partial,
225                remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
226                None
227            )
228            .map(|_| ())
229        }
230    }
231
232    /// Set relationship with another user
233    ///
234    /// This should use pull_relationship if relationship is None.
235    async fn set_relationship(
236        &self,
237        user_id: &str,
238        target_id: &str,
239        relationship: &RelationshipStatus,
240    ) -> Result<()> {
241        if let RelationshipStatus::None = relationship {
242            return self.pull_relationship(user_id, target_id).await;
243        }
244
245        self.col::<User>(COL)
246            .update_one(
247                doc! {
248                    "_id": user_id
249                },
250                vec![doc! {
251                    "$set": {
252                        "relations": {
253                            "$concatArrays": [
254                                {
255                                    "$ifNull": [
256                                        {
257                                            "$filter": {
258                                                "input": "$relations",
259                                                "cond": {
260                                                    "$ne": [
261                                                        "$$this._id",
262                                                        target_id
263                                                    ]
264                                                }
265                                            }
266                                        },
267                                        []
268                                    ]
269                                },
270                                [
271                                    {
272                                        "_id": target_id,
273                                        "status": format!("{relationship:?}")
274                                    }
275                                ]
276                            ]
277                        }
278                    }
279                }],
280            )
281            .await
282            .map(|_| ())
283            .map_err(|_| create_database_error!("update_one", "user"))
284    }
285
286    /// Remove relationship with another user
287    async fn pull_relationship(&self, user_id: &str, target_id: &str) -> Result<()> {
288        self.col::<User>(COL)
289            .update_one(
290                doc! {
291                    "_id": user_id
292                },
293                doc! {
294                    "$pull": {
295                        "relations": {
296                            "_id": target_id
297                        }
298                    }
299                },
300            )
301            .await
302            .map(|_| ())
303            .map_err(|_| create_database_error!("update_one", COL))
304    }
305
306    /// Delete a user by their id
307    async fn delete_user(&self, id: &str) -> Result<()> {
308        query!(self, delete_one_by_id, COL, id).map(|_| ())
309    }
310
311    /// Removes all relationships with the user from the list of users
312    async fn clear_user_relationships(&self, target_id: &str, user_ids: Vec<String>) -> Result<()> {
313        self.col::<User>(COL)
314            .update_many(
315                doc! { "_id": { "$in": user_ids } },
316                doc! {
317                    "$pull": {
318                        "relations": {
319                            "_id": target_id.to_string()
320                        }
321                    }
322                },
323            )
324            .await
325            .map(|_| ())
326            .map_err(|_| create_database_error!("bulk_write", COL))
327    }
328}
329
330impl IntoDocumentPath for FieldsUser {
331    fn as_path(&self) -> Option<&'static str> {
332        Some(match self {
333            FieldsUser::Avatar => "avatar",
334            FieldsUser::ProfileBackground => "profile.background",
335            FieldsUser::ProfileContent => "profile.content",
336            FieldsUser::StatusPresence => "status.presence",
337            FieldsUser::StatusText => "status.text",
338            FieldsUser::DisplayName => "display_name",
339            FieldsUser::Pronouns => "pronouns",
340            FieldsUser::Suspension => "suspended_until",
341            FieldsUser::None => "none",
342        })
343    }
344}