1use oiseau::cache::Cache;
2use crate::database::common::NAME_REGEX;
3use crate::model::{auth::User, journals::Note, permissions::FinePermission, Error, Result};
4use crate::{auto_method, DataManager};
5use oiseau::{execute, get, params, query_row, query_rows, PostgresRow};
6
7impl DataManager {
8 pub(crate) fn get_note_from_row(x: &PostgresRow) -> Note {
10 Note {
11 id: get!(x->0(i64)) as usize,
12 created: get!(x->1(i64)) as usize,
13 owner: get!(x->2(i64)) as usize,
14 title: get!(x->3(String)),
15 journal: get!(x->4(i64)) as usize,
16 content: get!(x->5(String)),
17 edited: get!(x->6(i64)) as usize,
18 dir: get!(x->7(i64)) as usize,
19 tags: serde_json::from_str(&get!(x->8(String))).unwrap(),
20 is_global: get!(x->9(i32)) as i8 == 1,
21 }
22 }
23
24 auto_method!(get_note_by_id(usize as i64)@get_note_from_row -> "SELECT * FROM notes WHERE id = $1" --name="note" --returns=Note --cache-key-tmpl="atto.note:{}");
25 auto_method!(get_global_note_by_title(&str)@get_note_from_row -> "SELECT * FROM notes WHERE title = $1 AND is_global = 1" --name="note" --returns=Note --cache-key-tmpl="atto.note:{}");
26
27 pub async fn get_user_global_notes_count(&self, owner: usize) -> Result<i32> {
29 let conn = match self.0.connect().await {
30 Ok(c) => c,
31 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
32 };
33
34 let res = query_row!(
35 &conn,
36 "SELECT COUNT(*)::int FROM notes WHERE owner = $1 AND is_global = 1",
37 &[&(owner as i64)],
38 |x| Ok(x.get::<usize, i32>(0))
39 );
40
41 if let Err(e) = res {
42 return Err(Error::DatabaseError(e.to_string()));
43 }
44
45 Ok(res.unwrap())
46 }
47
48 pub async fn get_note_by_journal_title(&self, journal: usize, title: &str) -> Result<Note> {
50 let conn = match self.0.connect().await {
51 Ok(c) => c,
52 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
53 };
54
55 let res = query_row!(
56 &conn,
57 "SELECT * FROM notes WHERE journal = $1 AND title = $2",
58 params![&(journal as i64), &title],
59 |x| { Ok(Self::get_note_from_row(x)) }
60 );
61
62 if res.is_err() {
63 return Err(Error::GeneralNotFound("note".to_string()));
64 }
65
66 Ok(res.unwrap())
67 }
68
69 pub async fn get_notes_by_journal(&self, id: usize) -> Result<Vec<Note>> {
74 let conn = match self.0.connect().await {
75 Ok(c) => c,
76 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
77 };
78
79 let res = query_rows!(
80 &conn,
81 "SELECT * FROM notes WHERE journal = $1 ORDER BY edited DESC",
82 &[&(id as i64)],
83 |x| { Self::get_note_from_row(x) }
84 );
85
86 if res.is_err() {
87 return Err(Error::GeneralNotFound("note".to_string()));
88 }
89
90 Ok(res.unwrap())
91 }
92
93 pub async fn get_notes_by_journal_tag(&self, id: usize, tag: &str) -> Result<Vec<Note>> {
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_rows!(
105 &conn,
106 "SELECT * FROM notes WHERE journal = $1 AND tags::jsonb ? $2 ORDER BY edited DESC",
107 params![&(id as i64), tag],
108 |x| { Self::get_note_from_row(x) }
109 );
110
111 if res.is_err() {
112 return Err(Error::GeneralNotFound("note".to_string()));
113 }
114
115 Ok(res.unwrap())
116 }
117
118 const MAXIMUM_FREE_NOTES_PER_JOURNAL: usize = 10;
119
120 pub const MAXIMUM_FREE_GLOBAL_NOTES: usize = 10;
121 pub const MAXIMUM_SUPPORTER_GLOBAL_NOTES: usize = 50;
122
123 pub async fn create_note(&self, mut data: Note) -> Result<Note> {
128 if data.title.len() < 2 {
130 return Err(Error::DataTooShort("title".to_string()));
131 } else if data.title.len() > 64 {
132 return Err(Error::DataTooLong("title".to_string()));
133 }
134
135 if data.content.len() < 2 {
136 return Err(Error::DataTooShort("content".to_string()));
137 } else if data.content.len() > 262144 {
138 return Err(Error::DataTooLong("content".to_string()));
139 }
140
141 data.title = data.title.replace(" ", "_").to_lowercase();
142
143 let owner = self.get_user_by_id(data.owner).await?;
145
146 if !owner.permissions.check(FinePermission::SUPPORTER) {
147 let journals = self.get_notes_by_journal(data.owner).await?;
148
149 if journals.len() >= Self::MAXIMUM_FREE_NOTES_PER_JOURNAL {
150 return Err(Error::MiscError(
151 "You already have the maximum number of notes you can have in this journal"
152 .to_string(),
153 ));
154 }
155 }
156
157 let regex = regex::RegexBuilder::new(NAME_REGEX)
159 .multi_line(true)
160 .build()
161 .unwrap();
162
163 if regex.captures(&data.title).is_some() {
164 return Err(Error::MiscError(
165 "This title contains invalid characters".to_string(),
166 ));
167 }
168
169 if self
171 .get_note_by_journal_title(data.journal, &data.title)
172 .await
173 .is_ok()
174 {
175 return Err(Error::TitleInUse);
176 }
177
178 let journal = self.get_journal_by_id(data.journal).await?;
180
181 if data.owner != journal.owner {
182 return Err(Error::NotAllowed);
183 }
184
185 let conn = match self.0.connect().await {
187 Ok(c) => c,
188 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
189 };
190
191 let res = execute!(
192 &conn,
193 "INSERT INTO notes VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)",
194 params![
195 &(data.id as i64),
196 &(data.created as i64),
197 &(data.owner as i64),
198 &data.title,
199 &(data.journal as i64),
200 &data.content,
201 &(data.edited as i64),
202 &(data.dir as i64),
203 &serde_json::to_string(&data.tags).unwrap(),
204 &if data.is_global { 1 } else { 0 }
205 ]
206 );
207
208 if let Err(e) = res {
209 return Err(Error::DatabaseError(e.to_string()));
210 }
211
212 Ok(data)
213 }
214
215 pub async fn delete_note(&self, id: usize, user: &User) -> Result<()> {
216 let note = self.get_note_by_id(id).await?;
217
218 if user.id != note.owner && !user.permissions.check(FinePermission::MANAGE_NOTES) {
220 return Err(Error::NotAllowed);
221 }
222
223 let conn = match self.0.connect().await {
225 Ok(c) => c,
226 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
227 };
228
229 let res = execute!(&conn, "DELETE FROM notes WHERE id = $1", &[&(id as i64)]);
230
231 if let Err(e) = res {
232 return Err(Error::DatabaseError(e.to_string()));
233 }
234
235 self.cache_clear_note(¬e).await;
237 Ok(())
238 }
239
240 pub async fn delete_notes_by_journal_dir(
246 &self,
247 journal: usize,
248 dir: usize,
249 user: &User,
250 ) -> Result<()> {
251 let journal = self.get_journal_by_id(journal).await?;
252
253 if journal.owner != user.id && !user.permissions.check(FinePermission::MANAGE_NOTES) {
254 return Err(Error::NotAllowed);
255 }
256
257 let conn = match self.0.connect().await {
259 Ok(c) => c,
260 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
261 };
262
263 let res = execute!(
264 &conn,
265 "DELETE FROM notes WHERE dir = $1 AND journal = $2 ORDER BY edited DESC",
266 &[&(dir as i64), &(journal.id as i64)]
267 );
268
269 if let Err(e) = res {
270 return Err(Error::DatabaseError(e.to_string()));
271 }
272
273 Ok(())
274 }
275
276 pub async fn incr_note_views(&self, id: usize) {
280 self.0.1.incr(format!("atto.note:{id}/views")).await;
281 }
282
283 pub async fn get_note_views(&self, id: usize) -> Option<String> {
284 self.0.1.get(format!("atto.note:{id}/views")).await
285 }
286
287 pub async fn cache_clear_note(&self, x: &Note) {
288 self.0.1.remove(format!("atto.note:{}", x.id)).await;
289 self.0.1.remove(format!("atto.note:{}", x.title)).await;
290 }
291
292 auto_method!(update_note_title(&str)@get_note_by_id:FinePermission::MANAGE_NOTES; -> "UPDATE notes SET title = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_note);
293 auto_method!(update_note_content(&str)@get_note_by_id:FinePermission::MANAGE_NOTES; -> "UPDATE notes SET content = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_note);
294 auto_method!(update_note_dir(i64)@get_note_by_id:FinePermission::MANAGE_NOTES; -> "UPDATE notes SET dir = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_note);
295 auto_method!(update_note_tags(Vec<String>)@get_note_by_id:FinePermission::MANAGE_NOTES; -> "UPDATE notes SET tags = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_note);
296 auto_method!(update_note_edited(i64)@get_note_by_id -> "UPDATE notes SET edited = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_note);
297 auto_method!(update_note_is_global(i32)@get_note_by_id -> "UPDATE notes SET is_global = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_note);
298}