1use oiseau::cache::Cache;
2use crate::model::{
3 addr::RemoteAddr,
4 auth::{AchievementName, Notification, User},
5 permissions::FinePermission,
6 reactions::{AssetType, Reaction},
7 Error, Result,
8};
9use crate::{auto_method, DataManager};
10use oiseau::{PostgresRow, execute, get, query_row, query_rows, params};
11
12impl DataManager {
13 pub(crate) fn get_reaction_from_row(x: &PostgresRow) -> Reaction {
15 Reaction {
16 id: get!(x->0(i64)) as usize,
17 created: get!(x->1(i64)) as usize,
18 owner: get!(x->2(i64)) as usize,
19 asset: get!(x->3(i64)) as usize,
20 asset_type: serde_json::from_str(&get!(x->4(String))).unwrap(),
21 is_like: get!(x->5(i32)) as i8 == 1,
22 }
23 }
24
25 auto_method!(get_reaction_by_id()@get_reaction_from_row -> "SELECT * FROM reactions WHERE id = $1" --name="reaction" --returns=Reaction --cache-key-tmpl="atto.reaction:{}");
26
27 pub async fn fill_reactions(
29 &self,
30 reactions: &Vec<Reaction>,
31 ignore_users: Vec<usize>,
32 ) -> Result<Vec<(Reaction, User)>> {
33 let mut out = Vec::new();
34
35 for reaction in reactions {
36 if ignore_users.contains(&reaction.owner) {
37 continue;
38 }
39
40 out.push((
41 reaction.to_owned(),
42 self.get_user_by_id(reaction.owner.to_owned()).await?,
43 ));
44 }
45
46 Ok(out)
47 }
48
49 pub async fn get_reactions_by_asset(
51 &self,
52 asset: usize,
53 batch: usize,
54 page: usize,
55 ) -> Result<Vec<Reaction>> {
56 let conn = match self.0.connect().await {
57 Ok(c) => c,
58 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
59 };
60
61 let res = query_rows!(
62 &conn,
63 "SELECT * FROM reactions WHERE asset = $1 ORDER BY created DESC LIMIT $2 OFFSET $3",
64 &[&(asset as i64), &(batch as i64), &((page * batch) as i64)],
65 |x| { Self::get_reaction_from_row(x) }
66 );
67
68 if res.is_err() {
69 return Err(Error::GeneralNotFound("reaction".to_string()));
70 }
71
72 Ok(res.unwrap())
73 }
74
75 pub async fn get_likes_reactions_by_asset(
77 &self,
78 asset: usize,
79 batch: usize,
80 page: usize,
81 ) -> Result<Vec<Reaction>> {
82 let conn = match self.0.connect().await {
83 Ok(c) => c,
84 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
85 };
86
87 let res = query_rows!(
88 &conn,
89 "SELECT * FROM reactions WHERE asset = $1 AND is_like = 1 ORDER BY created DESC LIMIT $2 OFFSET $3",
90 &[&(asset as i64), &(batch as i64), &((page * batch) as i64)],
91 |x| { Self::get_reaction_from_row(x) }
92 );
93
94 if res.is_err() {
95 return Err(Error::GeneralNotFound("reaction".to_string()));
96 }
97
98 Ok(res.unwrap())
99 }
100
101 pub async fn get_reaction_by_owner_asset(
103 &self,
104 owner: usize,
105 asset: usize,
106 ) -> Result<Reaction> {
107 let conn = match self.0.connect().await {
108 Ok(c) => c,
109 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
110 };
111
112 let res = query_row!(
113 &conn,
114 "SELECT * FROM reactions WHERE owner = $1 AND asset = $2",
115 &[&(owner as i64), &(asset as i64)],
116 |x| { Ok(Self::get_reaction_from_row(x)) }
117 );
118
119 if res.is_err() {
120 return Err(Error::GeneralNotFound("reaction".to_string()));
121 }
122
123 Ok(res.unwrap())
124 }
125
126 pub async fn create_reaction(
131 &self,
132 data: Reaction,
133 user: &User,
134 addr: &RemoteAddr,
135 ) -> Result<()> {
136 let conn = match self.0.connect().await {
137 Ok(c) => c,
138 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
139 };
140
141 if data.asset_type == AssetType::Post {
142 let post = self.get_post_by_id(data.asset).await?;
143
144 if (self
145 .get_userblock_by_initiator_receiver(post.owner, user.id)
146 .await
147 .is_ok()
148 | (self
149 .get_user_stack_blocked_users(post.owner)
150 .await
151 .contains(&user.id)
152 | self
153 .get_ipblock_by_initiator_receiver(post.owner, &addr)
154 .await
155 .is_ok()))
156 && !user.permissions.check(FinePermission::MANAGE_POSTS)
157 {
158 return Err(Error::NotAllowed);
159 }
160
161 if user.id != post.owner {
163 let mut owner = self.get_user_by_id(post.owner).await?;
164 self.add_achievement(&mut owner, AchievementName::Get1Like.into(), true)
165 .await?;
166
167 if post.likes >= 9 {
168 self.add_achievement(&mut owner, AchievementName::Get10Likes.into(), true)
169 .await?;
170 }
171
172 if post.likes >= 49 {
173 self.add_achievement(&mut owner, AchievementName::Get50Likes.into(), true)
174 .await?;
175 }
176
177 if post.likes >= 99 {
178 self.add_achievement(&mut owner, AchievementName::Get100Likes.into(), true)
179 .await?;
180 }
181
182 if post.dislikes >= 24 {
183 self.add_achievement(&mut owner, AchievementName::Get25Dislikes.into(), true)
184 .await?;
185 }
186 }
187 } else if data.asset_type == AssetType::Question {
188 let question = self.get_question_by_id(data.asset).await?;
189
190 if (self
191 .get_userblock_by_initiator_receiver(question.owner, user.id)
192 .await
193 .is_ok()
194 | self
195 .get_user_stack_blocked_users(question.owner)
196 .await
197 .contains(&user.id))
198 && !user.permissions.check(FinePermission::MANAGE_POSTS)
199 {
200 return Err(Error::NotAllowed);
201 }
202 }
203
204 let res = execute!(
206 &conn,
207 "INSERT INTO reactions VALUES ($1, $2, $3, $4, $5, $6)",
208 params![
209 &(data.id as i64),
210 &(data.created as i64),
211 &(data.owner as i64),
212 &(data.asset as i64),
213 &serde_json::to_string(&data.asset_type).unwrap().as_str(),
214 &if data.is_like { 1 } else { 0 }
215 ]
216 );
217
218 if let Err(e) = res {
219 return Err(Error::DatabaseError(e.to_string()));
220 }
221
222 match data.asset_type {
224 AssetType::Community => {
225 if let Err(e) = {
226 if data.is_like {
227 self.incr_community_likes(data.asset).await
228 } else {
229 self.incr_community_dislikes(data.asset).await
230 }
231 } {
232 return Err(e);
233 } else if data.is_like {
234 let community = self.get_community_by_id_no_void(data.asset).await.unwrap();
235
236 if community.owner != user.id {
237 self
238 .create_notification(Notification::new(
239 "Your community has received a like!".to_string(),
240 format!(
241 "[@{}](/api/v1/auth/user/find/{}) has liked your [community](/api/v1/communities/find/{})!",
242 user.username, user.id, community.id
243 ),
244 community.owner,
245 ))
246 .await?
247 }
248 }
249 }
250 AssetType::Post => {
251 if let Err(e) = {
252 if data.is_like {
253 self.incr_post_likes(data.asset).await
254 } else {
255 self.incr_post_dislikes(data.asset).await
256 }
257 } {
258 return Err(e);
259 } else if data.is_like {
260 let post = self.get_post_by_id(data.asset).await.unwrap();
261
262 if post.owner != user.id {
263 self.create_notification(Notification::new(
264 "Your post has received a like!".to_string(),
265 format!(
266 "[@{}](/api/v1/auth/user/find/{}) has liked your [post](/post/{})!",
267 user.username, user.id, data.asset
268 ),
269 post.owner,
270 ))
271 .await?
272 }
273 }
274 }
275 AssetType::Question => {
276 if let Err(e) = {
277 if data.is_like {
278 self.incr_question_likes(data.asset).await
279 } else {
280 self.incr_question_dislikes(data.asset).await
281 }
282 } {
283 return Err(e);
284 } else if data.is_like {
285 let question = self.get_question_by_id(data.asset).await.unwrap();
286
287 if question.owner != user.id {
288 self
289 .create_notification(Notification::new(
290 "Your question has received a like!".to_string(),
291 format!(
292 "[@{}](/api/v1/auth/user/find/{}) has liked your [question](/question/{})!",
293 user.username, user.id, data.asset
294 ),
295 question.owner,
296 ))
297 .await?
298 }
299 }
300 }
301 AssetType::User => {
302 return Err(Error::NotAllowed);
303 }
304 AssetType::Letter => {
305 if let Err(e) = {
306 if data.is_like {
307 self.incr_letter_likes(data.asset).await
308 } else {
309 self.incr_letter_dislikes(data.asset).await
310 }
311 } {
312 return Err(e);
313 } else if data.is_like {
314 let letter = self.get_letter_by_id(data.asset).await.unwrap();
315
316 if letter.owner != user.id {
317 self.create_notification(Notification::new(
318 "Your letter has received a like!".to_string(),
319 format!(
320 "[@{}](/api/v1/auth/user/find/{}) has liked your [letter](/mail/letter/{})!",
321 user.username, user.id, data.asset
322 ),
323 letter.owner,
324 ))
325 .await?
326 }
327 }
328 }
329 };
330
331 Ok(())
333 }
334
335 pub async fn delete_reaction(&self, id: usize, user: &User) -> Result<()> {
336 let reaction = self.get_reaction_by_id(id).await?;
337
338 if user.id != reaction.owner && !user.permissions.check(FinePermission::MANAGE_REACTIONS) {
339 return Err(Error::NotAllowed);
340 }
341
342 let conn = match self.0.connect().await {
343 Ok(c) => c,
344 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
345 };
346
347 let res = execute!(
348 &conn,
349 "DELETE FROM reactions WHERE id = $1",
350 &[&(id as i64)]
351 );
352
353 if let Err(e) = res {
354 return Err(Error::DatabaseError(e.to_string()));
355 }
356
357 self.0.1.remove(format!("atto.reaction:{}", id)).await;
358
359 match reaction.asset_type {
361 AssetType::Community => {
362 if reaction.is_like {
363 self.decr_community_likes(reaction.asset).await
364 } else {
365 self.decr_community_dislikes(reaction.asset).await
366 }
367 }?,
368 AssetType::Post => {
369 if reaction.is_like {
370 self.decr_post_likes(reaction.asset).await
371 } else {
372 self.decr_post_dislikes(reaction.asset).await
373 }
374 }?,
375 AssetType::Question => {
376 if reaction.is_like {
377 self.decr_question_likes(reaction.asset).await
378 } else {
379 self.decr_question_dislikes(reaction.asset).await
380 }
381 }?,
382 AssetType::User => {
383 return Err(Error::NotAllowed);
384 }
385 AssetType::Letter => {
386 if reaction.is_like {
387 self.decr_letter_likes(reaction.asset).await
388 } else {
389 self.decr_letter_dislikes(reaction.asset).await
390 }
391 }?,
392 };
393
394 Ok(())
396 }
397}