feat(ap): @mention notification from inbound remote Notes
This commit is contained in:
@@ -55,6 +55,14 @@ pub trait ApObjectHandler: Send + Sync {
|
|||||||
/// Called when a remote actor removes a Like from a local thought.
|
/// Called when a remote actor removes a Like from a local thought.
|
||||||
async fn on_unlike(&self, object_url: &Url, actor_url: &Url) -> anyhow::Result<()>;
|
async fn on_unlike(&self, object_url: &Url, actor_url: &Url) -> anyhow::Result<()>;
|
||||||
|
|
||||||
|
/// Called when an inbound Note tags a local user with a Mention.
|
||||||
|
async fn on_mention(
|
||||||
|
&self,
|
||||||
|
thought_ap_id: &Url,
|
||||||
|
mentioned_user_uuid: uuid::Uuid,
|
||||||
|
actor_url: &Url,
|
||||||
|
) -> anyhow::Result<()>;
|
||||||
|
|
||||||
/// Total number of locally-authored posts across all users.
|
/// Total number of locally-authored posts across all users.
|
||||||
async fn count_local_posts(&self) -> anyhow::Result<u64>;
|
async fn count_local_posts(&self) -> anyhow::Result<u64>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,7 +147,44 @@ impl ApObjectHandler for ThoughtsObjectHandler {
|
|||||||
note.in_reply_to.as_ref(),
|
note.in_reply_to.as_ref(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| anyhow!("{e}"))
|
.map_err(|e| anyhow!("{e}"))?;
|
||||||
|
|
||||||
|
// Fire mention notifications for local @mentions in the note's tag array.
|
||||||
|
let base_url = url::Url::parse(&self.urls.base_url)
|
||||||
|
.ok()
|
||||||
|
.and_then(|u| u.host_str().map(|h| h.to_string()))
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
for tag in ¬e.tag {
|
||||||
|
if tag.get("type").and_then(|t| t.as_str()) != Some("Mention") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let href = match tag.get("href").and_then(|h| h.as_str()) {
|
||||||
|
Some(h) => h,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
let href_url = match url::Url::parse(href) {
|
||||||
|
Ok(u) => u,
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
|
if href_url.host_str().unwrap_or("") != base_url {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let user_uuid = href_url
|
||||||
|
.path()
|
||||||
|
.strip_prefix("/users/")
|
||||||
|
.and_then(|s| s.split('/').next())
|
||||||
|
.and_then(|s| uuid::Uuid::parse_str(s).ok());
|
||||||
|
if let Some(uuid) = user_uuid {
|
||||||
|
self.on_mention(ap_id, uuid, actor_url)
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
tracing::warn!(error = %e, "failed to process mention notification");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn on_update(
|
async fn on_update(
|
||||||
@@ -225,6 +262,46 @@ impl ApObjectHandler for ThoughtsObjectHandler {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn on_mention(
|
||||||
|
&self,
|
||||||
|
thought_ap_id: &url::Url,
|
||||||
|
mentioned_user_uuid: uuid::Uuid,
|
||||||
|
actor_url: &url::Url,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let author_user_id = match self
|
||||||
|
.repo
|
||||||
|
.find_remote_actor_id(actor_url)
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow!("{e}"))?
|
||||||
|
{
|
||||||
|
Some(id) => id,
|
||||||
|
None => return Ok(()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let thought_uuid = thought_ap_id
|
||||||
|
.path()
|
||||||
|
.strip_prefix("/thoughts/")
|
||||||
|
.and_then(|s| s.split('/').next())
|
||||||
|
.and_then(|s| uuid::Uuid::parse_str(s).ok());
|
||||||
|
|
||||||
|
let thought_uuid = match thought_uuid {
|
||||||
|
Some(u) => u,
|
||||||
|
None => return Ok(()),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(ep) = &self.event_publisher {
|
||||||
|
ep.publish(&domain::events::DomainEvent::MentionReceived {
|
||||||
|
thought_id: domain::value_objects::ThoughtId::from_uuid(thought_uuid),
|
||||||
|
mentioned_user_id: domain::value_objects::UserId::from_uuid(mentioned_user_uuid),
|
||||||
|
author_user_id,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow!("{e}"))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn on_announce_received(&self, object_url: &Url, actor_url: &Url) -> Result<()> {
|
async fn on_announce_received(&self, object_url: &Url, actor_url: &Url) -> Result<()> {
|
||||||
let thought_uuid = object_url
|
let thought_uuid = object_url
|
||||||
.path()
|
.path()
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ pub struct ThoughtNote {
|
|||||||
pub sensitive: bool,
|
pub sensitive: bool,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub summary: Option<String>,
|
pub summary: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Vec::is_empty", default)]
|
||||||
|
pub tag: Vec<serde_json::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ThoughtNote {
|
impl ThoughtNote {
|
||||||
@@ -50,6 +52,7 @@ impl ThoughtNote {
|
|||||||
in_reply_to,
|
in_reply_to,
|
||||||
sensitive,
|
sensitive,
|
||||||
summary,
|
summary,
|
||||||
|
tag: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,11 @@ pub enum EventPayload {
|
|||||||
connection_type: String,
|
connection_type: String,
|
||||||
page: u32,
|
page: u32,
|
||||||
},
|
},
|
||||||
|
MentionReceived {
|
||||||
|
thought_id: String,
|
||||||
|
mentioned_user_id: String,
|
||||||
|
author_user_id: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventPayload {
|
impl EventPayload {
|
||||||
@@ -104,6 +109,7 @@ impl EventPayload {
|
|||||||
Self::ProfileUpdated { .. } => "users.profile_updated",
|
Self::ProfileUpdated { .. } => "users.profile_updated",
|
||||||
Self::FetchRemoteActorPosts { .. } => "federation.fetch_outbox",
|
Self::FetchRemoteActorPosts { .. } => "federation.fetch_outbox",
|
||||||
Self::FetchActorConnections { .. } => "federation.fetch_connections",
|
Self::FetchActorConnections { .. } => "federation.fetch_connections",
|
||||||
|
Self::MentionReceived { .. } => "mentions.received",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -234,6 +240,15 @@ impl From<&DomainEvent> for EventPayload {
|
|||||||
connection_type: connection_type.clone(),
|
connection_type: connection_type.clone(),
|
||||||
page: *page,
|
page: *page,
|
||||||
},
|
},
|
||||||
|
DomainEvent::MentionReceived {
|
||||||
|
thought_id,
|
||||||
|
mentioned_user_id,
|
||||||
|
author_user_id,
|
||||||
|
} => Self::MentionReceived {
|
||||||
|
thought_id: thought_id.to_string(),
|
||||||
|
mentioned_user_id: mentioned_user_id.to_string(),
|
||||||
|
author_user_id: author_user_id.to_string(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -373,6 +388,18 @@ impl TryFrom<EventPayload> for DomainEvent {
|
|||||||
connection_type,
|
connection_type,
|
||||||
page,
|
page,
|
||||||
},
|
},
|
||||||
|
EventPayload::MentionReceived {
|
||||||
|
thought_id,
|
||||||
|
mentioned_user_id,
|
||||||
|
author_user_id,
|
||||||
|
} => DomainEvent::MentionReceived {
|
||||||
|
thought_id: ThoughtId::from_uuid(parse_uuid(&thought_id, "thought_id")?),
|
||||||
|
mentioned_user_id: UserId::from_uuid(parse_uuid(
|
||||||
|
&mentioned_user_id,
|
||||||
|
"mentioned_user_id",
|
||||||
|
)?),
|
||||||
|
author_user_id: UserId::from_uuid(parse_uuid(&author_user_id, "author_user_id")?),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,6 +112,23 @@ impl NotificationEventService {
|
|||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
DomainEvent::MentionReceived {
|
||||||
|
thought_id,
|
||||||
|
mentioned_user_id,
|
||||||
|
author_user_id,
|
||||||
|
} => {
|
||||||
|
self.notifications
|
||||||
|
.save(&Notification {
|
||||||
|
id: NotificationId::new(),
|
||||||
|
user_id: mentioned_user_id.clone(),
|
||||||
|
notification_type: NotificationType::Mention,
|
||||||
|
from_user_id: Some(author_user_id.clone()),
|
||||||
|
thought_id: Some(thought_id.clone()),
|
||||||
|
read: false,
|
||||||
|
created_at: Utc::now(),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
_ => Ok(()),
|
_ => Ok(()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,6 +73,11 @@ pub enum DomainEvent {
|
|||||||
connection_type: String,
|
connection_type: String,
|
||||||
page: u32,
|
page: u32,
|
||||||
},
|
},
|
||||||
|
MentionReceived {
|
||||||
|
thought_id: ThoughtId,
|
||||||
|
mentioned_user_id: UserId,
|
||||||
|
author_user_id: UserId,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct EventEnvelope {
|
pub struct EventEnvelope {
|
||||||
|
|||||||
Reference in New Issue
Block a user