feat(activitypub-base): broadcast_like/undo_like + LikeActivity inbox handler
This commit is contained in:
@@ -516,6 +516,12 @@ impl Activity for AnnounceActivity {
|
|||||||
self.published.unwrap_or_else(chrono::Utc::now),
|
self.published.unwrap_or_else(chrono::Utc::now),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
data.object_handler
|
||||||
|
.on_announce_received(&self.object, self.actor.inner())
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
tracing::warn!(error = %e, "failed to process announce notification");
|
||||||
|
});
|
||||||
tracing::info!(actor = %self.actor.inner(), object = %self.object, "received announce");
|
tracing::info!(actor = %self.actor.inner(), object = %self.object, "received announce");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -533,6 +539,45 @@ pub struct LikeActivity {
|
|||||||
pub object: Url,
|
pub object: Url,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl Activity for LikeActivity {
|
||||||
|
type DataType = FederationData;
|
||||||
|
type Error = crate::error::Error;
|
||||||
|
|
||||||
|
fn id(&self) -> &Url {
|
||||||
|
&self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn actor(&self) -> &Url {
|
||||||
|
self.actor.inner()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn verify(&self, _data: &Data<Self::DataType>) -> Result<(), Self::Error> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn receive(self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
|
||||||
|
let domain = self.actor().host_str().unwrap_or("");
|
||||||
|
if data.federation_repo.is_domain_blocked(domain).await? {
|
||||||
|
tracing::info!(actor = %self.actor(), "ignoring Like from blocked domain");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only process if the liked object is on our instance.
|
||||||
|
if self.object.host_str().unwrap_or("") != data.domain {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
data.object_handler
|
||||||
|
.on_like(&self.object, self.actor.inner())
|
||||||
|
.await
|
||||||
|
.map_err(|e| crate::error::Error::from(anyhow::anyhow!(e)))?;
|
||||||
|
|
||||||
|
tracing::info!(actor = %self.actor.inner(), object = %self.object, "received like");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Add ---
|
// --- Add ---
|
||||||
|
|
||||||
#[derive(Clone, Default, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Default, Debug, Serialize, Deserialize)]
|
||||||
@@ -664,4 +709,6 @@ pub enum InboxActivities {
|
|||||||
Add(AddActivity),
|
Add(AddActivity),
|
||||||
#[serde(rename = "Block")]
|
#[serde(rename = "Block")]
|
||||||
Block(BlockActivity),
|
Block(BlockActivity),
|
||||||
|
#[serde(rename = "Like")]
|
||||||
|
Like(LikeActivity),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -340,6 +340,105 @@ impl ActivityPubService {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Send a Like activity to a single inbox.
|
||||||
|
pub async fn broadcast_like_to_inbox(
|
||||||
|
&self,
|
||||||
|
liker_user_id: uuid::Uuid,
|
||||||
|
object_ap_id: url::Url,
|
||||||
|
author_inbox_url: url::Url,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let data = self.federation_config.to_request_data();
|
||||||
|
let local_actor = get_local_actor(liker_user_id, &data)
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||||
|
|
||||||
|
// Deterministic ID so Undo(Like) can reference the same activity.
|
||||||
|
let like_id = url::Url::parse(&format!(
|
||||||
|
"{}/activities/like/{}",
|
||||||
|
self.base_url,
|
||||||
|
uuid::Uuid::new_v5(
|
||||||
|
&uuid::Uuid::NAMESPACE_URL,
|
||||||
|
format!("{}/{}", liker_user_id, object_ap_id).as_bytes(),
|
||||||
|
)
|
||||||
|
))?;
|
||||||
|
|
||||||
|
let like = crate::activities::LikeActivity {
|
||||||
|
id: like_id,
|
||||||
|
kind: Default::default(),
|
||||||
|
actor: ObjectId::from(local_actor.ap_id.clone()),
|
||||||
|
object: object_ap_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
let sends = SendActivityTask::prepare(
|
||||||
|
&WithContext::new_default(like),
|
||||||
|
&local_actor,
|
||||||
|
vec![author_inbox_url],
|
||||||
|
&data,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let failures = send_with_retry(sends, &data).await;
|
||||||
|
if !failures.is_empty() {
|
||||||
|
tracing::warn!(
|
||||||
|
count = failures.len(),
|
||||||
|
"some Like deliveries failed permanently"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send an Undo(Like) activity to a single inbox.
|
||||||
|
pub async fn broadcast_undo_like_to_inbox(
|
||||||
|
&self,
|
||||||
|
liker_user_id: uuid::Uuid,
|
||||||
|
object_ap_id: url::Url,
|
||||||
|
author_inbox_url: url::Url,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let data = self.federation_config.to_request_data();
|
||||||
|
let local_actor = get_local_actor(liker_user_id, &data)
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||||
|
|
||||||
|
// Reconstruct the same deterministic like ID.
|
||||||
|
let like_id = url::Url::parse(&format!(
|
||||||
|
"{}/activities/like/{}",
|
||||||
|
self.base_url,
|
||||||
|
uuid::Uuid::new_v5(
|
||||||
|
&uuid::Uuid::NAMESPACE_URL,
|
||||||
|
format!("{}/{}", liker_user_id, object_ap_id).as_bytes(),
|
||||||
|
)
|
||||||
|
))?;
|
||||||
|
|
||||||
|
let undo_id = activity_url(&self.base_url).map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||||
|
|
||||||
|
let undo = crate::activities::UndoActivity {
|
||||||
|
id: undo_id,
|
||||||
|
kind: Default::default(),
|
||||||
|
actor: ObjectId::from(local_actor.ap_id.clone()),
|
||||||
|
object: serde_json::json!({
|
||||||
|
"type": "Like",
|
||||||
|
"id": like_id.to_string(),
|
||||||
|
"actor": local_actor.ap_id.to_string(),
|
||||||
|
"object": object_ap_id.to_string(),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let sends = SendActivityTask::prepare(
|
||||||
|
&WithContext::new_default(undo),
|
||||||
|
&local_actor,
|
||||||
|
vec![author_inbox_url],
|
||||||
|
&data,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let failures = send_with_retry(sends, &data).await;
|
||||||
|
if !failures.is_empty() {
|
||||||
|
tracing::warn!(
|
||||||
|
count = failures.len(),
|
||||||
|
"some Undo(Like) deliveries failed permanently"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Resolve a `@user@domain` handle to a `DbActor` over HTTPS directly.
|
/// Resolve a `@user@domain` handle to a `DbActor` over HTTPS directly.
|
||||||
/// The library's `webfinger_resolve_actor` tries HTTP first in debug mode, which breaks
|
/// The library's `webfinger_resolve_actor` tries HTTP first in debug mode, which breaks
|
||||||
/// on servers that don't redirect HTTP → HTTPS.
|
/// on servers that don't redirect HTTP → HTTPS.
|
||||||
@@ -1393,22 +1492,32 @@ impl domain::ports::OutboundFederationPort for ActivityPubService {
|
|||||||
|
|
||||||
async fn broadcast_like(
|
async fn broadcast_like(
|
||||||
&self,
|
&self,
|
||||||
_liker_user_id: &domain::value_objects::UserId,
|
liker_user_id: &domain::value_objects::UserId,
|
||||||
_object_ap_id: &str,
|
object_ap_id: &str,
|
||||||
_author_inbox_url: &str,
|
author_inbox_url: &str,
|
||||||
) -> Result<(), domain::errors::DomainError> {
|
) -> Result<(), domain::errors::DomainError> {
|
||||||
// TODO: implement Like activity broadcasting
|
let object = url::Url::parse(object_ap_id)
|
||||||
Ok(())
|
.map_err(|e| domain::errors::DomainError::Internal(e.to_string()))?;
|
||||||
|
let inbox = url::Url::parse(author_inbox_url)
|
||||||
|
.map_err(|e| domain::errors::DomainError::Internal(e.to_string()))?;
|
||||||
|
self.broadcast_like_to_inbox(liker_user_id.as_uuid(), object, inbox)
|
||||||
|
.await
|
||||||
|
.map_err(|e| domain::errors::DomainError::Internal(e.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn broadcast_undo_like(
|
async fn broadcast_undo_like(
|
||||||
&self,
|
&self,
|
||||||
_liker_user_id: &domain::value_objects::UserId,
|
liker_user_id: &domain::value_objects::UserId,
|
||||||
_object_ap_id: &str,
|
object_ap_id: &str,
|
||||||
_author_inbox_url: &str,
|
author_inbox_url: &str,
|
||||||
) -> Result<(), domain::errors::DomainError> {
|
) -> Result<(), domain::errors::DomainError> {
|
||||||
// TODO: implement Undo(Like) activity broadcasting
|
let object = url::Url::parse(object_ap_id)
|
||||||
Ok(())
|
.map_err(|e| domain::errors::DomainError::Internal(e.to_string()))?;
|
||||||
|
let inbox = url::Url::parse(author_inbox_url)
|
||||||
|
.map_err(|e| domain::errors::DomainError::Internal(e.to_string()))?;
|
||||||
|
self.broadcast_undo_like_to_inbox(liker_user_id.as_uuid(), object, inbox)
|
||||||
|
.await
|
||||||
|
.map_err(|e| domain::errors::DomainError::Internal(e.to_string()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user