tetratto_core/database/
memberships.rs

1use oiseau::cache::Cache;
2use crate::model::auth::AchievementName;
3use crate::model::communities::Community;
4use crate::model::requests::{ActionRequest, ActionType};
5use crate::model::{
6    Error, Result,
7    auth::User,
8    communities::{CommunityJoinAccess, CommunityMembership},
9    communities_permissions::CommunityPermission,
10    permissions::FinePermission,
11};
12use crate::{auto_method, DataManager};
13
14use oiseau::{PostgresRow, execute, get, query_row, query_rows, params};
15
16impl DataManager {
17    /// Get a [`CommunityMembership`] from an SQL row.
18    pub(crate) fn get_membership_from_row(x: &PostgresRow) -> CommunityMembership {
19        CommunityMembership {
20            id: get!(x->0(i64)) as usize,
21            created: get!(x->1(i64)) as usize,
22            owner: get!(x->2(i64)) as usize,
23            community: get!(x->3(i64)) as usize,
24            role: CommunityPermission::from_bits(get!(x->4(i32)) as u32).unwrap(),
25        }
26    }
27
28    auto_method!(get_membership_by_id()@get_membership_from_row -> "SELECT * FROM memberships WHERE id = $1" --name="community membership" --returns=CommunityMembership --cache-key-tmpl="atto.membership:{}");
29
30    /// Replace a list of community memberships with the proper community.
31    pub async fn fill_communities(&self, list: Vec<CommunityMembership>) -> Result<Vec<Community>> {
32        let mut communities: Vec<Community> = Vec::new();
33
34        for membership in &list {
35            if membership.community == 0 {
36                continue;
37            }
38
39            communities.push(self.get_community_by_id(membership.community).await?);
40        }
41
42        Ok(communities)
43    }
44
45    /// Replace a list of community memberships with the proper user.
46    pub async fn fill_users(
47        &self,
48        list: Vec<CommunityMembership>,
49    ) -> Result<Vec<(CommunityMembership, User)>> {
50        let mut users: Vec<(CommunityMembership, User)> = Vec::new();
51        for membership in list {
52            let owner = membership.owner;
53            users.push((membership, self.get_user_by_id(owner).await?));
54        }
55        Ok(users)
56    }
57
58    /// Get a community membership by `owner` and `community`.
59    pub async fn get_membership_by_owner_community(
60        &self,
61        owner: usize,
62        community: usize,
63    ) -> Result<CommunityMembership> {
64        let conn = match self.0.connect().await {
65            Ok(c) => c,
66            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
67        };
68
69        let res = query_row!(
70            &conn,
71            "SELECT * FROM memberships WHERE owner = $1 AND community = $2",
72            &[&(owner as i64), &(community as i64)],
73            |x| { Ok(Self::get_membership_from_row(x)) }
74        );
75
76        if res.is_err() {
77            // return Err(Error::GeneralNotFound("community membership".to_string()));
78            return Ok(CommunityMembership::new(
79                owner,
80                community,
81                CommunityPermission::DEFAULT,
82            ));
83        }
84
85        Ok(res.unwrap())
86    }
87
88    /// Get a community membership by `owner` and `community`.
89    pub async fn get_membership_by_owner_community_no_void(
90        &self,
91        owner: usize,
92        community: usize,
93    ) -> Result<CommunityMembership> {
94        let conn = match self.0.connect().await {
95            Ok(c) => c,
96            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
97        };
98
99        let res = query_row!(
100            &conn,
101            "SELECT * FROM memberships WHERE owner = $1 AND community = $2",
102            &[&(owner as i64), &(community as i64)],
103            |x| { Ok(Self::get_membership_from_row(x)) }
104        );
105
106        if res.is_err() {
107            return Err(Error::GeneralNotFound("community membership".to_string()));
108        }
109
110        Ok(res.unwrap())
111    }
112
113    /// Get all community memberships by `owner`.
114    pub async fn get_memberships_by_owner(&self, owner: usize) -> Result<Vec<CommunityMembership>> {
115        let conn = match self.0.connect().await {
116            Ok(c) => c,
117            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
118        };
119
120        let res = query_rows!(
121            &conn,
122            // 33 = banned, 65 = pending membership
123            "SELECT * FROM memberships WHERE owner = $1 AND NOT role = 33 AND NOT role = 65 ORDER BY created DESC",
124            &[&(owner as i64)],
125            |x| { Self::get_membership_from_row(x) }
126        );
127
128        if res.is_err() {
129            return Err(Error::GeneralNotFound("community membership".to_string()));
130        }
131
132        Ok(res.unwrap())
133    }
134
135    /// Get all community memberships by `community`.
136    pub async fn get_memberships_by_community(
137        &self,
138        community: usize,
139        community_owner: usize, // the owner is always shown at the top of the first page
140        batch: usize,
141        page: usize,
142    ) -> Result<Vec<CommunityMembership>> {
143        let conn = match self.0.connect().await {
144            Ok(c) => c,
145            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
146        };
147
148        let res = query_rows!(
149            &conn,
150            // 33 = banned, 65 = pending membership
151            "SELECT * FROM memberships WHERE community = $1 AND NOT owner = $2 AND NOT role = 33 AND NOT role = 65 ORDER BY created DESC LIMIT $3 OFFSET $4",
152            &[
153                &(community as i64),
154                &(community_owner as i64),
155                &(batch as i64),
156                &((page * batch) as i64)
157            ],
158            |x| { Self::get_membership_from_row(x) }
159        );
160
161        if res.is_err() {
162            return Err(Error::GeneralNotFound("community membership".to_string()));
163        }
164
165        Ok(res.unwrap())
166    }
167
168    /// Create a new community membership in the database.
169    ///
170    /// # Arguments
171    /// * `data` - a mock [`CommunityMembership`] object to insert
172    #[async_recursion::async_recursion]
173    pub async fn create_membership(
174        &self,
175        data: CommunityMembership,
176        user: &User,
177    ) -> Result<String> {
178        // make sure membership doesn't already exist
179        if self
180            .get_membership_by_owner_community_no_void(data.owner, data.community)
181            .await
182            .is_ok()
183        {
184            return Err(Error::MiscError("Already joined community".to_string()));
185        }
186
187        // check permission
188        let community = self.get_community_by_id(data.community).await?;
189
190        match community.join_access {
191            CommunityJoinAccess::Nobody => return Err(Error::NotAllowed),
192            CommunityJoinAccess::Request => {
193                if !data.role.check(CommunityPermission::REQUESTED) {
194                    let mut data = data.clone();
195                    data.role = CommunityPermission::DEFAULT | CommunityPermission::REQUESTED;
196
197                    // create join request
198                    self.create_request(ActionRequest::with_id(
199                        data.owner,
200                        community.owner,
201                        ActionType::CommunityJoin,
202                        community.id,
203                        None,
204                    ))
205                    .await?;
206
207                    // ...
208                    return self.create_membership(data, user).await;
209                }
210            }
211            _ => (),
212        }
213
214        // ...
215        let conn = match self.0.connect().await {
216            Ok(c) => c,
217            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
218        };
219
220        let res = execute!(
221            &conn,
222            "INSERT INTO memberships VALUES ($1, $2, $3, $4, $5)",
223            params![
224                &(data.id as i64),
225                &(data.created as i64),
226                &(data.owner as i64),
227                &(data.community as i64),
228                &(data.role.bits() as i32),
229            ]
230        );
231
232        if let Err(e) = res {
233            return Err(Error::DatabaseError(e.to_string()));
234        }
235
236        if !data.role.check(CommunityPermission::REQUESTED) {
237            // users who are just a requesting to join do not count towards the member count
238            self.incr_community_member_count(data.community)
239                .await
240                .unwrap();
241        }
242
243        Ok(if data.role.check(CommunityPermission::REQUESTED) {
244            "Join request sent".to_string()
245        } else {
246            self.add_achievement(
247                &mut user.clone(),
248                AchievementName::JoinCommunity.into(),
249                true,
250            )
251            .await?;
252
253            "Community joined".to_string()
254        })
255    }
256
257    /// Delete a membership given its `id`
258    pub async fn delete_membership(&self, id: usize, user: &User) -> Result<()> {
259        let y = self.get_membership_by_id(id).await?;
260
261        if user.id != y.owner {
262            // pull other user's membership status
263            if let Ok(z) = self
264                .get_membership_by_owner_community_no_void(user.id, y.community)
265                .await
266            {
267                // somebody with MANAGE_ROLES _and_ a higher role number can remove us
268                if (!z.role.check(CommunityPermission::MANAGE_ROLES) | (z.role < y.role))
269                    && !z.role.check(CommunityPermission::ADMINISTRATOR)
270                {
271                    return Err(Error::NotAllowed);
272                }
273            } else if !user.permissions.check(FinePermission::MANAGE_MEMBERSHIPS) {
274                return Err(Error::NotAllowed);
275            }
276        }
277
278        let conn = match self.0.connect().await {
279            Ok(c) => c,
280            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
281        };
282
283        let res = execute!(
284            &conn,
285            "DELETE FROM memberships WHERE id = $1",
286            &[&(id as i64)]
287        );
288
289        if let Err(e) = res {
290            return Err(Error::DatabaseError(e.to_string()));
291        }
292
293        self.0.1.remove(format!("atto.membership:{}", id)).await;
294
295        self.decr_community_member_count(y.community).await.unwrap();
296
297        Ok(())
298    }
299
300    /// Delete a membership given its `id`
301    pub async fn delete_membership_force(&self, id: usize) -> Result<()> {
302        let y = self.get_membership_by_id(id).await?;
303
304        let conn = match self.0.connect().await {
305            Ok(c) => c,
306            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
307        };
308
309        let res = execute!(
310            &conn,
311            "DELETE FROM memberships WHERE id = $1",
312            &[&(id as i64)]
313        );
314
315        if let Err(e) = res {
316            return Err(Error::DatabaseError(e.to_string()));
317        }
318
319        self.0.1.remove(format!("atto.membership:{}", id)).await;
320
321        self.decr_community_member_count(y.community).await.unwrap();
322
323        Ok(())
324    }
325
326    /// Update a membership's role given its `id`
327    pub async fn update_membership_role(
328        &self,
329        id: usize,
330        new_role: CommunityPermission,
331    ) -> Result<()> {
332        let conn = match self.0.connect().await {
333            Ok(c) => c,
334            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
335        };
336
337        let res = execute!(
338            &conn,
339            "UPDATE memberships SET role = $1 WHERE id = $2",
340            params![&(new_role.bits() as i32), &(id as i64)]
341        );
342
343        if let Err(e) = res {
344            return Err(Error::DatabaseError(e.to_string()));
345        }
346
347        self.0.1.remove(format!("atto.membership:{}", id)).await;
348
349        Ok(())
350    }
351}