rustyclaw_core/messengers/
group_chat.rs1use serde::{Deserialize, Serialize};
12use tracing::debug;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(rename_all = "snake_case")]
17pub enum ActivationMode {
18 Always,
20 Mention,
22 Prefix,
24 Never,
26}
27
28impl Default for ActivationMode {
29 fn default() -> Self {
30 Self::Mention
31 }
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(rename_all = "snake_case")]
37pub enum IsolationMode {
38 PerGroup,
40 Shared,
42 PerUser,
44}
45
46impl Default for IsolationMode {
47 fn default() -> Self {
48 Self::PerGroup
49 }
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct GroupChatConfig {
55 #[serde(default)]
57 pub enabled: bool,
58
59 #[serde(default)]
61 pub activation: ActivationMode,
62
63 #[serde(default = "default_prefix")]
65 pub prefix: String,
66
67 #[serde(default)]
69 pub isolation: IsolationMode,
70
71 #[serde(default = "default_max_context")]
73 pub max_context_messages: usize,
74
75 #[serde(default)]
77 pub allowed_groups: Vec<String>,
78
79 #[serde(default)]
81 pub blocked_groups: Vec<String>,
82
83 #[serde(default = "default_true")]
86 pub include_sender_names: bool,
87}
88
89fn default_prefix() -> String {
90 "!claw".to_string()
91}
92
93fn default_max_context() -> usize {
94 50
95}
96
97fn default_true() -> bool {
98 true
99}
100
101impl Default for GroupChatConfig {
102 fn default() -> Self {
103 Self {
104 enabled: false,
105 activation: ActivationMode::default(),
106 prefix: default_prefix(),
107 isolation: IsolationMode::default(),
108 max_context_messages: default_max_context(),
109 allowed_groups: Vec::new(),
110 blocked_groups: Vec::new(),
111 include_sender_names: true,
112 }
113 }
114}
115
116impl GroupChatConfig {
117 pub fn is_group_allowed(&self, group_id: &str) -> bool {
119 if self.blocked_groups.contains(&group_id.to_string()) {
120 return false;
121 }
122 if self.allowed_groups.is_empty() {
123 return true;
124 }
125 self.allowed_groups.contains(&group_id.to_string())
126 }
127
128 pub fn should_respond(&self, message: &str, agent_name: &str) -> bool {
130 if !self.enabled {
131 return false;
132 }
133
134 match self.activation {
135 ActivationMode::Always => true,
136 ActivationMode::Never => false,
137 ActivationMode::Mention => {
138 let lower = message.to_lowercase();
139 let name_lower = agent_name.to_lowercase();
140 lower.contains(&name_lower)
141 || lower.contains(&format!("@{}", name_lower))
142 }
143 ActivationMode::Prefix => {
144 message.starts_with(&self.prefix)
145 }
146 }
147 }
148
149 pub fn session_key(&self, group_id: &str, user_id: Option<&str>) -> String {
151 match self.isolation {
152 IsolationMode::PerGroup => format!("group:{}", group_id),
153 IsolationMode::Shared => "shared".to_string(),
154 IsolationMode::PerUser => {
155 if let Some(uid) = user_id {
156 format!("group:{}:user:{}", group_id, uid)
157 } else {
158 format!("group:{}", group_id)
159 }
160 }
161 }
162 }
163
164 pub fn strip_prefix<'a>(&self, message: &'a str) -> &'a str {
166 if self.activation == ActivationMode::Prefix {
167 message
168 .strip_prefix(&self.prefix)
169 .map(|s| s.trim_start())
170 .unwrap_or(message)
171 } else {
172 message
173 }
174 }
175}
176
177pub fn format_group_message(sender_name: &str, message: &str, include_sender: bool) -> String {
179 if include_sender {
180 format!("[{}]: {}", sender_name, message)
181 } else {
182 message.to_string()
183 }
184}
185
186pub fn group_context_key(messenger_name: &str, group_id: &str) -> String {
188 debug!(messenger = %messenger_name, group = %group_id, "Generating group context key");
189 format!("{}:{}", messenger_name, group_id)
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195
196 #[test]
197 fn test_default_config() {
198 let config = GroupChatConfig::default();
199 assert!(!config.enabled);
200 assert_eq!(config.activation, ActivationMode::Mention);
201 assert_eq!(config.isolation, IsolationMode::PerGroup);
202 assert_eq!(config.prefix, "!claw");
203 assert!(config.include_sender_names);
204 }
205
206 #[test]
207 fn test_should_respond_mention() {
208 let config = GroupChatConfig {
209 enabled: true,
210 activation: ActivationMode::Mention,
211 ..Default::default()
212 };
213
214 assert!(config.should_respond("Hey @rustyclaw help me", "RustyClaw"));
215 assert!(config.should_respond("rustyclaw what do you think?", "RustyClaw"));
216 assert!(!config.should_respond("Just chatting with friends", "RustyClaw"));
217 }
218
219 #[test]
220 fn test_should_respond_prefix() {
221 let config = GroupChatConfig {
222 enabled: true,
223 activation: ActivationMode::Prefix,
224 prefix: "!claw".to_string(),
225 ..Default::default()
226 };
227
228 assert!(config.should_respond("!claw help me", "RustyClaw"));
229 assert!(!config.should_respond("Hey rustyclaw", "RustyClaw"));
230 }
231
232 #[test]
233 fn test_should_respond_always() {
234 let config = GroupChatConfig {
235 enabled: true,
236 activation: ActivationMode::Always,
237 ..Default::default()
238 };
239
240 assert!(config.should_respond("anything at all", "RustyClaw"));
241 }
242
243 #[test]
244 fn test_should_respond_never() {
245 let config = GroupChatConfig {
246 enabled: true,
247 activation: ActivationMode::Never,
248 ..Default::default()
249 };
250
251 assert!(!config.should_respond("@rustyclaw please", "RustyClaw"));
252 }
253
254 #[test]
255 fn test_should_respond_disabled() {
256 let config = GroupChatConfig {
257 enabled: false,
258 activation: ActivationMode::Always,
259 ..Default::default()
260 };
261
262 assert!(!config.should_respond("anything", "RustyClaw"));
263 }
264
265 #[test]
266 fn test_group_allowed() {
267 let config = GroupChatConfig {
268 allowed_groups: vec!["group1".to_string(), "group2".to_string()],
269 ..Default::default()
270 };
271
272 assert!(config.is_group_allowed("group1"));
273 assert!(!config.is_group_allowed("group3"));
274 }
275
276 #[test]
277 fn test_group_blocked() {
278 let config = GroupChatConfig {
279 blocked_groups: vec!["spam".to_string()],
280 ..Default::default()
281 };
282
283 assert!(!config.is_group_allowed("spam"));
284 assert!(config.is_group_allowed("general"));
285 }
286
287 #[test]
288 fn test_session_key_per_group() {
289 let config = GroupChatConfig {
290 isolation: IsolationMode::PerGroup,
291 ..Default::default()
292 };
293 assert_eq!(config.session_key("g123", Some("u456")), "group:g123");
294 }
295
296 #[test]
297 fn test_session_key_per_user() {
298 let config = GroupChatConfig {
299 isolation: IsolationMode::PerUser,
300 ..Default::default()
301 };
302 assert_eq!(
303 config.session_key("g123", Some("u456")),
304 "group:g123:user:u456"
305 );
306 }
307
308 #[test]
309 fn test_session_key_shared() {
310 let config = GroupChatConfig {
311 isolation: IsolationMode::Shared,
312 ..Default::default()
313 };
314 assert_eq!(config.session_key("g123", Some("u456")), "shared");
315 }
316
317 #[test]
318 fn test_strip_prefix() {
319 let config = GroupChatConfig {
320 activation: ActivationMode::Prefix,
321 prefix: "!claw".to_string(),
322 ..Default::default()
323 };
324 assert_eq!(config.strip_prefix("!claw help me"), "help me");
325 assert_eq!(config.strip_prefix("no prefix"), "no prefix");
326 }
327
328 #[test]
329 fn test_format_group_message() {
330 assert_eq!(
331 format_group_message("Alice", "Hello!", true),
332 "[Alice]: Hello!"
333 );
334 assert_eq!(format_group_message("Alice", "Hello!", false), "Hello!");
335 }
336}