1use serde::{Deserialize, Serialize};
33
34#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
46pub struct GroupActivation {
47 pub group_id: String,
49 #[serde(default = "default_true")]
52 pub enabled: bool,
53 #[serde(default)]
56 pub require_mention: bool,
57 #[serde(default)]
60 pub cooldown_seconds: Option<u64>,
61 #[serde(default)]
64 pub whitelist: Option<Vec<String>>,
65}
66
67fn default_true() -> bool {
68 true
69}
70
71impl GroupActivation {
72 pub fn new(group_id: impl Into<String>) -> Self {
92 Self {
93 group_id: group_id.into(),
94 enabled: true,
95 require_mention: false,
96 cooldown_seconds: None,
97 whitelist: None,
98 }
99 }
100
101 pub fn disabled(group_id: impl Into<String>) -> Self {
118 Self {
119 group_id: group_id.into(),
120 enabled: false,
121 require_mention: false,
122 cooldown_seconds: None,
123 whitelist: None,
124 }
125 }
126
127 pub fn with_enabled(mut self, enabled: bool) -> Self {
144 self.enabled = enabled;
145 self
146 }
147
148 pub fn with_require_mention(mut self, require_mention: bool) -> Self {
165 self.require_mention = require_mention;
166 self
167 }
168
169 pub fn with_cooldown(mut self, seconds: u64) -> Self {
186 self.cooldown_seconds = Some(seconds);
187 self
188 }
189
190 pub fn with_whitelist(mut self, users: Vec<String>) -> Self {
209 self.whitelist = Some(users);
210 self
211 }
212
213 pub fn should_trigger(&self, mentions_bot: bool) -> Result<(), GroupRejectionReason> {
242 if !self.enabled {
244 return Err(GroupRejectionReason::GroupDisabled);
245 }
246
247 if self.require_mention && !mentions_bot {
249 return Err(GroupRejectionReason::RequiresMention);
250 }
251
252 Ok(())
253 }
254
255 pub fn is_user_whitelisted(&self, user_id: &str) -> Option<bool> {
285 self.whitelist
286 .as_ref()
287 .map(|list| list.iter().any(|u| u == user_id))
288 }
289
290 pub fn effective_cooldown(&self, default_cooldown: u64) -> u64 {
316 self.cooldown_seconds.unwrap_or(default_cooldown)
317 }
318
319 pub fn group_id(&self) -> &str {
321 &self.group_id
322 }
323
324 pub fn is_enabled(&self) -> bool {
328 self.enabled
329 }
330
331 pub fn requires_mention(&self) -> bool {
335 self.require_mention
336 }
337
338 pub fn has_custom_cooldown(&self) -> bool {
342 self.cooldown_seconds.is_some()
343 }
344
345 pub fn has_custom_whitelist(&self) -> bool {
349 self.whitelist.is_some()
350 }
351}
352
353#[derive(Debug, Clone, Copy, PartialEq, Eq)]
357pub enum GroupRejectionReason {
358 GroupDisabled,
361 RequiresMention,
364}
365
366impl std::fmt::Display for GroupRejectionReason {
367 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
368 match self {
369 GroupRejectionReason::GroupDisabled => write!(f, "Group has auto-reply disabled"),
370 GroupRejectionReason::RequiresMention => {
371 write!(f, "Group requires @mention to trigger")
372 }
373 }
374 }
375}
376
377impl std::error::Error for GroupRejectionReason {}
378
379#[derive(Debug, Clone, Default)]
383pub struct GroupActivationManager {
384 activations: std::collections::HashMap<String, GroupActivation>,
386}
387
388impl GroupActivationManager {
389 pub fn new() -> Self {
391 Self {
392 activations: std::collections::HashMap::new(),
393 }
394 }
395
396 pub fn from_activations(activations: Vec<GroupActivation>) -> Self {
398 let mut manager = Self::new();
399 for activation in activations {
400 manager.set(activation);
401 }
402 manager
403 }
404
405 pub fn set(&mut self, activation: GroupActivation) {
407 self.activations
408 .insert(activation.group_id.clone(), activation);
409 }
410
411 pub fn get(&self, group_id: &str) -> Option<&GroupActivation> {
413 self.activations.get(group_id)
414 }
415
416 pub fn remove(&mut self, group_id: &str) -> Option<GroupActivation> {
418 self.activations.remove(group_id)
419 }
420
421 pub fn list(&self) -> Vec<&GroupActivation> {
423 self.activations.values().collect()
424 }
425
426 pub fn len(&self) -> usize {
428 self.activations.len()
429 }
430
431 pub fn is_empty(&self) -> bool {
433 self.activations.is_empty()
434 }
435
436 pub fn should_trigger(
440 &self,
441 group_id: &str,
442 mentions_bot: bool,
443 ) -> Result<(), GroupRejectionReason> {
444 match self.get(group_id) {
445 Some(activation) => activation.should_trigger(mentions_bot),
446 None => Ok(()), }
448 }
449
450 pub fn effective_cooldown(&self, group_id: &str, default_cooldown: u64) -> u64 {
454 self.get(group_id)
455 .map(|a| a.effective_cooldown(default_cooldown))
456 .unwrap_or(default_cooldown)
457 }
458
459 pub fn is_user_whitelisted(&self, group_id: &str, user_id: &str) -> Option<bool> {
463 self.get(group_id)
464 .and_then(|a| a.is_user_whitelisted(user_id))
465 }
466}
467
468#[cfg(test)]
469mod tests {
470 use super::*;
471
472 #[test]
478 fn test_new_group_activation() {
479 let activation = GroupActivation::new("group-123");
480
481 assert_eq!(activation.group_id, "group-123");
482 assert!(activation.enabled);
483 assert!(!activation.require_mention);
484 assert!(activation.cooldown_seconds.is_none());
485 assert!(activation.whitelist.is_none());
486 }
487
488 #[test]
491 fn test_disabled_group_activation() {
492 let activation = GroupActivation::disabled("group-123");
493
494 assert_eq!(activation.group_id, "group-123");
495 assert!(!activation.enabled);
496 }
497
498 #[test]
500 fn test_builder_pattern() {
501 let activation = GroupActivation::new("group-123")
502 .with_enabled(true)
503 .with_require_mention(true)
504 .with_cooldown(120)
505 .with_whitelist(vec!["user1".to_string(), "user2".to_string()]);
506
507 assert!(activation.enabled);
508 assert!(activation.require_mention);
509 assert_eq!(activation.cooldown_seconds, Some(120));
510 assert_eq!(activation.whitelist.as_ref().unwrap().len(), 2);
511 }
512
513 #[test]
516 fn test_should_trigger_disabled_group() {
517 let activation = GroupActivation::disabled("group-123");
518
519 assert_eq!(
521 activation.should_trigger(true),
522 Err(GroupRejectionReason::GroupDisabled)
523 );
524 assert_eq!(
525 activation.should_trigger(false),
526 Err(GroupRejectionReason::GroupDisabled)
527 );
528 }
529
530 #[test]
533 fn test_should_trigger_require_mention() {
534 let activation = GroupActivation::new("group-123").with_require_mention(true);
535
536 assert_eq!(
538 activation.should_trigger(false),
539 Err(GroupRejectionReason::RequiresMention)
540 );
541
542 assert_eq!(activation.should_trigger(true), Ok(()));
544 }
545
546 #[test]
548 fn test_should_trigger_no_require_mention() {
549 let activation = GroupActivation::new("group-123");
550
551 assert_eq!(activation.should_trigger(false), Ok(()));
553 assert_eq!(activation.should_trigger(true), Ok(()));
554 }
555
556 #[test]
559 fn test_is_user_whitelisted_no_whitelist() {
560 let activation = GroupActivation::new("group-123");
561
562 assert_eq!(activation.is_user_whitelisted("any_user"), None);
564 }
565
566 #[test]
569 fn test_is_user_whitelisted_with_whitelist() {
570 let activation = GroupActivation::new("group-123")
571 .with_whitelist(vec!["user1".to_string(), "user2".to_string()]);
572
573 assert_eq!(activation.is_user_whitelisted("user1"), Some(true));
575 assert_eq!(activation.is_user_whitelisted("user2"), Some(true));
576
577 assert_eq!(activation.is_user_whitelisted("user3"), Some(false));
579 }
580
581 #[test]
584 fn test_effective_cooldown() {
585 let no_cooldown = GroupActivation::new("group-123");
587 assert_eq!(no_cooldown.effective_cooldown(60), 60);
588
589 let with_cooldown = GroupActivation::new("group-456").with_cooldown(120);
591 assert_eq!(with_cooldown.effective_cooldown(60), 120);
592 }
593
594 #[test]
596 fn test_helper_methods() {
597 let activation = GroupActivation::new("group-123")
598 .with_require_mention(true)
599 .with_cooldown(120)
600 .with_whitelist(vec!["user1".to_string()]);
601
602 assert_eq!(activation.group_id(), "group-123");
603 assert!(activation.is_enabled());
604 assert!(activation.requires_mention());
605 assert!(activation.has_custom_cooldown());
606 assert!(activation.has_custom_whitelist());
607 }
608
609 #[test]
611 fn test_serialization_roundtrip() {
612 let activation = GroupActivation::new("group-123")
613 .with_require_mention(true)
614 .with_cooldown(120)
615 .with_whitelist(vec!["user1".to_string()]);
616
617 let json = serde_json::to_string(&activation).unwrap();
618 let parsed: GroupActivation = serde_json::from_str(&json).unwrap();
619
620 assert_eq!(activation, parsed);
621 }
622
623 #[test]
625 fn test_deserialization_defaults() {
626 let json = r#"{"group_id": "group-123"}"#;
627 let activation: GroupActivation = serde_json::from_str(json).unwrap();
628
629 assert_eq!(activation.group_id, "group-123");
630 assert!(activation.enabled); assert!(!activation.require_mention); assert!(activation.cooldown_seconds.is_none());
633 assert!(activation.whitelist.is_none());
634 }
635
636 #[test]
641 fn test_rejection_reason_display() {
642 assert_eq!(
643 GroupRejectionReason::GroupDisabled.to_string(),
644 "Group has auto-reply disabled"
645 );
646 assert_eq!(
647 GroupRejectionReason::RequiresMention.to_string(),
648 "Group requires @mention to trigger"
649 );
650 }
651
652 #[test]
657 fn test_manager_new() {
658 let manager = GroupActivationManager::new();
659 assert!(manager.is_empty());
660 assert_eq!(manager.len(), 0);
661 }
662
663 #[test]
664 fn test_manager_from_activations() {
665 let activations = vec![
666 GroupActivation::new("group-1"),
667 GroupActivation::new("group-2"),
668 ];
669 let manager = GroupActivationManager::from_activations(activations);
670
671 assert_eq!(manager.len(), 2);
672 assert!(manager.get("group-1").is_some());
673 assert!(manager.get("group-2").is_some());
674 }
675
676 #[test]
677 fn test_manager_set_and_get() {
678 let mut manager = GroupActivationManager::new();
679
680 manager.set(GroupActivation::new("group-123").with_require_mention(true));
681
682 let activation = manager.get("group-123").unwrap();
683 assert!(activation.require_mention);
684 }
685
686 #[test]
687 fn test_manager_remove() {
688 let mut manager = GroupActivationManager::new();
689 manager.set(GroupActivation::new("group-123"));
690
691 let removed = manager.remove("group-123");
692 assert!(removed.is_some());
693 assert!(manager.get("group-123").is_none());
694 }
695
696 #[test]
697 fn test_manager_should_trigger() {
698 let mut manager = GroupActivationManager::new();
699 manager.set(GroupActivation::disabled("disabled-group"));
700 manager.set(GroupActivation::new("mention-group").with_require_mention(true));
701
702 assert_eq!(
704 manager.should_trigger("disabled-group", true),
705 Err(GroupRejectionReason::GroupDisabled)
706 );
707
708 assert_eq!(
710 manager.should_trigger("mention-group", false),
711 Err(GroupRejectionReason::RequiresMention)
712 );
713 assert_eq!(manager.should_trigger("mention-group", true), Ok(()));
714
715 assert_eq!(manager.should_trigger("unknown-group", false), Ok(()));
717 }
718
719 #[test]
720 fn test_manager_effective_cooldown() {
721 let mut manager = GroupActivationManager::new();
722 manager.set(GroupActivation::new("group-123").with_cooldown(120));
723
724 assert_eq!(manager.effective_cooldown("group-123", 60), 120);
726
727 assert_eq!(manager.effective_cooldown("unknown-group", 60), 60);
729 }
730
731 #[test]
732 fn test_manager_is_user_whitelisted() {
733 let mut manager = GroupActivationManager::new();
734 manager.set(GroupActivation::new("group-123").with_whitelist(vec!["user1".to_string()]));
735
736 assert_eq!(
738 manager.is_user_whitelisted("group-123", "user1"),
739 Some(true)
740 );
741 assert_eq!(
742 manager.is_user_whitelisted("group-123", "user2"),
743 Some(false)
744 );
745
746 assert_eq!(
748 manager.is_user_whitelisted("unknown-group", "any_user"),
749 None
750 );
751 }
752}