use std::sync::Arc; use ::agent::task::service::TaskService; use avatar::AppAvatar; use config::AppConfig; use db::cache::AppCache; use db::database::AppDatabase; use email::AppEmail; use queue::{ start_email_worker, EmailEnvelope, EmailSendFn, EmailSendFut, GetRedis, MessageProducer, RedisFuture, RedisPubSub, }; use room::metrics::RoomMetrics; use room::RoomService; use serde::{Deserialize, Serialize}; use slog::{Drain, OwnedKVList, Record}; use utoipa::ToSchema; use ws_token::WsTokenService; #[derive(Clone)] pub struct AppService { pub db: AppDatabase, pub config: AppConfig, pub cache: AppCache, pub email: AppEmail, pub logs: slog::Logger, pub avatar: AppAvatar, pub room: RoomService, pub ws_token: Arc, pub queue_producer: MessageProducer, } impl AppService { pub async fn start_room_workers( &self, shutdown_rx: tokio::sync::broadcast::Receiver<()>, log: slog::Logger, ) -> anyhow::Result<()> { self.room.start_workers(shutdown_rx, log).await } pub fn build_slog_logger(level: &str) -> slog::Logger { let level_filter = match level { "trace" => 0usize, "debug" => 1usize, "info" => 2usize, "warn" => 3usize, "error" => 4usize, _ => 2usize, }; struct StderrDrain(usize); impl Drain for StderrDrain { type Ok = (); type Err = (); #[inline] fn log(&self, record: &Record, _logger: &OwnedKVList) -> Result<(), ()> { let slog_level = match record.level() { slog::Level::Trace => 0, slog::Level::Debug => 1, slog::Level::Info => 2, slog::Level::Warning => 3, slog::Level::Error => 4, slog::Level::Critical => 5, }; if slog_level < self.0 { return Ok(()); } let _ = eprintln!( "{} [{}] {}:{} - {}", chrono::Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ"), record.level().to_string(), record .file() .rsplit_once('/') .map(|(_, s)| s) .unwrap_or(record.file()), record.line(), record.msg(), ); Ok(()) } } let drain = StderrDrain(level_filter); let drain = std::sync::Mutex::new(drain); let drain = slog::Fuse::new(drain); slog::Logger::root(drain, slog::o!()) } pub async fn new(config: AppConfig) -> anyhow::Result { let db = AppDatabase::init(&config).await?; let cache = AppCache::init(&config).await?; let email = AppEmail::init(&config).await?; let avatar = AppAvatar::init(&config).await?; let log_level = config.log_level().unwrap_or_else(|_| "info".to_string()); let logs = Self::build_slog_logger(&log_level); // Build get_redis closure for MessageProducer let get_redis: Arc< dyn Fn() -> tokio::task::JoinHandle> + Send + Sync, > = Arc::new({ let pool = cache.redis_pool().clone(); move || { let pool = pool.clone(); tokio::spawn(async move { pool.get().await.map_err(|e| anyhow::anyhow!("{}", e)) }) } }); let redis_pubsub = Some(RedisPubSub { get_redis: get_redis.clone(), log: logs.clone(), }); let message_producer = MessageProducer::new(get_redis.clone(), redis_pubsub.clone(), 10000, logs.clone()); // Build RoomService let task_service = Arc::new(TaskService::new(db.clone())); let room_metrics = Arc::new(RoomMetrics::default()); let room_manager = Arc::new(room::connection::RoomConnectionManager::new( room_metrics.clone(), )); let redis_url = config .redis_urls() .ok() .and_then(|urls| urls.first().cloned()) .unwrap_or_else(|| "redis://127.0.0.1:6379".to_string()); let room = RoomService::new( db.clone(), cache.clone(), message_producer.clone(), room_manager, redis_url, None, Some(task_service.clone()), logs.clone(), None, ); // Build WsTokenService let ws_token = Arc::new(WsTokenService::new(get_redis.clone())); Ok(Self { db, config, cache, email, logs, avatar, room, ws_token, queue_producer: message_producer, }) } pub async fn start_email_workers( &self, shutdown_rx: tokio::sync::broadcast::Receiver<()>, ) -> anyhow::Result<()> { let get_redis_fn = self.queue_producer.get_redis.clone(); let get_redis: GetRedis = Arc::new(move || -> RedisFuture { let get_redis_fn = get_redis_fn.clone(); Box::pin(async move { get_redis_fn().await? }) }); let email = self.email.clone(); let logs = self.logs.clone(); let send_fn: EmailSendFn = Arc::new(move |envelopes: Vec| -> EmailSendFut { let email = email.clone(); let logs = logs.clone(); Box::pin(async move { for envelope in envelopes { let to = envelope.to.clone(); let msg = email::EmailMessage { to: envelope.to, subject: envelope.subject, body: envelope.body, }; if let Err(e) = email.send(msg).await { slog::error!(logs, "email send failed"; "to" => to, "error" => %e); } } Ok(()) }) }); start_email_worker(get_redis, send_fn, shutdown_rx, self.logs.clone()).await; Ok(()) } } pub mod agent; pub mod auth; pub mod error; pub mod git; pub mod issue; pub mod project; pub mod pull_request; pub mod search; pub mod skill; pub mod user; pub mod utils; pub mod workspace; pub mod ws_token; #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] pub struct Pager { pub page: i64, pub par_page: i64, }