1use crate::db::kv::SortOrder;
2use crate::db::{get_neo4j_graph, queries, RedisOps};
3use crate::types::DynError;
4use crate::types::Pagination;
5use chrono::Utc;
6use neo4rs::Row;
7use serde::{Deserialize, Serialize};
8use utoipa::ToSchema;
9
10#[derive(Serialize, Deserialize, ToSchema, Clone, Debug, PartialEq)]
11#[serde(rename_all = "snake_case")]
12pub enum PostChangedSource {
13 Reply, Repost, Bookmark, ReplyParent, RepostEmbed, TaggedPost, }
20
21pub enum PostChangedType {
22 Edited,
23 Deleted,
24}
25
26#[derive(Serialize, Deserialize, ToSchema, Default, Debug)]
27pub struct Notification {
28 pub timestamp: i64,
29 pub body: NotificationBody,
30}
31
32#[derive(Serialize, Deserialize, Clone, ToSchema, Debug)]
33#[serde(tag = "type", rename_all = "snake_case")]
34pub enum NotificationBody {
35 Follow {
36 followed_by: String,
37 },
38 NewFriend {
39 followed_by: String,
40 },
41 LostFriend {
42 unfollowed_by: String,
43 },
44 TagPost {
45 tagged_by: String,
46 tag_label: String,
47 post_uri: String,
48 },
49 TagProfile {
50 tagged_by: String,
51 tag_label: String,
52 },
53 Reply {
54 replied_by: String,
55 parent_post_uri: String,
56 reply_uri: String,
57 },
58 Repost {
59 reposted_by: String,
60 embed_uri: String,
61 repost_uri: String,
62 },
63 Mention {
64 mentioned_by: String,
65 post_uri: String,
66 },
67 PostDeleted {
68 delete_source: PostChangedSource,
69 deleted_by: String,
70 deleted_uri: String,
71 linked_uri: String,
72 },
73 PostEdited {
74 edit_source: PostChangedSource,
75 edited_by: String,
76 edited_uri: String,
77 linked_uri: String,
78 },
79}
80
81type QueryFunction = fn(&str, &str) -> neo4rs::Query;
82type ExtractFunction = Box<dyn Fn(&Row) -> (String, String) + Send>;
83
84impl Default for NotificationBody {
85 fn default() -> Self {
86 NotificationBody::Follow {
87 followed_by: String::new(),
88 }
89 }
90}
91
92impl RedisOps for Notification {}
93
94impl Notification {
95 pub fn new(body: NotificationBody) -> Self {
96 Self {
97 body,
98 timestamp: Utc::now().timestamp_millis(), }
100 }
101
102 async fn put_to_index(&self, user_id: &str) -> Result<(), DynError> {
104 let notification_body_json = serde_json::to_string(&self.body)?;
105 let score = self.timestamp as f64;
106
107 Notification::put_index_sorted_set(
108 &["Notification", user_id],
109 &[(score, notification_body_json.as_str())],
110 None,
111 None,
112 )
113 .await
114 }
115
116 pub async fn get_by_id(user_id: &str, pagination: Pagination) -> Result<Vec<Self>, DynError> {
118 let skip = pagination.skip.unwrap_or(0);
120 let limit = pagination.limit.unwrap_or(20);
121
122 let notifications = Notification::try_from_index_sorted_set(
123 &["Notification", user_id],
124 pagination.start,
125 pagination.end,
126 Some(skip),
127 Some(limit),
128 SortOrder::Descending, None,
130 )
131 .await?;
132
133 let mut result = Vec::new();
134
135 if let Some(notifications) = notifications {
136 for (notification_body_str, score) in notifications {
137 if let Ok(body) = serde_json::from_str::<NotificationBody>(¬ification_body_str) {
138 let notification = Notification {
139 timestamp: score as i64,
140 body,
141 };
142 result.push(notification);
143 }
144 }
145 }
146
147 Ok(result)
148 }
149
150 pub async fn new_follow(
151 user_id: &str,
152 followee_id: &str,
153 new_friend: bool,
154 ) -> Result<(), DynError> {
155 let body = match new_friend {
156 true => NotificationBody::NewFriend {
157 followed_by: user_id.to_string(),
158 },
159 false => NotificationBody::Follow {
160 followed_by: user_id.to_string(),
161 },
162 };
163
164 let notification = Notification::new(body);
165 notification.put_to_index(followee_id).await?;
166
167 Ok(())
168 }
169
170 pub async fn lost_follow(
171 user_id: &str,
172 followee_id: &str,
173 were_friends: bool,
174 ) -> Result<(), DynError> {
175 if !were_friends {
176 return Ok(());
177 }
178
179 let body = NotificationBody::LostFriend {
180 unfollowed_by: user_id.to_string(),
181 };
182 let notification = Notification::new(body);
183 notification.put_to_index(followee_id).await?;
184
185 Ok(())
186 }
187
188 pub async fn new_post_tag(
189 user_id: &str,
190 author_id: &str,
191 label: &str,
192 post_uri: &str,
193 ) -> Result<(), DynError> {
194 if user_id == author_id {
195 return Ok(());
196 }
197 let body = NotificationBody::TagPost {
198 tagged_by: user_id.to_string(),
199 tag_label: label.to_string(),
200 post_uri: post_uri.to_string(),
201 };
202 let notification = Notification::new(body);
203 notification.put_to_index(author_id).await
204 }
205
206 pub async fn new_user_tag(
207 tagger_user_id: &str,
208 tagged_user_id: &str,
209 label: &str,
210 ) -> Result<(), DynError> {
211 if tagger_user_id == tagged_user_id {
212 return Ok(());
213 }
214 let body = NotificationBody::TagProfile {
215 tagged_by: tagger_user_id.to_string(),
216 tag_label: label.to_string(),
217 };
218 let notification = Notification::new(body);
219 notification.put_to_index(tagged_user_id).await
220 }
221
222 pub async fn new_post_reply(
223 user_id: &str,
224 parent_uri: &str,
225 reply_uri: &str,
226 parent_post_author: &str,
227 ) -> Result<(), DynError> {
228 if user_id == parent_post_author {
229 return Ok(());
230 }
231 let body = NotificationBody::Reply {
232 replied_by: user_id.to_string(),
233 parent_post_uri: parent_uri.to_string(),
234 reply_uri: reply_uri.to_string(),
235 };
236 let notification = Notification::new(body);
237 notification.put_to_index(parent_post_author).await
238 }
239
240 pub async fn new_mention(
241 user_id: &str,
242 mentioned_id: &str,
243 post_id: &str,
244 ) -> Result<Option<String>, DynError> {
245 if user_id == mentioned_id {
246 return Ok(None);
247 }
248 let body = NotificationBody::Mention {
249 mentioned_by: user_id.to_string(),
250 post_uri: format!("pubky://{user_id}/pub/pubky.app/posts/{post_id}"),
251 };
252 let notification = Notification::new(body);
253 notification.put_to_index(mentioned_id).await?;
254
255 Ok(Some(mentioned_id.to_string()))
256 }
257
258 pub async fn new_repost(
259 user_id: &str,
260 embed_uri: &str,
261 repost_uri: &str,
262 embed_post_author: &str,
263 ) -> Result<(), DynError> {
264 if user_id == embed_post_author {
265 return Ok(());
266 }
267 let body = NotificationBody::Repost {
268 reposted_by: user_id.to_string(),
269 embed_uri: embed_uri.to_string(),
270 repost_uri: repost_uri.to_string(),
271 };
272 let notification = Notification::new(body);
273 notification.put_to_index(embed_post_author).await
274 }
275
276 pub async fn post_children_changed(
277 user_id: &str,
278 linked_uri: &str,
279 linked_post_author: &str,
280 changed_uri: &str,
281 change_source: PostChangedSource,
282 changed_type: &PostChangedType,
283 ) -> Result<(), DynError> {
284 if user_id == linked_post_author {
285 return Ok(());
286 }
287 let body = match changed_type {
288 PostChangedType::Deleted => NotificationBody::PostDeleted {
289 delete_source: change_source,
290 deleted_by: user_id.to_string(),
291 deleted_uri: changed_uri.to_string(),
292 linked_uri: linked_uri.to_string(),
293 },
294 PostChangedType::Edited => NotificationBody::PostEdited {
295 edit_source: change_source,
296 edited_by: user_id.to_string(),
297 edited_uri: changed_uri.to_string(),
298 linked_uri: linked_uri.to_string(),
299 },
300 };
301 let notification = Notification::new(body);
302 notification.put_to_index(linked_post_author).await
303 }
304
305 pub async fn changed_post(
309 author_id: &str,
310 post_id: &str,
311 changed_uri: &str,
312 changed_type: &PostChangedType,
313 ) -> Result<(), DynError> {
314 let notification_types: Vec<(QueryFunction, PostChangedSource, ExtractFunction)> = vec![
316 (
317 queries::get::get_post_replies as QueryFunction,
318 PostChangedSource::ReplyParent,
319 Box::new(|row: &Row| {
320 let replier_id: &str = row.get("replier_id").unwrap_or_default();
321 let reply_id: &str = row.get("reply_id").unwrap_or_default();
322 let linked_uri = format!("pubky://{replier_id}/pub/pubky.app/posts/{reply_id}");
323 (replier_id.to_string(), linked_uri)
324 }),
325 ),
326 (
327 queries::get::get_post_tags as QueryFunction,
328 PostChangedSource::TaggedPost,
329 Box::new(|row: &Row| {
330 let tagger_id: &str = row.get("tagger_id").unwrap_or_default();
331 let tag_id: &str = row.get("tag_id").unwrap_or_default();
332 let linked_uri = format!("pubky://{tagger_id}/pub/pubky.app/tags/{tag_id}");
333 (tagger_id.to_string(), linked_uri)
334 }),
335 ),
336 (
337 queries::get::get_post_bookmarks as QueryFunction,
338 PostChangedSource::Bookmark,
339 Box::new(|row: &Row| {
340 let bookmarker_id: &str = row.get("bookmarker_id").unwrap_or_default();
341 let bookmark_id: &str = row.get("bookmark_id").unwrap_or_default();
342 let linked_uri =
343 format!("pubky://{bookmarker_id}/pub/pubky.app/bookmarks/{bookmark_id}");
344 (bookmarker_id.to_string(), linked_uri)
345 }),
346 ),
347 (
348 queries::get::get_post_reposts as QueryFunction,
349 PostChangedSource::RepostEmbed,
350 Box::new(|row: &Row| {
351 let reposter_id: &str = row.get("reposter_id").unwrap_or_default();
352 let repost_id: &str = row.get("repost_id").unwrap_or_default();
353 let linked_uri =
354 format!("pubky://{reposter_id}/pub/pubky.app/posts/{repost_id}");
355 (reposter_id.to_string(), linked_uri)
356 }),
357 ),
358 ];
359
360 for (query_fn, post_changed_source, extract_fn) in notification_types {
361 let mut result;
362 {
363 let graph = get_neo4j_graph()?;
364 let query = query_fn(author_id, post_id);
365
366 let graph = graph.lock().await;
367 result = graph.execute(query).await?;
368 }
369
370 while let Some(row) = result.next().await? {
371 let (user_id, linked_uri) = extract_fn(&row);
372
373 if author_id == user_id {
374 continue;
376 }
377
378 let notification_body = match changed_type {
379 PostChangedType::Deleted => NotificationBody::PostDeleted {
380 delete_source: post_changed_source.clone(),
381 deleted_by: author_id.to_string(),
382 deleted_uri: changed_uri.to_string(),
383 linked_uri,
384 },
385 PostChangedType::Edited => NotificationBody::PostEdited {
386 edit_source: post_changed_source.clone(),
387 edited_by: author_id.to_string(),
388 edited_uri: changed_uri.to_string(),
389 linked_uri,
390 },
391 };
392
393 let notification = Notification::new(notification_body);
394 notification.put_to_index(&user_id).await?;
395 }
396 }
397 Ok(())
398 }
399}