1use super::common::NAME_REGEX;
2use oiseau::cache::Cache;
3use crate::{
4 auto_method, DataManager,
5 model::{
6 Error, Result,
7 auth::User,
8 communities::{
9 CommunityReadAccess, CommunityWriteAccess, ForumTopic, Community, CommunityContext,
10 CommunityJoinAccess, CommunityMembership,
11 },
12 permissions::{FinePermission, SecondaryPermission},
13 communities_permissions::CommunityPermission,
14 },
15};
16use pathbufd::PathBufD;
17use std::{
18 fs::{exists, remove_file},
19 collections::HashMap,
20};
21
22use oiseau::{PostgresRow, execute, get, query_row, query_rows, params};
23
24impl DataManager {
25 pub(crate) fn get_community_from_row(x: &PostgresRow) -> Community {
27 Community {
28 id: get!(x->0(i64)) as usize,
29 created: get!(x->1(i64)) as usize,
30 title: get!(x->2(String)),
31 context: serde_json::from_str(&get!(x->3(String))).unwrap(),
32 owner: get!(x->4(i64)) as usize,
33 read_access: serde_json::from_str(&get!(x->5(String))).unwrap(),
34 write_access: serde_json::from_str(&get!(x->6(String))).unwrap(),
35 join_access: serde_json::from_str(&get!(x->7(String))).unwrap(),
36 likes: get!(x->8(i32)) as isize,
37 dislikes: get!(x->9(i32)) as isize,
38 member_count: get!(x->10(i32)) as usize,
39 is_forge: get!(x->11(i32)) as i8 == 1,
40 post_count: get!(x->12(i32)) as usize,
41 is_forum: get!(x->13(i32)) as i8 == 1,
42 topics: serde_json::from_str(&get!(x->14(String))).unwrap(),
43 }
44 }
45
46 pub async fn get_community_by_id(&self, id: usize) -> Result<Community> {
47 if id == 0 {
48 return Ok(Community::void());
49 }
50
51 if let Some(cached) = self.0.1.get(format!("atto.community:{}", id)).await {
52 match serde_json::from_str(&cached) {
53 Ok(c) => return Ok(c),
54 Err(_) => self.0.1.remove(format!("atto.community:{}", id)).await,
55 };
56 }
57
58 let conn = match self.0.connect().await {
59 Ok(c) => c,
60 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
61 };
62
63 let res = query_row!(
64 &conn,
65 "SELECT * FROM communities WHERE id = $1",
66 &[&(id as i64)],
67 |x| { Ok(Self::get_community_from_row(x)) }
68 );
69
70 if res.is_err() {
71 return Ok(Community::void());
72 }
74
75 let x = res.unwrap();
76 self.0
77 .1
78 .set(
79 format!("atto.community:{}", id),
80 serde_json::to_string(&x).unwrap(),
81 )
82 .await;
83
84 Ok(x)
85 }
86
87 pub async fn get_community_by_title(&self, id: &str) -> Result<Community> {
88 if id == "void" {
89 return Ok(Community::void());
90 }
91
92 if let Some(cached) = self.0.1.get(format!("atto.community:{}", id)).await {
93 match serde_json::from_str(&cached) {
94 Ok(c) => return Ok(c),
95 Err(_) => self.0.1.remove(format!("atto.community:{}", id)).await,
96 };
97 }
98
99 let conn = match self.0.connect().await {
100 Ok(c) => c,
101 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
102 };
103
104 let res = query_row!(
105 &conn,
106 "SELECT * FROM communities WHERE title = $1",
107 params![&id],
108 |x| { Ok(Self::get_community_from_row(x)) }
109 );
110
111 if res.is_err() {
112 return Ok(Community::void());
113 }
115
116 let x = res.unwrap();
117 self.0
118 .1
119 .set(
120 format!("atto.community:{}", id),
121 serde_json::to_string(&x).unwrap(),
122 )
123 .await;
124
125 Ok(x)
126 }
127
128 auto_method!(get_community_by_id_no_void()@get_community_from_row -> "SELECT * FROM communities WHERE id = $1" --name="community" --returns=Community --cache-key-tmpl="atto.community:{}");
129 auto_method!(get_community_by_title_no_void(&str)@get_community_from_row -> "SELECT * FROM communities WHERE title = $1" --name="community" --returns=Community --cache-key-tmpl="atto.community:{}");
130
131 pub async fn get_popular_communities(&self) -> Result<Vec<Community>> {
133 let conn = match self.0.connect().await {
134 Ok(c) => c,
135 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
136 };
137
138 let res = query_rows!(
139 &conn,
140 "SELECT * FROM communities WHERE NOT context LIKE '%\"is_nsfw\":true%' ORDER BY member_count DESC LIMIT 12",
141 params![],
142 |x| { Self::get_community_from_row(x) }
143 );
144
145 if res.is_err() {
146 return Err(Error::GeneralNotFound("communities".to_string()));
147 }
148
149 Ok(res.unwrap())
150 }
151
152 pub async fn get_communities_searched(
155 &self,
156 query: &str,
157 batch: usize,
158 page: usize,
159 ) -> Result<Vec<Community>> {
160 let conn = match self.0.connect().await {
161 Ok(c) => c,
162 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
163 };
164
165 let res = query_rows!(
166 &conn,
167 "SELECT * FROM communities WHERE title LIKE $1 ORDER BY member_count DESC, created DESC LIMIT $2 OFFSET $3",
168 params![
169 &format!("%{query}%"),
170 &(batch as i64),
171 &((page * batch) as i64)
172 ],
173 |x| { Self::get_community_from_row(x) }
174 );
175
176 if res.is_err() {
177 return Err(Error::GeneralNotFound("communities".to_string()));
178 }
179
180 Ok(res.unwrap())
181 }
182
183 pub async fn get_communities_by_owner(&self, id: usize) -> Result<Vec<Community>> {
185 let conn = match self.0.connect().await {
186 Ok(c) => c,
187 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
188 };
189
190 let res = query_rows!(
191 &conn,
192 "SELECT * FROM communities WHERE owner = $1",
193 params![&(id as i64)],
194 |x| { Self::get_community_from_row(x) }
195 );
196
197 if res.is_err() {
198 return Err(Error::GeneralNotFound("communities".to_string()));
199 }
200
201 Ok(res.unwrap())
202 }
203
204 pub async fn create_community(&self, data: Community) -> Result<String> {
209 if data.title.trim().len() < 2 {
211 return Err(Error::DataTooShort("title".to_string()));
212 } else if data.title.len() > 32 {
213 return Err(Error::DataTooLong("title".to_string()));
214 }
215
216 if self.0.0.banned_usernames.contains(&data.title) {
217 return Err(Error::MiscError("This title cannot be used".to_string()));
218 }
219
220 let regex = regex::RegexBuilder::new(NAME_REGEX)
221 .multi_line(true)
222 .build()
223 .unwrap();
224
225 if regex.captures(&data.title).is_some() {
226 return Err(Error::MiscError(
227 "This title contains invalid characters".to_string(),
228 ));
229 }
230
231 let owner = self.get_user_by_id(data.owner).await?;
233
234 if !owner
235 .permissions
236 .check(FinePermission::INFINITE_COMMUNITIES)
237 {
238 let memberships = self.get_memberships_by_owner(data.owner).await?;
239 let mut admin_count = 0; for membership in memberships {
242 if membership.role.check(CommunityPermission::ADMINISTRATOR) {
243 admin_count += 1;
244 }
245 }
246
247 let maximum_count = if owner.permissions.check(FinePermission::SUPPORTER) {
248 10
249 } else {
250 5
251 };
252
253 if admin_count >= maximum_count {
254 return Err(Error::MiscError(
255 "You are already owner/co-owner of too many communities to create another"
256 .to_string(),
257 ));
258 }
259 }
260
261 if data.is_forge
264 && !owner
265 .secondary_permissions
266 .check(SecondaryPermission::DEVELOPER_PASS)
267 {
268 return Err(Error::RequiresSupporter);
269 }
270
271 if self
273 .get_community_by_title_no_void(&data.title.to_lowercase())
274 .await
275 .is_ok()
276 {
277 return Err(Error::MiscError("Title already in use".to_string()));
278 }
279
280 let conn = match self.0.connect().await {
282 Ok(c) => c,
283 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
284 };
285
286 let res = execute!(
287 &conn,
288 "INSERT INTO communities VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)",
289 params![
290 &(data.id as i64),
291 &(data.created as i64),
292 &data.title.to_lowercase(),
293 &serde_json::to_string(&data.context).unwrap().as_str(),
294 &(data.owner as i64),
295 &serde_json::to_string(&data.read_access).unwrap().as_str(),
296 &serde_json::to_string(&data.write_access).unwrap().as_str(),
297 &serde_json::to_string(&data.join_access).unwrap().as_str(),
298 &0_i32,
299 &0_i32,
300 &1_i32,
301 &{ if data.is_forge { 1 } else { 0 } },
302 &0_i32,
303 &{ if data.is_forum { 1 } else { 0 } },
304 &serde_json::to_string(&data.topics).unwrap().as_str(),
305 ]
306 );
307
308 if let Err(e) = res {
309 return Err(Error::DatabaseError(e.to_string()));
310 }
311
312 self.create_membership(
314 CommunityMembership::new(data.owner, data.id, CommunityPermission::ADMINISTRATOR),
315 &owner,
316 )
317 .await
318 .unwrap();
319
320 Ok(data.title)
322 }
323
324 pub async fn cache_clear_community(&self, community: &Community) {
325 self.0
326 .1
327 .remove(format!("atto.community:{}", community.id))
328 .await;
329 self.0
330 .1
331 .remove(format!("atto.community:{}", community.title))
332 .await;
333 }
334
335 pub async fn delete_community(&self, id: usize, user: &User) -> Result<()> {
336 let y = self.get_community_by_id(id).await?;
337
338 if user.id != y.owner {
339 if !user.permissions.check(FinePermission::MANAGE_COMMUNITIES) {
340 return Err(Error::NotAllowed);
341 } else {
342 self.create_audit_log_entry(crate::model::moderation::AuditLogEntry::new(
343 user.id,
344 format!("invoked `delete_community` with x value `{id}`"),
345 ))
346 .await?
347 }
348 }
349
350 let conn = match self.0.connect().await {
351 Ok(c) => c,
352 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
353 };
354
355 let res = execute!(
356 &conn,
357 "DELETE FROM communities WHERE id = $1",
358 &[&(id as i64)]
359 );
360
361 if let Err(e) = res {
362 return Err(Error::DatabaseError(e.to_string()));
363 }
364
365 self.cache_clear_community(&y).await;
366
367 let res = execute!(
369 &conn,
370 "DELETE FROM memberships WHERE community = $1",
371 &[&(id as i64)]
372 );
373
374 if let Err(e) = res {
375 return Err(Error::DatabaseError(e.to_string()));
376 }
377
378 for channel in self.get_channels_by_community(id).await? {
380 self.delete_channel(channel.id, &user).await?;
381 }
382
383 let avatar = PathBufD::current().extend(&[
385 self.0.0.dirs.media.as_str(),
386 "community_avatars",
387 &format!("{}.avif", &y.id),
388 ]);
389
390 let banner = PathBufD::current().extend(&[
391 self.0.0.dirs.media.as_str(),
392 "community_banners",
393 &format!("{}.avif", &y.id),
394 ]);
395
396 if exists(&avatar).unwrap() {
397 remove_file(avatar).unwrap();
398 }
399
400 if exists(&banner).unwrap() {
401 remove_file(banner).unwrap();
402 }
403
404 Ok(())
406 }
407
408 pub async fn update_community_title(&self, id: usize, user: User, title: &str) -> Result<()> {
409 if title.len() < 2 {
411 return Err(Error::DataTooShort("title".to_string()));
412 } else if title.len() > 32 {
413 return Err(Error::DataTooLong("title".to_string()));
414 }
415
416 if self.0.0.banned_usernames.contains(&title.to_string()) {
417 return Err(Error::MiscError("This title cannot be used".to_string()));
418 }
419
420 let regex = regex::RegexBuilder::new(NAME_REGEX)
421 .multi_line(true)
422 .build()
423 .unwrap();
424
425 if regex.captures(&title).is_some() {
426 return Err(Error::MiscError(
427 "This title contains invalid characters".to_string(),
428 ));
429 }
430
431 let y = self.get_community_by_id(id).await?;
433
434 if user.id != y.owner {
435 if !user.permissions.check(FinePermission::MANAGE_COMMUNITIES) {
436 return Err(Error::NotAllowed);
437 } else {
438 self.create_audit_log_entry(crate::model::moderation::AuditLogEntry::new(
439 user.id,
440 format!("invoked `update_community_title` with x value `{id}`"),
441 ))
442 .await?
443 }
444 }
445
446 let title = &title.to_lowercase();
448 if self.get_community_by_title_no_void(title).await.is_ok() {
449 return Err(Error::TitleInUse);
450 }
451
452 let conn = match self.0.connect().await {
454 Ok(c) => c,
455 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
456 };
457
458 let res = execute!(
459 &conn,
460 "UPDATE communities SET title = $1 WHERE id = $2",
461 params![&title, &(id as i64)]
462 );
463
464 if let Err(e) = res {
465 return Err(Error::DatabaseError(e.to_string()));
466 }
467
468 self.cache_clear_community(&y).await;
469
470 Ok(())
471 }
472
473 pub async fn update_community_owner(
474 &self,
475 id: usize,
476 user: User,
477 new_owner: usize,
478 ) -> Result<()> {
479 let y = self.get_community_by_id(id).await?;
480
481 if user.id != y.owner {
482 if !user.permissions.check(FinePermission::MANAGE_COMMUNITIES) {
483 return Err(Error::NotAllowed);
484 } else {
485 self.create_audit_log_entry(crate::model::moderation::AuditLogEntry::new(
486 user.id,
487 format!("invoked `update_community_owner` with x value `{id}`"),
488 ))
489 .await?
490 }
491 }
492
493 let new_owner_membership = self
494 .get_membership_by_owner_community(new_owner, y.id)
495 .await?;
496 let current_owner_membership = self
497 .get_membership_by_owner_community(y.owner, y.id)
498 .await?;
499
500 let conn = match self.0.connect().await {
502 Ok(c) => c,
503 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
504 };
505
506 let res = execute!(
507 &conn,
508 "UPDATE communities SET owner = $1 WHERE id = $2",
509 params![&(new_owner as i64), &(id as i64)]
510 );
511
512 if let Err(e) = res {
513 return Err(Error::DatabaseError(e.to_string()));
514 }
515
516 self.cache_clear_community(&y).await;
517
518 self.update_membership_role(
520 new_owner_membership.id,
521 CommunityPermission::DEFAULT | CommunityPermission::ADMINISTRATOR,
522 )
523 .await?;
524
525 self.update_membership_role(
526 current_owner_membership.id,
527 CommunityPermission::DEFAULT | CommunityPermission::MEMBER,
528 )
529 .await?;
530
531 Ok(())
533 }
534
535 pub async fn delete_topic_posts(&self, id: usize, topic: usize) -> Result<()> {
536 let conn = match self.0.connect().await {
537 Ok(c) => c,
538 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
539 };
540
541 let res = execute!(
542 &conn,
543 "DELETE FROM posts WHERE community = $1 AND topic = $2",
544 params![&(id as i64), &(topic as i64)]
545 );
546
547 if let Err(e) = res {
548 return Err(Error::DatabaseError(e.to_string()));
549 }
550
551 Ok(())
552 }
553
554 auto_method!(update_community_context(CommunityContext)@get_community_by_id_no_void:FinePermission::MANAGE_COMMUNITIES; -> "UPDATE communities SET context = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_community);
555 auto_method!(update_community_read_access(CommunityReadAccess)@get_community_by_id_no_void:FinePermission::MANAGE_COMMUNITIES; -> "UPDATE communities SET read_access = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_community);
556 auto_method!(update_community_write_access(CommunityWriteAccess)@get_community_by_id_no_void:FinePermission::MANAGE_COMMUNITIES; -> "UPDATE communities SET write_access = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_community);
557 auto_method!(update_community_join_access(CommunityJoinAccess)@get_community_by_id_no_void:FinePermission::MANAGE_COMMUNITIES; -> "UPDATE communities SET join_access = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_community);
558 auto_method!(update_community_topics(HashMap<usize, ForumTopic>)@get_community_by_id_no_void:FinePermission::MANAGE_COMMUNITIES; -> "UPDATE communities SET topics = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_community);
559 auto_method!(update_community_is_forum(i32)@get_community_by_id_no_void:FinePermission::MANAGE_COMMUNITIES; -> "UPDATE communities SET is_forum = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_community);
560
561 auto_method!(incr_community_likes()@get_community_by_id_no_void -> "UPDATE communities SET likes = likes + 1 WHERE id = $1" --cache-key-tmpl=cache_clear_community --incr);
562 auto_method!(incr_community_dislikes()@get_community_by_id_no_void -> "UPDATE communities SET dislikes = dislikes + 1 WHERE id = $1" --cache-key-tmpl=cache_clear_community --incr);
563 auto_method!(decr_community_likes()@get_community_by_id_no_void -> "UPDATE communities SET likes = likes - 1 WHERE id = $1" --cache-key-tmpl=cache_clear_community --decr=likes);
564 auto_method!(decr_community_dislikes()@get_community_by_id_no_void -> "UPDATE communities SET dislikes = dislikes - 1 WHERE id = $1" --cache-key-tmpl=cache_clear_community --decr=dislikes);
565
566 auto_method!(incr_community_member_count()@get_community_by_id_no_void -> "UPDATE communities SET member_count = member_count + 1 WHERE id = $1" --cache-key-tmpl=cache_clear_community --incr);
567 auto_method!(decr_community_member_count()@get_community_by_id_no_void -> "UPDATE communities SET member_count = member_count - 1 WHERE id = $1" --cache-key-tmpl=cache_clear_community --decr=member_count);
568
569 auto_method!(incr_community_post_count()@get_community_by_id_no_void -> "UPDATE communities SET post_count = post_count + 1 WHERE id = $1" --cache-key-tmpl=cache_clear_community --incr);
570 auto_method!(decr_community_post_count()@get_community_by_id_no_void -> "UPDATE communities SET post_count = post_count - 1 WHERE id = $1" --cache-key-tmpl=cache_clear_community --decr=post_count);
571}