tetratto_core/database/
journals.rs1use oiseau::{cache::Cache, query_row};
2use crate::{
3 database::common::NAME_REGEX,
4 model::{
5 auth::User,
6 journals::{Journal, JournalPrivacyPermission},
7 permissions::FinePermission,
8 Error, Result,
9 },
10};
11use crate::{auto_method, DataManager};
12use oiseau::{PostgresRow, execute, get, query_rows, params};
13
14impl DataManager {
15 pub(crate) fn get_journal_from_row(x: &PostgresRow) -> Journal {
17 Journal {
18 id: get!(x->0(i64)) as usize,
19 created: get!(x->1(i64)) as usize,
20 owner: get!(x->2(i64)) as usize,
21 title: get!(x->3(String)),
22 privacy: serde_json::from_str(&get!(x->4(String))).unwrap(),
23 dirs: serde_json::from_str(&get!(x->5(String))).unwrap(),
24 }
25 }
26
27 auto_method!(get_journal_by_id(usize as i64)@get_journal_from_row -> "SELECT * FROM journals WHERE id = $1" --name="journal" --returns=Journal --cache-key-tmpl="atto.journal:{}");
28
29 pub async fn get_journal_by_owner_title(&self, owner: usize, title: &str) -> Result<Journal> {
31 let conn = match self.0.connect().await {
32 Ok(c) => c,
33 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
34 };
35
36 let res = query_row!(
37 &conn,
38 "SELECT * FROM journals WHERE owner = $1 AND title = $2",
39 params![&(owner as i64), &title],
40 |x| { Ok(Self::get_journal_from_row(x)) }
41 );
42
43 if res.is_err() {
44 return Err(Error::GeneralNotFound("journal".to_string()));
45 }
46
47 Ok(res.unwrap())
48 }
49
50 pub async fn get_journals_by_user(&self, id: usize) -> Result<Vec<Journal>> {
55 let conn = match self.0.connect().await {
56 Ok(c) => c,
57 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
58 };
59
60 let res = query_rows!(
61 &conn,
62 "SELECT * FROM journals WHERE owner = $1 ORDER BY title ASC",
63 &[&(id as i64)],
64 |x| { Self::get_journal_from_row(x) }
65 );
66
67 if res.is_err() {
68 return Err(Error::GeneralNotFound("journal".to_string()));
69 }
70
71 Ok(res.unwrap())
72 }
73
74 const MAXIMUM_FREE_JOURNALS: usize = 5;
75
76 pub async fn create_journal(&self, mut data: Journal) -> Result<Journal> {
81 if data.title.len() < 2 {
83 return Err(Error::DataTooShort("title".to_string()));
84 } else if data.title.len() > 32 {
85 return Err(Error::DataTooLong("title".to_string()));
86 }
87
88 data.title = data.title.replace(" ", "_").to_lowercase();
89
90 let regex = regex::RegexBuilder::new(NAME_REGEX)
92 .multi_line(true)
93 .build()
94 .unwrap();
95
96 if regex.captures(&data.title).is_some() {
97 return Err(Error::MiscError(
98 "This title contains invalid characters".to_string(),
99 ));
100 }
101
102 if self
104 .get_journal_by_owner_title(data.owner, &data.title)
105 .await
106 .is_ok()
107 {
108 return Err(Error::TitleInUse);
109 }
110
111 let owner = self.get_user_by_id(data.owner).await?;
113
114 if !owner.permissions.check(FinePermission::SUPPORTER) {
115 let journals = self.get_journals_by_user(data.owner).await?;
116
117 if journals.len() >= Self::MAXIMUM_FREE_JOURNALS {
118 return Err(Error::MiscError(
119 "You already have the maximum number of journals you can have".to_string(),
120 ));
121 }
122 }
123
124 let conn = match self.0.connect().await {
126 Ok(c) => c,
127 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
128 };
129
130 let res = execute!(
131 &conn,
132 "INSERT INTO journals VALUES ($1, $2, $3, $4, $5, $6)",
133 params![
134 &(data.id as i64),
135 &(data.created as i64),
136 &(data.owner as i64),
137 &data.title,
138 &serde_json::to_string(&data.privacy).unwrap(),
139 &serde_json::to_string(&data.dirs).unwrap(),
140 ]
141 );
142
143 if let Err(e) = res {
144 return Err(Error::DatabaseError(e.to_string()));
145 }
146
147 Ok(data)
148 }
149
150 pub async fn delete_journal(&self, id: usize, user: &User) -> Result<()> {
151 let journal = self.get_journal_by_id(id).await?;
152
153 if user.id != journal.owner && !user.permissions.check(FinePermission::MANAGE_JOURNALS) {
155 return Err(Error::NotAllowed);
156 }
157
158 let conn = match self.0.connect().await {
160 Ok(c) => c,
161 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
162 };
163
164 let res = execute!(&conn, "DELETE FROM journals WHERE id = $1", &[&(id as i64)]);
165
166 if let Err(e) = res {
167 return Err(Error::DatabaseError(e.to_string()));
168 }
169
170 let res = execute!(
172 &conn,
173 "DELETE FROM notes WHERE journal = $1",
174 &[&(id as i64)]
175 );
176
177 if let Err(e) = res {
178 return Err(Error::DatabaseError(e.to_string()));
179 }
180
181 self.0.1.remove(format!("atto.journal:{}", id)).await;
183 Ok(())
184 }
185
186 auto_method!(update_journal_title(&str)@get_journal_by_id:FinePermission::MANAGE_JOURNALS; -> "UPDATE journals SET title = $1 WHERE id = $2" --cache-key-tmpl="atto.journal:{}");
187 auto_method!(update_journal_privacy(JournalPrivacyPermission)@get_journal_by_id:FinePermission::MANAGE_JOURNALS; -> "UPDATE journals SET privacy = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.journal:{}");
188 auto_method!(update_journal_dirs(Vec<(usize, usize, String)>)@get_journal_by_id:FinePermission::MANAGE_JOURNALS; -> "UPDATE journals SET dirs = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.journal:{}");
189}