1use oiseau::cache::Cache;
2use crate::model::{
3 Error, Result,
4 addr::RemoteAddr,
5 auth::{AchievementName, Notification, User},
6 id::Id,
7 permissions::FinePermission,
8 reactions::{AssetType, Reaction},
9};
10use crate::{auto_method, DataManager};
11use oiseau::{PostgresRow, execute, get, query_row, query_rows, params};
12
13impl DataManager {
14 pub(crate) fn get_reaction_from_row(x: &PostgresRow) -> Reaction {
16 Reaction {
17 id: Id::deserialize(&get!(x->0(String))),
18 created: get!(x->1(i64)) as u128,
19 owner: Id::deserialize(&get!(x->2(String))),
20 asset: Id::deserialize(&get!(x->3(String))),
21 asset_type: serde_json::from_str(&get!(x->4(String))).unwrap(),
22 is_like: get!(x->5(i32)) as i8 == 1,
23 }
24 }
25
26 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:{}");
27
28 pub async fn fill_reactions(
30 &self,
31 reactions: &Vec<Reaction>,
32 ignore_users: Vec<Id>,
33 ) -> Result<Vec<(Reaction, User)>> {
34 let mut out = Vec::new();
35
36 for reaction in reactions {
37 if ignore_users.contains(&reaction.owner) {
38 continue;
39 }
40
41 out.push((
42 reaction.to_owned(),
43 self.get_user_by_id(&reaction.owner).await?,
44 ));
45 }
46
47 Ok(out)
48 }
49
50 pub async fn get_reactions_by_asset(
52 &self,
53 asset: usize,
54 batch: usize,
55 page: usize,
56 ) -> Result<Vec<Reaction>> {
57 let conn = match self.0.connect().await {
58 Ok(c) => c,
59 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
60 };
61
62 let res = query_rows!(
63 &conn,
64 "SELECT * FROM reactions WHERE asset = $1 ORDER BY created DESC LIMIT $2 OFFSET $3",
65 &[&(asset as i64), &(batch as i64), &((page * batch) as i64)],
66 |x| { Self::get_reaction_from_row(x) }
67 );
68
69 if res.is_err() {
70 return Err(Error::GeneralNotFound("reaction".to_string()));
71 }
72
73 Ok(res.unwrap())
74 }
75
76 pub async fn get_likes_reactions_by_asset(
78 &self,
79 asset: usize,
80 batch: usize,
81 page: usize,
82 ) -> Result<Vec<Reaction>> {
83 let conn = match self.0.connect().await {
84 Ok(c) => c,
85 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
86 };
87
88 let res = query_rows!(
89 &conn,
90 "SELECT * FROM reactions WHERE asset = $1 AND is_like = 1 ORDER BY created DESC LIMIT $2 OFFSET $3",
91 &[&(asset as i64), &(batch as i64), &((page * batch) as i64)],
92 |x| { Self::get_reaction_from_row(x) }
93 );
94
95 if res.is_err() {
96 return Err(Error::GeneralNotFound("reaction".to_string()));
97 }
98
99 Ok(res.unwrap())
100 }
101
102 pub async fn get_reaction_by_owner_asset(&self, owner: &Id, asset: &Id) -> Result<Reaction> {
104 let conn = match self.0.connect().await {
105 Ok(c) => c,
106 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
107 };
108
109 let res = query_row!(
110 &conn,
111 "SELECT * FROM reactions WHERE owner = $1 AND asset = $2",
112 &[&owner.printable(), &asset.printable()],
113 |x| { Ok(Self::get_reaction_from_row(x)) }
114 );
115
116 if res.is_err() {
117 return Err(Error::GeneralNotFound("reaction".to_string()));
118 }
119
120 Ok(res.unwrap())
121 }
122
123 pub async fn create_reaction(
128 &self,
129 data: Reaction,
130 user: &User,
131 addr: &RemoteAddr,
132 ) -> Result<()> {
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 if data.asset_type == AssetType::Post {
139 let post = self.get_post_by_id(&data.asset).await?;
140
141 if (self
142 .get_user_block_by_initiator_receiver(&post.owner, &user.id)
143 .await
144 .is_ok()
145 | self
146 .get_ip_block_by_initiator_receiver(&post.owner, addr)
147 .await
148 .is_ok())
149 && !user.permissions.check(FinePermission::ManagePosts)
150 {
151 return Err(Error::NotAllowed);
152 }
153
154 if user.id != post.owner {
156 let mut owner = self.get_user_by_id(&post.owner).await?;
157 self.add_achievement(&mut owner, AchievementName::Get1Like.into(), true)
158 .await?;
159
160 if post.likes >= 9 {
161 self.add_achievement(&mut owner, AchievementName::Get10Likes.into(), true)
162 .await?;
163 }
164
165 if post.likes >= 49 {
166 self.add_achievement(&mut owner, AchievementName::Get50Likes.into(), true)
167 .await?;
168 }
169
170 if post.likes >= 99 {
171 self.add_achievement(&mut owner, AchievementName::Get100Likes.into(), true)
172 .await?;
173 }
174
175 if post.dislikes >= 24 {
176 self.add_achievement(&mut owner, AchievementName::Get25Dislikes.into(), true)
177 .await?;
178 }
179 }
180 } else if data.asset_type == AssetType::Question {
181 let question = self.get_question_by_id(&data.asset).await?;
182
183 if self
184 .get_user_block_by_initiator_receiver(&question.owner, &user.id)
185 .await
186 .is_ok()
187 && !user.permissions.check(FinePermission::ManagePosts)
188 {
189 return Err(Error::NotAllowed);
190 }
191 }
192
193 let res = execute!(
195 &conn,
196 "INSERT INTO reactions VALUES ($1, $2, $3, $4, $5, $6)",
197 params![
198 &data.id.printable(),
199 &(data.created as i64),
200 &data.owner.printable(),
201 &data.asset.printable(),
202 &serde_json::to_string(&data.asset_type).unwrap().as_str(),
203 &if data.is_like { 1 } else { 0 }
204 ]
205 );
206
207 if let Err(e) = res {
208 return Err(Error::DatabaseError(e.to_string()));
209 }
210
211 match data.asset_type {
213 AssetType::Post => {
214 if let Err(e) = {
215 if data.is_like {
216 self.incr_post_likes(&data.asset).await
217 } else {
218 self.incr_post_dislikes(&data.asset).await
219 }
220 } {
221 return Err(e);
222 } else if data.is_like {
223 let post = self.get_post_by_id(&data.asset).await.unwrap();
224
225 if post.owner != user.id {
226 self.create_notification(Notification::new(
227 "Your post has received a like!".to_string(),
228 format!(
229 "[@{}](/api/v2/users/find/{}) has liked your [post](/posts/{})!",
230 user.username, user.id, data.asset
231 ),
232 post.owner,
233 ))
234 .await?
235 }
236 }
237 }
238 AssetType::Question => {
239 if let Err(e) = {
240 if data.is_like {
241 self.incr_question_likes(&data.asset).await
242 } else {
243 self.incr_question_dislikes(&data.asset).await
244 }
245 } {
246 return Err(e);
247 } else if data.is_like {
248 let question = self.get_question_by_id(&data.asset).await.unwrap();
249
250 if question.owner != user.id {
251 self
252 .create_notification(Notification::new(
253 "Your question has received a like!".to_string(),
254 format!(
255 "[@{}](/api/v2/users/find/{}) has liked your [question](/questions/{})!",
256 user.username, user.id, data.asset
257 ),
258 question.owner,
259 ))
260 .await?
261 }
262 }
263 }
264 AssetType::Group => {
265 return Err(Error::NotAllowed);
266 }
267 AssetType::User => {
268 return Err(Error::NotAllowed);
269 }
270 };
271
272 Ok(())
274 }
275
276 pub async fn delete_reaction(&self, id: &Id, user: &User) -> Result<()> {
277 let reaction = self.get_reaction_by_id(id).await?;
278
279 if user.id != reaction.owner && !user.permissions.check(FinePermission::ManageReactions) {
280 return Err(Error::NotAllowed);
281 }
282
283 let conn = match self.0.connect().await {
284 Ok(c) => c,
285 Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
286 };
287
288 let res = execute!(
289 &conn,
290 "DELETE FROM reactions WHERE id = $1",
291 &[&id.printable()]
292 );
293
294 if let Err(e) = res {
295 return Err(Error::DatabaseError(e.to_string()));
296 }
297
298 self.0.1.remove(format!("atto.reaction:{}", id)).await;
299
300 match reaction.asset_type {
302 AssetType::Post => {
303 if reaction.is_like {
304 self.decr_post_likes(&reaction.asset).await
305 } else {
306 self.decr_post_dislikes(&reaction.asset).await
307 }
308 }?,
309 AssetType::Question => {
310 if reaction.is_like {
311 self.decr_question_likes(&reaction.asset).await
312 } else {
313 self.decr_question_dislikes(&reaction.asset).await
314 }
315 }?,
316 AssetType::Group => {
317 return Err(Error::NotAllowed);
318 }
319 AssetType::User => {
320 return Err(Error::NotAllowed);
321 }
322 };
323
324 Ok(())
326 }
327}