use crate::db::kv::SortOrder;
use crate::db::{get_neo4j_graph, queries, RedisOps};
use crate::types::DynError;
use crate::types::Pagination;
use chrono::Utc;
use neo4rs::Row;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
#[derive(Serialize, Deserialize, ToSchema, Clone, Debug, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum PostChangedSource {
Reply, Repost, Bookmark, ReplyParent, RepostEmbed, TaggedPost, }
pub enum PostChangedType {
Edited,
Deleted,
}
#[derive(Serialize, Deserialize, ToSchema, Default, Debug)]
pub struct Notification {
pub timestamp: i64,
pub body: NotificationBody,
}
#[derive(Serialize, Deserialize, Clone, ToSchema, Debug)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum NotificationBody {
Follow {
followed_by: String,
},
NewFriend {
followed_by: String,
},
LostFriend {
unfollowed_by: String,
},
TagPost {
tagged_by: String,
tag_label: String,
post_uri: String,
},
TagProfile {
tagged_by: String,
tag_label: String,
},
Reply {
replied_by: String,
parent_post_uri: String,
reply_uri: String,
},
Repost {
reposted_by: String,
embed_uri: String,
repost_uri: String,
},
Mention {
mentioned_by: String,
post_uri: String,
},
PostDeleted {
delete_source: PostChangedSource,
deleted_by: String,
deleted_uri: String,
linked_uri: String,
},
PostEdited {
edit_source: PostChangedSource,
edited_by: String,
edited_uri: String,
linked_uri: String,
},
}
type QueryFunction = fn(&str, &str) -> neo4rs::Query;
type ExtractFunction = Box<dyn Fn(&Row) -> (String, String) + Send>;
impl Default for NotificationBody {
fn default() -> Self {
NotificationBody::Follow {
followed_by: String::new(),
}
}
}
impl RedisOps for Notification {}
impl Notification {
pub fn new(body: NotificationBody) -> Self {
Self {
body,
timestamp: Utc::now().timestamp_millis(), }
}
async fn put_to_index(&self, user_id: &str) -> Result<(), DynError> {
let notification_body_json = serde_json::to_string(&self.body)?;
let score = self.timestamp as f64;
Notification::put_index_sorted_set(
&["Notification", user_id],
&[(score, notification_body_json.as_str())],
None,
None,
)
.await
}
pub async fn get_by_id(user_id: &str, pagination: Pagination) -> Result<Vec<Self>, DynError> {
let skip = pagination.skip.unwrap_or(0);
let limit = pagination.limit.unwrap_or(20);
let notifications = Notification::try_from_index_sorted_set(
&["Notification", user_id],
pagination.start,
pagination.end,
Some(skip),
Some(limit),
SortOrder::Descending, None,
)
.await?;
let mut result = Vec::new();
if let Some(notifications) = notifications {
for (notification_body_str, score) in notifications {
if let Ok(body) = serde_json::from_str::<NotificationBody>(¬ification_body_str) {
let notification = Notification {
timestamp: score as i64,
body,
};
result.push(notification);
}
}
}
Ok(result)
}
pub async fn new_follow(
user_id: &str,
followee_id: &str,
new_friend: bool,
) -> Result<(), DynError> {
let body = match new_friend {
true => NotificationBody::NewFriend {
followed_by: user_id.to_string(),
},
false => NotificationBody::Follow {
followed_by: user_id.to_string(),
},
};
let notification = Notification::new(body);
notification.put_to_index(followee_id).await?;
Ok(())
}
pub async fn lost_follow(
user_id: &str,
followee_id: &str,
were_friends: bool,
) -> Result<(), DynError> {
if !were_friends {
return Ok(());
}
let body = NotificationBody::LostFriend {
unfollowed_by: user_id.to_string(),
};
let notification = Notification::new(body);
notification.put_to_index(followee_id).await?;
Ok(())
}
pub async fn new_post_tag(
user_id: &str,
author_id: &str,
label: &str,
post_uri: &str,
) -> Result<(), DynError> {
if user_id == author_id {
return Ok(());
}
let body = NotificationBody::TagPost {
tagged_by: user_id.to_string(),
tag_label: label.to_string(),
post_uri: post_uri.to_string(),
};
let notification = Notification::new(body);
notification.put_to_index(author_id).await
}
pub async fn new_user_tag(
tagger_user_id: &str,
tagged_user_id: &str,
label: &str,
) -> Result<(), DynError> {
if tagger_user_id == tagged_user_id {
return Ok(());
}
let body = NotificationBody::TagProfile {
tagged_by: tagger_user_id.to_string(),
tag_label: label.to_string(),
};
let notification = Notification::new(body);
notification.put_to_index(tagged_user_id).await
}
pub async fn new_post_reply(
user_id: &str,
parent_uri: &str,
reply_uri: &str,
parent_post_author: &str,
) -> Result<(), DynError> {
if user_id == parent_post_author {
return Ok(());
}
let body = NotificationBody::Reply {
replied_by: user_id.to_string(),
parent_post_uri: parent_uri.to_string(),
reply_uri: reply_uri.to_string(),
};
let notification = Notification::new(body);
notification.put_to_index(parent_post_author).await
}
pub async fn new_mention(
user_id: &str,
mentioned_id: &str,
post_id: &str,
) -> Result<Option<String>, DynError> {
if user_id == mentioned_id {
return Ok(None);
}
let body = NotificationBody::Mention {
mentioned_by: user_id.to_string(),
post_uri: format!("pubky://{user_id}/pub/pubky.app/posts/{post_id}"),
};
let notification = Notification::new(body);
notification.put_to_index(mentioned_id).await?;
Ok(Some(mentioned_id.to_string()))
}
pub async fn new_repost(
user_id: &str,
embed_uri: &str,
repost_uri: &str,
embed_post_author: &str,
) -> Result<(), DynError> {
if user_id == embed_post_author {
return Ok(());
}
let body = NotificationBody::Repost {
reposted_by: user_id.to_string(),
embed_uri: embed_uri.to_string(),
repost_uri: repost_uri.to_string(),
};
let notification = Notification::new(body);
notification.put_to_index(embed_post_author).await
}
pub async fn post_children_changed(
user_id: &str,
linked_uri: &str,
linked_post_author: &str,
changed_uri: &str,
change_source: PostChangedSource,
changed_type: &PostChangedType,
) -> Result<(), DynError> {
if user_id == linked_post_author {
return Ok(());
}
let body = match changed_type {
PostChangedType::Deleted => NotificationBody::PostDeleted {
delete_source: change_source,
deleted_by: user_id.to_string(),
deleted_uri: changed_uri.to_string(),
linked_uri: linked_uri.to_string(),
},
PostChangedType::Edited => NotificationBody::PostEdited {
edit_source: change_source,
edited_by: user_id.to_string(),
edited_uri: changed_uri.to_string(),
linked_uri: linked_uri.to_string(),
},
};
let notification = Notification::new(body);
notification.put_to_index(linked_post_author).await
}
pub async fn changed_post(
author_id: &str,
post_id: &str,
changed_uri: &str,
changed_type: &PostChangedType,
) -> Result<(), DynError> {
let notification_types: Vec<(QueryFunction, PostChangedSource, ExtractFunction)> = vec![
(
queries::get::get_post_replies as QueryFunction,
PostChangedSource::ReplyParent,
Box::new(|row: &Row| {
let replier_id: &str = row.get("replier_id").unwrap_or_default();
let reply_id: &str = row.get("reply_id").unwrap_or_default();
let linked_uri = format!("pubky://{replier_id}/pub/pubky.app/posts/{reply_id}");
(replier_id.to_string(), linked_uri)
}),
),
(
queries::get::get_post_tags as QueryFunction,
PostChangedSource::TaggedPost,
Box::new(|row: &Row| {
let tagger_id: &str = row.get("tagger_id").unwrap_or_default();
let tag_id: &str = row.get("tag_id").unwrap_or_default();
let linked_uri = format!("pubky://{tagger_id}/pub/pubky.app/tags/{tag_id}");
(tagger_id.to_string(), linked_uri)
}),
),
(
queries::get::get_post_bookmarks as QueryFunction,
PostChangedSource::Bookmark,
Box::new(|row: &Row| {
let bookmarker_id: &str = row.get("bookmarker_id").unwrap_or_default();
let bookmark_id: &str = row.get("bookmark_id").unwrap_or_default();
let linked_uri =
format!("pubky://{bookmarker_id}/pub/pubky.app/bookmarks/{bookmark_id}");
(bookmarker_id.to_string(), linked_uri)
}),
),
(
queries::get::get_post_reposts as QueryFunction,
PostChangedSource::RepostEmbed,
Box::new(|row: &Row| {
let reposter_id: &str = row.get("reposter_id").unwrap_or_default();
let repost_id: &str = row.get("repost_id").unwrap_or_default();
let linked_uri =
format!("pubky://{reposter_id}/pub/pubky.app/posts/{repost_id}");
(reposter_id.to_string(), linked_uri)
}),
),
];
for (query_fn, post_changed_source, extract_fn) in notification_types {
let mut result;
{
let graph = get_neo4j_graph()?;
let query = query_fn(author_id, post_id);
let graph = graph.lock().await;
result = graph.execute(query).await?;
}
while let Some(row) = result.next().await? {
let (user_id, linked_uri) = extract_fn(&row);
if author_id == user_id {
continue;
}
let notification_body = match changed_type {
PostChangedType::Deleted => NotificationBody::PostDeleted {
delete_source: post_changed_source.clone(),
deleted_by: author_id.to_string(),
deleted_uri: changed_uri.to_string(),
linked_uri,
},
PostChangedType::Edited => NotificationBody::PostEdited {
edit_source: post_changed_source.clone(),
edited_by: author_id.to_string(),
edited_uri: changed_uri.to_string(),
linked_uri,
},
};
let notification = Notification::new(notification_body);
notification.put_to_index(&user_id).await?;
}
}
Ok(())
}
}