1use crate::entity;
4use chrono::Utc;
5use sova_core::{ClientAddr, Request};
6use sova_db::{DbExt, DbHandle};
7use sea_orm::{ActiveModelTrait, Set};
8use serde_json::Value;
9
10#[derive(Debug, Clone)]
12pub struct ActivityEntry {
13 pub actor_id: Option<i64>,
14 pub subject_type: String,
15 pub subject_id: String,
16 pub event: String,
17 pub properties: Value,
18 pub ip: Option<String>,
19 pub user_agent: Option<String>,
20}
21
22#[derive(Clone, Copy, Debug)]
24pub struct ActivityActor(pub i64);
25
26#[derive(Clone, Debug, Default)]
28pub struct ActivityLog;
29
30impl ActivityLog {
31 pub async fn record(db: &DbHandle, entry: ActivityEntry) {
33 let props = serde_json::to_string(&entry.properties).unwrap_or_else(|_| "{}".into());
34 let model = entity::ActiveModel {
35 actor_id: Set(entry.actor_id),
36 subject_type: Set(entry.subject_type),
37 subject_id: Set(entry.subject_id),
38 event: Set(entry.event),
39 properties: Set(props),
40 ip: Set(entry.ip),
41 user_agent: Set(entry.user_agent),
42 created_at: Set(Utc::now()),
43 ..Default::default()
44 };
45 if let Err(e) = model.insert(db).await {
46 tracing::warn!(error = %e, "activity_log insert failed");
47 }
48 }
49}
50
51pub trait ActivityExt {
53 fn activity_enabled(&self) -> bool;
54
55 #[allow(async_fn_in_trait)]
56 async fn log_activity(
57 &self,
58 event: &str,
59 subject_type: &str,
60 subject_id: impl ToString,
61 properties: Value,
62 );
63
64 #[allow(async_fn_in_trait)]
65 async fn log_activity_as(
66 &self,
67 actor_id: Option<i64>,
68 event: &str,
69 subject_type: &str,
70 subject_id: impl ToString,
71 properties: Value,
72 );
73}
74
75impl ActivityExt for Request {
76 fn activity_enabled(&self) -> bool {
77 self.try_state::<ActivityLog>().is_some()
78 }
79
80 async fn log_activity(
81 &self,
82 event: &str,
83 subject_type: &str,
84 subject_id: impl ToString,
85 properties: Value,
86 ) {
87 let actor = self.get::<ActivityActor>().map(|a| a.0);
88 self.log_activity_as(actor, event, subject_type, subject_id, properties)
89 .await;
90 }
91
92 async fn log_activity_as(
93 &self,
94 actor_id: Option<i64>,
95 event: &str,
96 subject_type: &str,
97 subject_id: impl ToString,
98 properties: Value,
99 ) {
100 if !self.activity_enabled() {
101 return;
102 }
103 let ip = self
104 .get::<ClientAddr>()
105 .map(|a| a.0.ip().to_string())
106 .or_else(|| {
107 self.header("x-forwarded-for")
108 .and_then(|v| v.split(',').next().map(|s| s.trim().to_string()))
109 });
110 let user_agent = self.header("user-agent").map(str::to_string);
111 ActivityLog::record(
112 self.db(),
113 ActivityEntry {
114 actor_id,
115 subject_type: subject_type.into(),
116 subject_id: subject_id.to_string(),
117 event: event.into(),
118 properties,
119 ip,
120 user_agent,
121 },
122 )
123 .await;
124 }
125}