synheart_sensor_agent/flux/
adapter.rs1use crate::collector::types::{KeyboardEvent, MouseEvent, MouseEventType, ShortcutEvent};
7use crate::core::windowing::EventWindow;
8use synheart_flux::behavior::types::{ScrollEvent, TapEvent, TypingEvent};
9use synheart_flux::behavior::{BehaviorEvent, BehaviorEventType, BehaviorSession};
10
11pub struct SensorBehaviorAdapter {
13 device_id: String,
14 timezone: String,
15}
16
17impl SensorBehaviorAdapter {
18 pub fn new(device_id: String, timezone: String) -> Self {
20 Self {
21 device_id,
22 timezone,
23 }
24 }
25
26 pub fn with_defaults() -> Self {
28 Self {
29 device_id: format!("sensor-{}", uuid::Uuid::new_v4()),
30 timezone: "UTC".to_string(),
31 }
32 }
33
34 pub fn convert(&self, session_id: &str, window: &EventWindow) -> BehaviorSession {
36 let mut events = Vec::new();
37
38 for kb_event in &window.keyboard_events {
40 if kb_event.is_key_down {
41 events.push(self.keyboard_to_behavior(kb_event));
42 }
43 }
44
45 for shortcut_event in &window.shortcut_events {
47 events.push(self.shortcut_to_behavior(shortcut_event));
48 }
49
50 for mouse_event in &window.mouse_events {
52 if let Some(behavior_event) = self.mouse_to_behavior(mouse_event) {
53 events.push(behavior_event);
54 }
55 }
56
57 events.sort_by_key(|e| e.timestamp);
59
60 BehaviorSession {
61 session_id: session_id.to_string(),
62 device_id: self.device_id.clone(),
63 timezone: self.timezone.clone(),
64 start_time: window.start,
65 end_time: window.end,
66 events,
67 }
68 }
69
70 fn keyboard_to_behavior(&self, kb: &KeyboardEvent) -> BehaviorEvent {
72 BehaviorEvent {
73 timestamp: kb.timestamp,
74 event_type: BehaviorEventType::Typing,
75 scroll: None,
76 tap: None,
77 swipe: None,
78 interruption: None,
79 typing: Some(TypingEvent {
80 typing_speed_cpm: None, cadence_stability: None,
82 duration_sec: None,
83 pause_count: None,
84 start_at: None,
85 end_at: None,
86 typing_tap_count: None,
87 mean_inter_tap_interval_ms: None,
88 typing_cadence_variability: None,
89 typing_cadence_stability: None,
90 typing_gap_count: None,
91 typing_gap_ratio: None,
92 typing_burstiness: None,
93 typing_activity_ratio: None,
94 typing_interaction_intensity: None,
95 deep_typing: None,
96 number_of_backspace: None,
97 number_of_delete: None,
98 number_of_cut: None,
99 number_of_paste: None,
100 number_of_copy: None,
101 }),
102 app_switch: None,
103 }
104 }
105
106 fn shortcut_to_behavior(&self, shortcut: &ShortcutEvent) -> BehaviorEvent {
108 BehaviorEvent {
109 timestamp: shortcut.timestamp,
110 event_type: BehaviorEventType::Typing,
111 scroll: None,
112 tap: None,
113 swipe: None,
114 interruption: None,
115 typing: Some(TypingEvent {
116 typing_speed_cpm: None,
117 cadence_stability: None,
118 duration_sec: None,
119 pause_count: None,
120 start_at: None,
121 end_at: None,
122 typing_tap_count: None,
123 mean_inter_tap_interval_ms: None,
124 typing_cadence_variability: None,
125 typing_cadence_stability: None,
126 typing_gap_count: None,
127 typing_gap_ratio: None,
128 typing_burstiness: None,
129 typing_activity_ratio: None,
130 typing_interaction_intensity: None,
131 deep_typing: None,
132 number_of_backspace: None,
133 number_of_delete: None,
134 number_of_cut: None,
135 number_of_paste: None,
136 number_of_copy: None,
137 }),
138 app_switch: None,
139 }
140 }
141
142 fn mouse_to_behavior(&self, mouse: &MouseEvent) -> Option<BehaviorEvent> {
144 match mouse.event_type {
145 MouseEventType::Move => {
146 Some(BehaviorEvent {
149 timestamp: mouse.timestamp,
150 event_type: BehaviorEventType::Scroll,
151 scroll: Some(ScrollEvent {
152 velocity: mouse.delta_magnitude,
153 direction: None, direction_reversal: false,
155 }),
156 tap: None,
157 swipe: None,
158 interruption: None,
159 typing: None,
160 app_switch: None,
161 })
162 }
163 MouseEventType::LeftClick | MouseEventType::RightClick => {
164 Some(BehaviorEvent {
165 timestamp: mouse.timestamp,
166 event_type: BehaviorEventType::Tap,
167 scroll: None,
168 tap: Some(TapEvent {
169 tap_duration_ms: Some(100), long_press: false,
171 }),
172 swipe: None,
173 interruption: None,
174 typing: None,
175 app_switch: None,
176 })
177 }
178 MouseEventType::Scroll => {
179 Some(BehaviorEvent {
180 timestamp: mouse.timestamp,
181 event_type: BehaviorEventType::Scroll,
182 scroll: Some(ScrollEvent {
183 velocity: mouse.delta_magnitude,
184 direction: None, direction_reversal: false,
186 }),
187 tap: None,
188 swipe: None,
189 interruption: None,
190 typing: None,
191 app_switch: None,
192 })
193 }
194 }
195 }
196}
197
198pub fn convert_to_behavior_session(
200 session_id: &str,
201 window: &EventWindow,
202 device_id: Option<&str>,
203) -> BehaviorSession {
204 let adapter = match device_id {
205 Some(id) => SensorBehaviorAdapter::new(id.to_string(), "UTC".to_string()),
206 None => SensorBehaviorAdapter::with_defaults(),
207 };
208 adapter.convert(session_id, window)
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214 use chrono::{Duration, Utc};
215
216 #[test]
217 fn test_adapter_creation() {
218 let adapter = SensorBehaviorAdapter::with_defaults();
219 assert!(adapter.device_id.starts_with("sensor-"));
220 }
221
222 #[test]
223 fn test_empty_window_conversion() {
224 let adapter = SensorBehaviorAdapter::with_defaults();
225 let window = EventWindow::new(Utc::now(), Duration::seconds(10));
226 let session = adapter.convert("test-session", &window);
227
228 assert_eq!(session.session_id, "test-session");
229 assert!(session.events.is_empty());
230 }
231}