Files
movies-diary/crates/adapters/activitypub/src/goal_handler.rs
Gabriel Kaszewski 6a9b4e5c00 refactor: LOW+MEDIUM cleanups — CQRS DiaryQuery/GoalCommand+GoalQuery,
test-helpers out of production, decompose get_user_profile_html,
dedup secure_flag, remove vestigial social_query, drop
PersistedImportSession, LoginCommand rename, DeleteAccountDeps,
PersonId macro, ReviewSortBy rename, TUI Command for fs::read,
generic PaginatedResponse<T>, noop ports for production fallbacks
2026-07-10 05:29:02 +02:00

142 lines
4.3 KiB
Rust

use std::sync::Arc;
use async_trait::async_trait;
use chrono::DateTime;
use domain::{
models::RemoteGoalEntry,
ports::{GoalQuery, RemoteGoalRepository},
value_objects::UserId,
};
use k_ap::{ApContentReader, ApObjectHandler};
use url::Url;
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 goal_repo: Arc<dyn GoalQuery>,
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
.goal_repo
.list_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]
impl ApObjectHandler for GoalObjectHandler {
async fn on_create(
&self,
ap_id: &Url,
actor_url: &Url,
object: serde_json::Value,
) -> anyhow::Result<()> {
let obj: GoalObject = match serde_json::from_value(object) {
Ok(o) => o,
Err(e) => {
tracing::warn!(ap_id = %ap_id, "ignoring malformed goal Create: {}", e);
return Ok(());
}
};
let entry = RemoteGoalEntry {
ap_id: ap_id.as_str().to_string(),
actor_url: actor_url.as_str().to_string(),
year: obj.goal_year,
target_count: obj.goal_target,
current_count: obj.goal_current,
received_at: chrono::Utc::now(),
};
self.remote_goal_repo.save(entry).await?;
tracing::info!(ap_id = %ap_id, year = obj.goal_year, "saved remote goal");
Ok(())
}
async fn on_update(
&self,
ap_id: &Url,
actor_url: &Url,
object: serde_json::Value,
) -> anyhow::Result<()> {
let obj: GoalObject = match serde_json::from_value(object) {
Ok(o) => o,
Err(e) => {
tracing::warn!(ap_id = %ap_id, "ignoring malformed goal Update: {}", e);
return Ok(());
}
};
if obj.attributed_to != *actor_url {
anyhow::bail!("goal Update actor does not match object attributed_to");
}
self.remote_goal_repo
.update_by_ap_id(ap_id.as_str(), obj.goal_target, obj.goal_current)
.await?;
tracing::info!(ap_id = %ap_id, "updated remote goal progress");
Ok(())
}
async fn on_delete(&self, ap_id: &Url, actor_url: &Url) -> anyhow::Result<()> {
self.remote_goal_repo
.remove_by_ap_id(ap_id.as_str(), actor_url.as_str())
.await?;
tracing::info!(ap_id = %ap_id, "removed remote goal");
Ok(())
}
async fn on_actor_removed(&self, actor_url: &Url) -> anyhow::Result<()> {
self.remote_goal_repo
.remove_all_by_actor(actor_url.as_str())
.await?;
Ok(())
}
async fn on_like(&self, _: &Url, _: &Url) -> anyhow::Result<()> {
Ok(())
}
async fn on_announce_received(&self, _: &Url, _: &Url) -> anyhow::Result<()> {
Ok(())
}
async fn on_announce_of_remote(&self, _: &Url, _: &Url) -> anyhow::Result<()> {
Ok(())
}
async fn on_unlike(&self, _: &Url, _: &Url) -> anyhow::Result<()> {
Ok(())
}
async fn on_mention(&self, _: &Url, _: uuid::Uuid, _: &Url) -> anyhow::Result<()> {
Ok(())
}
}