Some checks failed
CI / Check / Test (push) Has been cancelled
- Broadcast GoalUpdated AP note after ReviewLogged so federated goal progress reflects the new review count without requiring a manual goal edit - Add attribution check in GoalObjectHandler::on_update (mirrors review_handler) to prevent any remote actor from overwriting another's goal - Implement on_actor_removed in GoalObjectHandler via new RemoteGoalRepository::remove_all_by_actor — remote goals were never cleaned up when an actor unfollowed or was deleted - Add remove_all_by_actor to SQLite, Postgres, Noop, and test Panic impls
115 lines
3.7 KiB
Rust
115 lines
3.7 KiB
Rust
use async_trait::async_trait;
|
|
use chrono::TimeZone;
|
|
use domain::{errors::DomainError, models::RemoteGoalEntry, ports::RemoteGoalRepository};
|
|
use sqlx::{Row, SqlitePool};
|
|
|
|
pub struct SqliteRemoteGoalRepository {
|
|
pool: SqlitePool,
|
|
}
|
|
|
|
impl SqliteRemoteGoalRepository {
|
|
pub fn new(pool: SqlitePool) -> Self {
|
|
Self { pool }
|
|
}
|
|
|
|
fn map_err(e: sqlx::Error) -> DomainError {
|
|
tracing::error!("Database error: {:?}", e);
|
|
DomainError::InfrastructureError("Database operation failed".into())
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl RemoteGoalRepository for SqliteRemoteGoalRepository {
|
|
async fn save(&self, entry: RemoteGoalEntry) -> Result<(), DomainError> {
|
|
let received = entry.received_at.format("%Y-%m-%d %H:%M:%S").to_string();
|
|
|
|
sqlx::query(
|
|
"INSERT OR REPLACE INTO remote_goals \
|
|
(ap_id, actor_url, year, target_count, current_count, received_at) \
|
|
VALUES (?, ?, ?, ?, ?, ?)",
|
|
)
|
|
.bind(&entry.ap_id)
|
|
.bind(&entry.actor_url)
|
|
.bind(entry.year as i64)
|
|
.bind(entry.target_count as i64)
|
|
.bind(entry.current_count as i64)
|
|
.bind(&received)
|
|
.execute(&self.pool)
|
|
.await
|
|
.map_err(Self::map_err)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn update_by_ap_id(
|
|
&self,
|
|
ap_id: &str,
|
|
target: u32,
|
|
current: u32,
|
|
) -> Result<(), DomainError> {
|
|
sqlx::query("UPDATE remote_goals SET target_count = ?, current_count = ? WHERE ap_id = ?")
|
|
.bind(target as i64)
|
|
.bind(current as i64)
|
|
.bind(ap_id)
|
|
.execute(&self.pool)
|
|
.await
|
|
.map_err(Self::map_err)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn remove_by_ap_id(&self, ap_id: &str, actor_url: &str) -> Result<(), DomainError> {
|
|
sqlx::query("DELETE FROM remote_goals WHERE ap_id = ? AND actor_url = ?")
|
|
.bind(ap_id)
|
|
.bind(actor_url)
|
|
.execute(&self.pool)
|
|
.await
|
|
.map_err(Self::map_err)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn remove_all_by_actor(&self, actor_url: &str) -> Result<(), DomainError> {
|
|
sqlx::query("DELETE FROM remote_goals WHERE actor_url = ?")
|
|
.bind(actor_url)
|
|
.execute(&self.pool)
|
|
.await
|
|
.map_err(Self::map_err)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn get_by_actor_url(&self, actor_url: &str) -> Result<Vec<RemoteGoalEntry>, DomainError> {
|
|
let rows = sqlx::query(
|
|
"SELECT ap_id, actor_url, year, target_count, current_count, received_at \
|
|
FROM remote_goals WHERE actor_url = ? ORDER BY year DESC",
|
|
)
|
|
.bind(actor_url)
|
|
.fetch_all(&self.pool)
|
|
.await
|
|
.map_err(Self::map_err)?;
|
|
|
|
rows.iter()
|
|
.map(|r| {
|
|
let year: i64 = r.try_get("year").unwrap_or(0);
|
|
let target: i64 = r.try_get("target_count").unwrap_or(0);
|
|
let current: i64 = r.try_get("current_count").unwrap_or(0);
|
|
let received_str: String = r.try_get("received_at").unwrap_or_default();
|
|
let received_at =
|
|
chrono::NaiveDateTime::parse_from_str(&received_str, "%Y-%m-%d %H:%M:%S")
|
|
.map(|ndt| chrono::Utc.from_utc_datetime(&ndt))
|
|
.unwrap_or_else(|_| chrono::Utc::now());
|
|
|
|
Ok(RemoteGoalEntry {
|
|
ap_id: r.try_get("ap_id").unwrap_or_default(),
|
|
actor_url: r.try_get("actor_url").unwrap_or_default(),
|
|
year: year as u16,
|
|
target_count: target as u32,
|
|
current_count: current as u32,
|
|
received_at,
|
|
})
|
|
})
|
|
.collect()
|
|
}
|
|
}
|