aves-tyto 0.5.0

Specialized messaging domain for the Aves async application runtime
use serde_json::Value;
use tokio::sync::mpsc;

use crate::channels::{MessagingSubscription, MessagingSubscriptionError, MessagingTask};
use crate::messages::ListenerType;
use crate::types::NormalizedMessage;

pub(super) struct SlackFilter {
    pub(super) subscribed_channels: Option<Vec<String>>,
    pub(super) allowed_senders: Vec<String>,
}

impl SlackFilter {
    pub(super) fn new(subscribed_channels: Option<Vec<String>>) -> Self {
        Self {
            subscribed_channels,
            allowed_senders: Vec::new(),
        }
    }

    fn with_senders(mut self, allowed_senders: Vec<String>) -> Self {
        self.allowed_senders = allowed_senders;
        self
    }

    fn accepts(&self, channel: &str) -> bool {
        self.subscribed_channels
            .as_ref()
            .is_none_or(|channels| channels.iter().any(|selected| selected == channel))
    }

    /// Checks message metadata against this subscription's channel and sender policy.
    fn accepts_message(&self, message: &NormalizedMessage) -> bool {
        let Some(channel) = message.metadata.get("channel_id").and_then(Value::as_str) else {
            return false;
        };
        let sender = message.metadata.get("user_id").and_then(Value::as_str);
        self.accepts(channel)
            && (self.allowed_senders.is_empty()
                || sender.is_some_and(|sender| self.allowed_senders.iter().any(|allowed| allowed == sender)))
    }
}

pub(super) fn subscribe(
    subscription: MessagingSubscription,
    mut rx: mpsc::Receiver<NormalizedMessage>,
    tx: mpsc::Sender<NormalizedMessage>,
) -> Result<MessagingTask, MessagingSubscriptionError> {
    subscription.validate(ListenerType::Slack)?;
    let policy = SlackFilter::new(subscription.subscribed_channels).with_senders(subscription.allowed_senders);
    Ok(tokio::spawn(async move {
        while let Some(message) = tokio::select! { _ = tx.closed() => None, message = rx.recv() => message } {
            if policy.accepts_message(&message) && tx.send(message).await.is_err() {
                break;
            }
        }
        Ok(())
    }))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn channel_policy_is_bound_to_each_subscription() {
        let restricted = SlackFilter::new(Some(vec!["C123".to_owned()]));
        assert!(restricted.accepts("C123"));
        assert!(!restricted.accepts("C999"));
        let unrestricted = SlackFilter::new(None);
        assert!(unrestricted.accepts("C123"));
        assert!(unrestricted.accepts("C999"));
        assert!(
            MessagingSubscription {
                subscribed_channels: Some(Vec::new()),
                allowed_senders: Vec::new()
            }
            .validate(ListenerType::Slack)
            .is_err()
        );
    }
}