Skip to main content

cloudillo_action/native_hooks/
react.rs

1//! REACT (Reaction) action native hooks
2//!
3//! Handles reaction lifecycle:
4//! - on_create: Updates subject action's reaction count for local reactions
5//! - on_receive: Updates subject action's reaction count for incoming reactions
6//!
7//! Note: REACT uses `subject` field to reference the action being reacted to,
8//! NOT `parent`. This is because reactions don't create visible hierarchy.
9
10use crate::hooks::{HookContext, HookResult};
11use crate::prelude::*;
12use cloudillo_core::app::App;
13use cloudillo_types::meta_adapter::UpdateActionDataOptions;
14use cloudillo_types::types::Patch;
15
16/// REACT on_create hook - Handle local reaction creation
17///
18/// Updates the subject action's reaction count:
19/// - Non-DEL subtypes (LIKE, LOVE, etc.): increment by 1
20/// - DEL subtype: decrement by 1
21pub async fn on_create(app: App, context: HookContext) -> ClResult<HookResult> {
22	tracing::debug!("Native hook: REACT on_create for action {}", context.action_id);
23
24	let tn_id = TnId(context.tenant_id as u32);
25	let Some(subject_id) = &context.subject else {
26		tracing::warn!("REACT on_create: No subject specified");
27		return Ok(HookResult::default());
28	};
29
30	// Get current subject action data
31	let subject_data = app.meta_adapter.get_action_data(tn_id, subject_id).await?;
32	let current_reactions = subject_data.as_ref().and_then(|d| d.reactions).unwrap_or(0);
33
34	let new_reactions = match context.subtype.as_deref() {
35		Some("DEL") => {
36			// Remove reaction: decrement (minimum 0)
37			tracing::info!(
38				"REACT:DEL on_create: {} removing reaction from {}",
39				context.issuer,
40				subject_id
41			);
42			current_reactions.saturating_sub(1)
43		}
44		_ => {
45			// Add reaction: increment
46			tracing::info!(
47				"REACT:{:?} on_create: {} reacting to {}",
48				context.subtype,
49				context.issuer,
50				subject_id
51			);
52			current_reactions.saturating_add(1)
53		}
54	};
55
56	// Update subject action's reaction count
57	let update_opts =
58		UpdateActionDataOptions { reactions: Patch::Value(new_reactions), ..Default::default() };
59
60	if let Err(e) = app.meta_adapter.update_action_data(tn_id, subject_id, &update_opts).await {
61		tracing::warn!("REACT on_create: Failed to update subject {} reactions: {}", subject_id, e);
62	} else {
63		tracing::debug!(
64			"REACT on_create: Updated subject {} reactions: {} -> {}",
65			subject_id,
66			current_reactions,
67			new_reactions
68		);
69	}
70
71	Ok(HookResult::default())
72}
73
74/// REACT on_receive hook - Handle incoming reaction
75///
76/// Updates the subject action's reaction count if we own the subject:
77/// - Non-DEL subtypes (LIKE, LOVE, etc.): increment by 1
78/// - DEL subtype: decrement by 1
79pub async fn on_receive(app: App, context: HookContext) -> ClResult<HookResult> {
80	tracing::debug!("Native hook: REACT on_receive for action {}", context.action_id);
81
82	let tn_id = TnId(context.tenant_id as u32);
83	let Some(subject_id) = &context.subject else {
84		tracing::warn!("REACT on_receive: No subject specified");
85		return Ok(HookResult::default());
86	};
87
88	// Get subject action to check ownership
89	let Some(subject_action) = app.meta_adapter.get_action(tn_id, subject_id).await? else {
90		tracing::debug!("REACT on_receive: Subject action {} not found locally", subject_id);
91		return Ok(HookResult::default());
92	};
93
94	// Only update if we own the subject action
95	if subject_action.issuer.id_tag.as_ref() != context.tenant_tag {
96		tracing::debug!(
97			"REACT on_receive: Subject {} owned by {}, not us ({})",
98			subject_id,
99			subject_action.issuer.id_tag,
100			context.tenant_tag
101		);
102		return Ok(HookResult::default());
103	}
104
105	// Get current reaction count
106	let subject_data = app.meta_adapter.get_action_data(tn_id, subject_id).await?;
107	let current_reactions = subject_data.as_ref().and_then(|d| d.reactions).unwrap_or(0);
108
109	let new_reactions = match context.subtype.as_deref() {
110		Some("DEL") => {
111			// Remove reaction: decrement (minimum 0)
112			tracing::info!(
113				"REACT:DEL on_receive: {} removing reaction from our action {}",
114				context.issuer,
115				subject_id
116			);
117			current_reactions.saturating_sub(1)
118		}
119		_ => {
120			// Add reaction: increment
121			tracing::info!(
122				"REACT:{:?} on_receive: {} reacting to our action {}",
123				context.subtype,
124				context.issuer,
125				subject_id
126			);
127			current_reactions.saturating_add(1)
128		}
129	};
130
131	// Update subject action's reaction count
132	let update_opts =
133		UpdateActionDataOptions { reactions: Patch::Value(new_reactions), ..Default::default() };
134
135	if let Err(e) = app.meta_adapter.update_action_data(tn_id, subject_id, &update_opts).await {
136		tracing::warn!(
137			"REACT on_receive: Failed to update subject {} reactions: {}",
138			subject_id,
139			e
140		);
141	} else {
142		tracing::debug!(
143			"REACT on_receive: Updated subject {} reactions: {} -> {}",
144			subject_id,
145			current_reactions,
146			new_reactions
147		);
148	}
149
150	Ok(HookResult::default())
151}
152
153// vim: ts=4