making it AP complaint
Some checks failed
CI / Check / Test (push) Has been cancelled

This commit is contained in:
2026-06-30 02:25:42 +02:00
parent 943a0abe54
commit 5ae38c834a
66 changed files with 3635 additions and 2369 deletions

View File

@@ -24,9 +24,33 @@ impl ApContentReader for CompositeObjectHandler {
before: Option<DateTime<Utc>>,
limit: usize,
) -> anyhow::Result<Vec<(Url, serde_json::Value, DateTime<Utc>)>> {
self.review
.get_local_objects_page(user_id, before, limit)
.await
// Fetch from all three sources (watchlist/goals return all, reviews use DB pagination)
let fetch_limit = limit * 3;
let reviews = self
.review
.get_local_objects_page(user_id, before, fetch_limit)
.await?;
let watchlist = self
.watchlist
.get_local_objects_page(user_id, None, usize::MAX)
.await?;
let goals = self
.goal
.get_local_objects_page(user_id, None, usize::MAX)
.await?;
let mut all: Vec<(Url, serde_json::Value, DateTime<Utc>)> = Vec::new();
all.extend(reviews);
all.extend(watchlist);
all.extend(goals);
// Apply before filter and sort descending by timestamp
if let Some(before_ts) = before {
all.retain(|(_, _, ts)| *ts < before_ts);
}
all.sort_by_key(|b| std::cmp::Reverse(b.2));
all.truncate(limit);
Ok(all)
}
async fn count_local_posts(&self) -> anyhow::Result<u64> {
@@ -42,10 +66,14 @@ impl ApObjectHandler for CompositeObjectHandler {
actor_url: &Url,
object: serde_json::Value,
) -> anyhow::Result<()> {
let is_review = object.get("review").and_then(|v| v.as_bool()) == Some(true)
|| object.get("rating").is_some();
let is_watchlist = object.get("watchlistEntry").and_then(|v| v.as_bool()) == Some(true)
|| (object.get("movieTitle").is_some() && object.get("rating").is_none());
|| (object.get("movieTitle").is_some()
&& object.get("rating").is_none()
&& object.get("review").is_none());
let is_goal = object.get("goal").and_then(|v| v.as_bool()) == Some(true);
if object.get("rating").is_some() {
if is_review {
self.review.on_create(ap_id, actor_url, object).await
} else if is_goal {
self.goal.on_create(ap_id, actor_url, object).await
@@ -63,11 +91,16 @@ impl ApObjectHandler for CompositeObjectHandler {
actor_url: &Url,
object: serde_json::Value,
) -> anyhow::Result<()> {
let is_review = object.get("review").and_then(|v| v.as_bool()) == Some(true)
|| object.get("rating").is_some();
let is_goal = object.get("goal").and_then(|v| v.as_bool()) == Some(true);
if object.get("rating").is_some() {
let is_watchlist = object.get("watchlistEntry").and_then(|v| v.as_bool()) == Some(true);
if is_review {
self.review.on_update(ap_id, actor_url, object).await
} else if is_goal {
self.goal.on_update(ap_id, actor_url, object).await
} else if is_watchlist {
self.watchlist.on_update(ap_id, actor_url, object).await
} else {
Ok(())
}
@@ -87,36 +120,19 @@ impl ApObjectHandler for CompositeObjectHandler {
Ok(())
}
async fn on_like(&self, _object_url: &Url, _actor_url: &Url) -> anyhow::Result<()> {
async fn on_like(&self, _: &Url, _: &Url) -> anyhow::Result<()> {
Ok(())
}
async fn on_announce_received(
&self,
_object_url: &Url,
_actor_url: &Url,
) -> anyhow::Result<()> {
async fn on_announce_received(&self, _: &Url, _: &Url) -> anyhow::Result<()> {
Ok(())
}
async fn on_announce_of_remote(
&self,
_object_url: &Url,
_actor_url: &Url,
) -> anyhow::Result<()> {
async fn on_announce_of_remote(&self, _: &Url, _: &Url) -> anyhow::Result<()> {
Ok(())
}
async fn on_unlike(&self, _object_url: &Url, _actor_url: &Url) -> anyhow::Result<()> {
async fn on_unlike(&self, _: &Url, _: &Url) -> anyhow::Result<()> {
Ok(())
}
async fn on_mention(
&self,
_thought_ap_id: &Url,
_mentioned_user_uuid: uuid::Uuid,
_actor_url: &Url,
) -> anyhow::Result<()> {
async fn on_mention(&self, _: &Url, _: uuid::Uuid, _: &Url) -> anyhow::Result<()> {
Ok(())
}
}

View File

@@ -11,7 +11,7 @@ use std::sync::Arc;
use k_ap::{ActivityPubService, ApVisibility};
use crate::objects::{goal_to_ap_object, review_to_ap_object};
use crate::objects::{ReviewApInput, goal_to_ap_object, review_to_ap_object};
use crate::urls::{actor_url, goal_url, review_url};
pub struct ActivityPubEventHandler {
@@ -127,6 +127,25 @@ impl EventHandler for ActivityPubEventHandler {
.on_goal_deleted(user_id, *year)
.await
.map_err(|e| DomainError::InfrastructureError(e.to_string())),
DomainEvent::UserDeleted { user_id } => {
let ap_id = actor_url(&self.base_url, user_id.value());
self.ap_service
.broadcast_delete_to_followers(user_id.value(), ap_id)
.await
.map_err(|e| DomainError::InfrastructureError(e.to_string()))
}
DomainEvent::UserAccountMoved {
user_id,
new_actor_url,
} => {
let target = new_actor_url.parse::<url::Url>().map_err(|e| {
DomainError::InfrastructureError(format!("invalid new_actor_url: {e}"))
})?;
self.ap_service
.broadcast_move(user_id.value(), target)
.await
.map_err(|e| DomainError::InfrastructureError(e.to_string()))
}
_ => Ok(()),
}
}
@@ -169,19 +188,23 @@ impl ActivityPubEventHandler {
.as_ref()
.map(|m| m.release_year().value())
.unwrap_or(0);
let poster_url = movie
.as_ref()
.and_then(|m| m.poster_path())
.map(|p| format!("{}/images/{}", self.base_url, p.value()));
let obj = review_to_ap_object(
&review,
ap_id.clone(),
actor,
movie_title,
release_year,
poster_url,
&self.base_url,
ReviewApInput {
ap_id: ap_id.clone(),
actor_url: actor,
movie_title,
release_year,
external_metadata_id: movie
.as_ref()
.and_then(|m| m.external_metadata_id())
.map(|id| id.value().to_string()),
poster_url: movie
.as_ref()
.and_then(|m| m.poster_path())
.map(|p| format!("{}/images/{}", self.base_url, p.value())),
base_url: self.base_url.clone(),
},
);
let json = serde_json::to_value(obj)?;
@@ -235,19 +258,23 @@ impl ActivityPubEventHandler {
.as_ref()
.map(|m| m.release_year().value())
.unwrap_or(0);
let poster_url = movie
.as_ref()
.and_then(|m| m.poster_path())
.map(|p| format!("{}/images/{}", self.base_url, p.value()));
let obj = review_to_ap_object(
&review,
ap_id,
actor,
movie_title,
release_year,
poster_url,
&self.base_url,
ReviewApInput {
ap_id,
actor_url: actor,
movie_title,
release_year,
external_metadata_id: movie
.as_ref()
.and_then(|m| m.external_metadata_id())
.map(|id| id.value().to_string()),
poster_url: movie
.as_ref()
.and_then(|m| m.poster_path())
.map(|p| format!("{}/images/{}", self.base_url, p.value())),
base_url: self.base_url.clone(),
},
);
let json = serde_json::to_value(obj)?;
@@ -351,6 +378,9 @@ impl ActivityPubEventHandler {
Some(m) => m,
None => return Ok(()),
};
let external_metadata_id = movie
.external_metadata_id()
.map(|id| id.value().to_string());
let poster_url = movie
.poster_path()
.map(|p| format!("{}/images/{}", self.base_url, p.value()));
@@ -377,12 +407,15 @@ impl ActivityPubEventHandler {
let obj = review_to_ap_object(
review,
ap_id,
actor,
movie.title().value().to_string(),
movie.release_year().value(),
poster_url.clone(),
&self.base_url,
ReviewApInput {
ap_id,
actor_url: actor,
movie_title: movie.title().value().to_string(),
release_year: movie.release_year().value(),
external_metadata_id: external_metadata_id.clone(),
poster_url: poster_url.clone(),
base_url: self.base_url.clone(),
},
);
let json = serde_json::to_value(obj)?;

View File

@@ -1,14 +1,60 @@
use std::sync::Arc;
use async_trait::async_trait;
use domain::{models::RemoteGoalEntry, ports::RemoteGoalRepository};
use k_ap::ApObjectHandler;
use chrono::DateTime;
use domain::{
models::RemoteGoalEntry,
ports::{LocalApContentQuery, RemoteGoalRepository},
value_objects::UserId,
};
use k_ap::{ApContentReader, ApObjectHandler};
use url::Url;
use crate::objects::GoalObject;
use crate::objects::{GoalObject, goal_to_ap_object};
use crate::urls::{actor_url, goal_url};
pub struct GoalObjectHandler {
pub remote_goal_repo: Arc<dyn RemoteGoalRepository>,
pub content_query: Arc<dyn LocalApContentQuery>,
pub base_url: String,
}
#[async_trait]
impl ApContentReader for GoalObjectHandler {
async fn get_local_objects_page(
&self,
user_id: uuid::Uuid,
_before: Option<DateTime<chrono::Utc>>,
_limit: usize,
) -> anyhow::Result<Vec<(Url, serde_json::Value, DateTime<chrono::Utc>)>> {
let uid = UserId::from_uuid(user_id);
let goals = self
.content_query
.list_goals_for_user(&uid)
.await
.map_err(|e| anyhow::anyhow!(e.to_string()))?;
let actor = actor_url(&self.base_url, user_id);
let mut results = Vec::new();
for goal in goals {
let ap_id = goal_url(&self.base_url, user_id, goal.year());
let published = DateTime::from_naive_utc_and_offset(*goal.created_at(), chrono::Utc);
let obj = goal_to_ap_object(
ap_id.clone(),
actor.clone(),
goal.year(),
goal.target_count(),
0,
&self.base_url,
);
results.push((ap_id, serde_json::to_value(obj)?, published));
}
Ok(results)
}
async fn count_local_posts(&self) -> anyhow::Result<u64> {
Ok(0)
}
}
#[async_trait]

View File

@@ -77,6 +77,7 @@ pub async fn wire(deps: ActivityPubDeps) -> anyhow::Result<ActivityPubWire> {
let review_handler = std::sync::Arc::new(ReviewObjectHandler {
content_query: std::sync::Arc::clone(&local_ap_content),
review_store,
event_publisher: std::sync::Arc::clone(&event_publisher),
base_url: base_url.clone(),
});
let watchlist_handler = std::sync::Arc::new(watchlist_handler::WatchlistObjectHandler {
@@ -84,7 +85,11 @@ pub async fn wire(deps: ActivityPubDeps) -> anyhow::Result<ActivityPubWire> {
content_query: std::sync::Arc::clone(&local_ap_content),
base_url: base_url.clone(),
});
let goal_handler = std::sync::Arc::new(goal_handler::GoalObjectHandler { remote_goal_repo });
let goal_handler = std::sync::Arc::new(goal_handler::GoalObjectHandler {
remote_goal_repo,
content_query: std::sync::Arc::clone(&local_ap_content),
base_url: base_url.clone(),
});
let composite = std::sync::Arc::new(composite_handler::CompositeObjectHandler {
review: review_handler,
watchlist: watchlist_handler,

View File

@@ -6,6 +6,18 @@ use url::Url;
use domain::models::Review;
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ApAttachment {
#[serde(rename = "type")]
pub(crate) kind: String,
pub(crate) url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) media_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) name: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ApHashtag {
#[serde(rename = "type")]
@@ -31,10 +43,17 @@ pub struct ReviewObject {
#[serde(default)]
pub(crate) release_year: u16,
#[serde(default)]
pub(crate) external_metadata_id: Option<String>,
#[serde(default)]
pub(crate) poster_url: Option<String>,
pub(crate) rating: u8,
pub(crate) comment: Option<String>,
pub(crate) watched_at: DateTime<Utc>,
/// Discriminator so Movies Diary instances detect this as a review Note.
#[serde(default)]
pub(crate) review: bool,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub(crate) attachment: Vec<ApAttachment>,
#[serde(default)]
pub(crate) tag: Vec<ApHashtag>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
@@ -43,17 +62,26 @@ pub struct ReviewObject {
pub(crate) cc: Vec<String>,
}
/// Serialize a local Review into a ReviewObject for AP delivery.
/// Takes movie metadata explicitly since the handler fetches it separately.
pub fn review_to_ap_object(
review: &Review,
ap_id: Url,
actor_url: Url,
movie_title: String,
release_year: u16,
poster_url: Option<String>,
base_url: &str,
) -> ReviewObject {
pub struct ReviewApInput {
pub ap_id: Url,
pub actor_url: Url,
pub movie_title: String,
pub release_year: u16,
pub external_metadata_id: Option<String>,
pub poster_url: Option<String>,
pub base_url: String,
}
pub fn review_to_ap_object(review: &Review, input: ReviewApInput) -> ReviewObject {
let ReviewApInput {
ap_id,
actor_url,
movie_title,
release_year,
external_metadata_id,
poster_url,
base_url,
} = input;
let stars: String = "\u{2B50}".repeat(review.rating().value() as usize);
let comment_text = review.comment().map(|c| c.value().to_string());
let year_str = if release_year > 0 {
@@ -74,16 +102,25 @@ pub fn review_to_ap_object(
let tag = vec![
ApHashtag {
kind: "Hashtag".to_string(),
href: Url::parse(&format!("{}/tags/moviesdiary", base_url)).expect("valid base_url"),
href: Url::parse(&format!("{}/tags/moviesdiary", &base_url)).expect("valid base_url"),
name: "#MoviesDiary".to_string(),
},
ApHashtag {
kind: "Hashtag".to_string(),
href: Url::parse(&format!("{}/tags/{}", base_url, normalized.to_lowercase()))
href: Url::parse(&format!("{}/tags/{}", &base_url, normalized.to_lowercase()))
.expect("valid base_url"),
name: format!("#{}", normalized),
},
];
let attachment = match &poster_url {
Some(url) => vec![ApAttachment {
kind: "Image".to_string(),
url: url.clone(),
media_type: Some("image/jpeg".to_string()),
name: Some(movie_title.clone()),
}],
None => vec![],
};
ReviewObject {
kind: NoteType::default(),
@@ -93,10 +130,13 @@ pub fn review_to_ap_object(
published: DateTime::from_naive_utc_and_offset(*review.created_at(), Utc),
movie_title,
release_year,
external_metadata_id,
poster_url,
rating: review.rating().value(),
comment: comment_text,
watched_at: DateTime::from_naive_utc_and_offset(*review.watched_at(), Utc),
review: true,
attachment,
tag,
to: vec![AS_PUBLIC.to_string()],
cc: vec![format!("{}/followers", actor_url)],

View File

@@ -11,6 +11,7 @@ pub trait RemoteReviewRepository: Send + Sync {
ap_id: &str,
movie_title: &str,
release_year: u16,
external_metadata_id: Option<&str>,
poster_url: Option<&str>,
) -> Result<()>;

View File

@@ -2,20 +2,22 @@ use std::sync::Arc;
use async_trait::async_trait;
use domain::{
events::DomainEvent,
models::ReviewSource,
ports::LocalApContentQuery,
value_objects::{Comment, MovieId, Rating, ReviewId, UserId},
ports::{EventPublisher, LocalApContentQuery},
value_objects::{Comment, ExternalMetadataId, MovieId, Rating, ReviewId, UserId},
};
use k_ap::{ApContentReader, ApObjectHandler};
use url::Url;
use crate::objects::{ReviewObject, review_to_ap_object};
use crate::objects::{ReviewApInput, ReviewObject, review_to_ap_object};
use crate::remote_review_repository::RemoteReviewRepository;
use crate::urls::{actor_url, review_url};
pub struct ReviewObjectHandler {
pub content_query: Arc<dyn LocalApContentQuery>,
pub review_store: Arc<dyn RemoteReviewRepository>,
pub event_publisher: Arc<dyn EventPublisher>,
pub base_url: String,
}
@@ -49,12 +51,17 @@ impl ApContentReader for ReviewObjectHandler {
let obj = review_to_ap_object(
review,
ap_id.clone(),
actor.clone(),
movie.title().value().to_string(),
movie.release_year().value(),
poster_url,
&self.base_url,
ReviewApInput {
ap_id: ap_id.clone(),
actor_url: actor.clone(),
movie_title: movie.title().value().to_string(),
release_year: movie.release_year().value(),
external_metadata_id: movie
.external_metadata_id()
.map(|id| id.value().to_string()),
poster_url,
base_url: self.base_url.clone(),
},
);
results.push((ap_id, serde_json::to_value(obj)?, published));
}
@@ -89,10 +96,24 @@ impl ApObjectHandler for ReviewObjectHandler {
let actor_url_str = obj.attributed_to.to_string();
let review_id = ReviewId::generate();
let movie_id = MovieId::from_uuid(uuid::Uuid::new_v5(
&uuid::Uuid::NAMESPACE_URL,
obj.movie_title.as_bytes(),
));
let movie_id = if let Some(ref ext_id) = obj.external_metadata_id {
match self
.content_query
.get_movie_by_external_metadata_id(ext_id)
.await
{
Ok(Some(movie)) => movie.id().clone(),
_ => MovieId::from_uuid(uuid::Uuid::new_v5(
&uuid::Uuid::NAMESPACE_URL,
ext_id.as_bytes(),
)),
}
} else {
MovieId::from_uuid(uuid::Uuid::new_v5(
&uuid::Uuid::NAMESPACE_URL,
format!("{}:{}", obj.movie_title, obj.release_year).as_bytes(),
))
};
let user_id = UserId::from_uuid(uuid::Uuid::new_v5(
&uuid::Uuid::NAMESPACE_URL,
actor_url_str.as_bytes(),
@@ -102,7 +123,7 @@ impl ApObjectHandler for ReviewObjectHandler {
let review = domain::models::Review::from_persistence(domain::models::PersistedReview {
id: review_id,
movie_id,
movie_id: movie_id.clone(),
user_id,
rating,
comment,
@@ -119,10 +140,23 @@ impl ApObjectHandler for ReviewObjectHandler {
obj.id.as_str(),
&obj.movie_title,
obj.release_year,
obj.external_metadata_id.as_deref(),
obj.poster_url.as_deref(),
)
.await?;
if let Some(ref ext_id_str) = obj.external_metadata_id
&& let Ok(external_metadata_id) = ExternalMetadataId::new(ext_id_str.clone())
{
let _ = self
.event_publisher
.publish(&DomainEvent::MovieEnrichmentRequested {
movie_id: movie_id.clone(),
external_metadata_id,
})
.await;
}
Ok(())
}

View File

@@ -32,12 +32,15 @@ fn review_to_ap_object_includes_two_hashtags() {
});
let obj = review_to_ap_object(
&review,
"https://example.com/reviews/1".parse().unwrap(),
"https://example.com/users/1".parse().unwrap(),
"Dune".to_string(),
2021,
None,
"https://example.com",
ReviewApInput {
ap_id: "https://example.com/reviews/1".parse().unwrap(),
actor_url: "https://example.com/users/1".parse().unwrap(),
movie_title: "Dune".to_string(),
release_year: 2021,
external_metadata_id: None,
poster_url: None,
base_url: "https://example.com".to_string(),
},
);
assert_eq!(obj.tag.len(), 2);
let names: Vec<&str> = obj.tag.iter().map(|t| t.name.as_str()).collect();
@@ -68,12 +71,15 @@ fn review_to_ap_object_has_public_addressing() {
let actor_url: url::Url = "https://example.com/users/abc".parse().unwrap();
let obj = review_to_ap_object(
&review,
"https://example.com/reviews/1".parse().unwrap(),
actor_url.clone(),
"Dune".to_string(),
2021,
None,
"https://example.com",
ReviewApInput {
ap_id: "https://example.com/reviews/1".parse().unwrap(),
actor_url: actor_url.clone(),
movie_title: "Dune".to_string(),
release_year: 2021,
external_metadata_id: None,
poster_url: None,
base_url: "https://example.com".to_string(),
},
);
assert_eq!(obj.to, vec!["https://www.w3.org/ns/activitystreams#Public"]);
assert_eq!(obj.cc, vec!["https://example.com/users/abc/followers"]);

View File

@@ -1,14 +1,17 @@
use std::sync::Arc;
use async_trait::async_trait;
use chrono::DateTime;
use domain::{
models::RemoteWatchlistEntry,
models::{RemoteWatchlistEntry, WatchlistWithMovie},
ports::{LocalApContentQuery, RemoteWatchlistRepository},
value_objects::UserId,
};
use k_ap::ApObjectHandler;
use k_ap::{ApContentReader, ApObjectHandler};
use url::Url;
use crate::objects::WatchlistObject;
use crate::objects::{WatchlistApInput, WatchlistObject, watchlist_to_ap_object};
use crate::urls::{actor_url, watchlist_entry_url};
pub struct WatchlistObjectHandler {
pub remote_watchlist_repo: Arc<dyn RemoteWatchlistRepository>,
@@ -16,6 +19,51 @@ pub struct WatchlistObjectHandler {
pub base_url: String,
}
#[async_trait]
impl ApContentReader for WatchlistObjectHandler {
async fn get_local_objects_page(
&self,
user_id: uuid::Uuid,
_before: Option<DateTime<chrono::Utc>>,
_limit: usize,
) -> anyhow::Result<Vec<(Url, serde_json::Value, DateTime<chrono::Utc>)>> {
let uid = UserId::from_uuid(user_id);
let entries = self
.content_query
.get_local_watchlist_for_user(&uid)
.await
.map_err(|e| anyhow::anyhow!(e.to_string()))?;
let actor = actor_url(&self.base_url, user_id);
let mut results = Vec::new();
for WatchlistWithMovie { entry, movie } in entries {
let ap_id = watchlist_entry_url(&self.base_url, user_id, entry.movie_id.value());
let published = DateTime::from_naive_utc_and_offset(entry.added_at, chrono::Utc);
let poster_url = movie
.poster_path()
.map(|p| format!("{}/images/{}", self.base_url, p.value()));
let obj = watchlist_to_ap_object(WatchlistApInput {
ap_id: ap_id.clone(),
actor_url: actor.clone(),
movie_title: movie.title().value().to_string(),
release_year: movie.release_year().value(),
external_metadata_id: movie
.external_metadata_id()
.map(|id| id.value().to_string()),
poster_url,
added_at: published,
base_url: self.base_url.clone(),
});
results.push((ap_id, serde_json::to_value(obj)?, published));
}
Ok(results)
}
async fn count_local_posts(&self) -> anyhow::Result<u64> {
Ok(0)
}
}
#[async_trait]
impl ApObjectHandler for WatchlistObjectHandler {
async fn on_create(
@@ -49,10 +97,32 @@ impl ApObjectHandler for WatchlistObjectHandler {
async fn on_update(
&self,
_ap_id: &Url,
_actor_url: &Url,
_object: serde_json::Value,
ap_id: &Url,
actor_url: &Url,
object: serde_json::Value,
) -> anyhow::Result<()> {
let mut obj: WatchlistObject = match serde_json::from_value(object) {
Ok(o) => o,
Err(e) => {
tracing::warn!(ap_id = %ap_id, "ignoring malformed watchlist Update: {}", e);
return Ok(());
}
};
if obj.attributed_to != *actor_url {
anyhow::bail!("watchlist Update actor does not match object attributed_to");
}
obj.movie_title = ammonia::clean(&obj.movie_title);
let entry = RemoteWatchlistEntry {
ap_id: ap_id.as_str().to_string(),
actor_url: actor_url.as_str().to_string(),
movie_title: obj.movie_title,
release_year: obj.release_year,
external_metadata_id: obj.external_metadata_id,
poster_url: obj.poster_url,
added_at: obj.published,
};
self.remote_watchlist_repo.save(entry).await?;
tracing::info!(ap_id = %ap_id, "updated remote watchlist entry");
Ok(())
}
@@ -71,36 +141,19 @@ impl ApObjectHandler for WatchlistObjectHandler {
Ok(())
}
async fn on_like(&self, _object_url: &Url, _actor_url: &Url) -> anyhow::Result<()> {
async fn on_like(&self, _: &Url, _: &Url) -> anyhow::Result<()> {
Ok(())
}
async fn on_announce_received(
&self,
_object_url: &Url,
_actor_url: &Url,
) -> anyhow::Result<()> {
async fn on_announce_received(&self, _: &Url, _: &Url) -> anyhow::Result<()> {
Ok(())
}
async fn on_announce_of_remote(
&self,
_object_url: &Url,
_actor_url: &Url,
) -> anyhow::Result<()> {
async fn on_announce_of_remote(&self, _: &Url, _: &Url) -> anyhow::Result<()> {
Ok(())
}
async fn on_unlike(&self, _object_url: &Url, _actor_url: &Url) -> anyhow::Result<()> {
async fn on_unlike(&self, _: &Url, _: &Url) -> anyhow::Result<()> {
Ok(())
}
async fn on_mention(
&self,
_thought_ap_id: &Url,
_mentioned_user_uuid: uuid::Uuid,
_actor_url: &Url,
) -> anyhow::Result<()> {
async fn on_mention(&self, _: &Url, _: uuid::Uuid, _: &Url) -> anyhow::Result<()> {
Ok(())
}
}