robson-core 0.0.1

Rust async agent orchestrator for automated development workflows
Documentation
use anyhow::Result;
use crate::entities::{allowed_channel, rate_limit::{self, RateLimitScope}};
use robson_slack::events::InnerEvent;
use sea_orm::DatabaseConnection;
use std::collections::HashSet;
use std::sync::{Arc, Mutex};

#[derive(Debug, Clone)]
pub enum SanitizedInput {
    Message {
        text: String,
        channel_id: String,
        user_id: String,
        thread_ts: Option<String>,
        ts: String,
    },
}

pub struct Sensorium {
    db: DatabaseConnection,
    seen_envelopes: Arc<Mutex<HashSet<String>>>,
    max_requests_per_user: i32,
    max_requests_per_channel: i32,
    rate_limit_window_secs: i64,
}

impl Sensorium {
    pub fn new(db: DatabaseConnection) -> Self {
        Self {
            db,
            seen_envelopes: Arc::new(Mutex::new(HashSet::new())),
            max_requests_per_user: 20,
            max_requests_per_channel: 100,
            rate_limit_window_secs: 3600,
        }
    }

    pub fn new_with_limits(
        db: DatabaseConnection,
        max_requests_per_user: i32,
        max_requests_per_channel: i32,
        rate_limit_window_secs: i64,
    ) -> Self {
        Self {
            db,
            seen_envelopes: Arc::new(Mutex::new(HashSet::new())),
            max_requests_per_user,
            max_requests_per_channel,
            rate_limit_window_secs,
        }
    }

    /// Process an incoming event. Returns None if it should be ignored (dedup, disallowed channel, rate limited).
    pub async fn process_event(
        &self,
        envelope_id: &str,
        event: InnerEvent,
        channel_id: &str,
    ) -> Result<Option<SanitizedInput>> {
        // 1. Dedup by envelope_id
        {
            let mut seen = self.seen_envelopes.lock().unwrap();
            if seen.contains(envelope_id) {
                return Ok(None);
            }
            seen.insert(envelope_id.to_string());
        }

        // 2. Check allowed channels
        if !allowed_channel::Model::is_allowed(&self.db, channel_id).await? {
            return Ok(None);
        }

        // Extract user_id and text from event
        let (user_id, text, ts, thread_ts) = match &event {
            InnerEvent::AppMention(m) => (
                m.user.clone(),
                m.text.clone(),
                m.ts.clone(),
                m.thread_ts.clone(),
            ),
            InnerEvent::Message(m) => (
                m.user.clone().unwrap_or_default(),
                m.text.clone().unwrap_or_default(),
                m.ts.clone(),
                m.thread_ts.clone(),
            ),
        };

        // Skip bot messages
        if let InnerEvent::Message(m) = &event {
            if m.bot_id.is_some() {
                return Ok(None);
            }
        }

        // 3. Rate limit by user
        let user_allowed = rate_limit::Model::check_and_increment(
            &self.db,
            RateLimitScope::User,
            &user_id,
            self.rate_limit_window_secs,
            self.max_requests_per_user,
        )
        .await?;
        if !user_allowed {
            return Ok(None);
        }

        // 4. Rate limit by channel
        let channel_allowed = rate_limit::Model::check_and_increment(
            &self.db,
            RateLimitScope::Channel,
            channel_id,
            self.rate_limit_window_secs,
            self.max_requests_per_channel,
        )
        .await?;
        if !channel_allowed {
            return Ok(None);
        }

        let sanitized_text = ::robson_slack::events::sanitize_text(&text);

        Ok(Some(SanitizedInput::Message {
            text: sanitized_text,
            channel_id: channel_id.to_string(),
            user_id,
            thread_ts,
            ts,
        }))
    }
}