0dbac480ae
- Add OpenTelemetry SDK, OTLP exporter, Prometheus integration - Implement connection tracking with active/total/disconnection metrics - Add health endpoint with uptime and connection counts - Integrate tracing spans for socket events and engine messages - Add metrics collection for event handling duration - Update health endpoint to include live runtime state - Add graceful telemetry shutdown in main function - Implement engine session active metrics tracking - Add namespace-specific attributes to connection metrics - Introduce message edit history retrieval endpoint - Add scheduled message CRUD operations and dispatcher - Update Socket.IO event registration with observability - Refactor component update to remove dead code allowance - Add comprehensive environment variables documentation - Implement detailed development guidelines in AGENTS.md
197 lines
6.7 KiB
Rust
197 lines
6.7 KiB
Rust
//! Scheduled message handler on `MessageService`.
|
|
//!
|
|
//! Provides:
|
|
//! - Client-facing CRUD: schedule, cancel, list pending scheduled messages
|
|
//! - Background dispatcher: periodically scans for due scheduled messages
|
|
//! and sends them through the normal message path.
|
|
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
|
|
use chrono::{DateTime, Utc};
|
|
use uuid::Uuid;
|
|
|
|
use crate::repo::CreateMessageInput;
|
|
use crate::socket::socket::Socket;
|
|
use crate::{ImksError, ImksResult};
|
|
|
|
use super::message::MessageService;
|
|
|
|
impl MessageService {
|
|
// ── Client-facing scheduled message CRUD ──
|
|
|
|
/// Handle `message:schedule` — schedule a message to be sent at a future time.
|
|
pub async fn schedule_message(
|
|
&self,
|
|
socket: Arc<Socket>,
|
|
data: &serde_json::Value,
|
|
) -> ImksResult<()> {
|
|
let user_id = self.user_id(&socket)?;
|
|
let payload = Self::first_payload(data)?;
|
|
|
|
let channel_id: Uuid = Self::parse_field(payload, "channel_id")?;
|
|
let body: String = Self::parse_field(payload, "body")?;
|
|
let thread_id: Option<Uuid> = Self::parse_optional(payload, "thread_id")?;
|
|
let reply_to_message_id: Option<Uuid> =
|
|
Self::parse_optional(payload, "reply_to_message_id")?;
|
|
let metadata: Option<serde_json::Value> =
|
|
Self::parse_optional(payload, "metadata")?;
|
|
let scheduled_at_str: String = Self::parse_field(payload, "scheduled_at")?;
|
|
|
|
let scheduled_at: DateTime<Utc> = chrono::DateTime::parse_from_rfc3339(&scheduled_at_str)
|
|
.map_err(|e| ImksError::InvalidInput(format!("Invalid scheduled_at: {e}")))?
|
|
.into();
|
|
|
|
let channel_id_str = channel_id.to_string();
|
|
let user_id_str = user_id.to_string();
|
|
|
|
self.validate_body_size(&body)?;
|
|
self.ensure_readable(&channel_id_str, &user_id_str).await?;
|
|
self.ensure_member(&channel_id_str, &user_id_str).await?;
|
|
|
|
// Validate scheduled_at is in the future
|
|
if scheduled_at <= Utc::now() {
|
|
return Err(ImksError::InvalidInput(
|
|
"scheduled_at must be in the future".into(),
|
|
));
|
|
}
|
|
|
|
let scheduled = self
|
|
.repo
|
|
.schedule_message(
|
|
channel_id,
|
|
user_id,
|
|
thread_id,
|
|
reply_to_message_id,
|
|
&body,
|
|
metadata,
|
|
scheduled_at,
|
|
)
|
|
.await?;
|
|
|
|
tracing::info!(
|
|
scheduled_id = %scheduled.id,
|
|
channel_id = %channel_id,
|
|
user_id = %user_id,
|
|
scheduled_at = %scheduled_at,
|
|
"Message scheduled"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
/// Handle `message:cancel_scheduled` — cancel a pending scheduled message.
|
|
pub async fn cancel_scheduled(
|
|
&self,
|
|
socket: Arc<Socket>,
|
|
data: &serde_json::Value,
|
|
) -> ImksResult<()> {
|
|
let user_id = self.user_id(&socket)?;
|
|
let payload = Self::first_payload(data)?;
|
|
let scheduled_id: Uuid = Self::parse_field(payload, "scheduled_id")?;
|
|
|
|
let cancelled = self.repo.cancel_scheduled(scheduled_id).await?;
|
|
|
|
if !cancelled {
|
|
return Err(ImksError::NotFound(format!(
|
|
"scheduled message {scheduled_id} not found or already processed"
|
|
)));
|
|
}
|
|
|
|
tracing::info!(%scheduled_id, %user_id, "Scheduled message cancelled");
|
|
Ok(())
|
|
}
|
|
|
|
/// Handle `message:list_scheduled` — list pending scheduled messages for a channel.
|
|
pub async fn list_scheduled(
|
|
&self,
|
|
socket: Arc<Socket>,
|
|
data: &serde_json::Value,
|
|
) -> ImksResult<()> {
|
|
let user_id = self.user_id(&socket)?;
|
|
let payload = Self::first_payload(data)?;
|
|
let channel_id: Uuid = Self::parse_field(payload, "channel_id")?;
|
|
|
|
let channel_id_str = channel_id.to_string();
|
|
let user_id_str = user_id.to_string();
|
|
|
|
self.ensure_readable(&channel_id_str, &user_id_str).await?;
|
|
|
|
let scheduled = self.repo.list_scheduled(channel_id, user_id).await?;
|
|
|
|
let _ = socket.emit(
|
|
"scheduled:loaded",
|
|
serde_json::to_value(&scheduled).unwrap_or_default(),
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
// ── Background dispatcher ──
|
|
|
|
/// Start the background scheduled-message dispatcher.
|
|
/// Scans every 30 seconds for pending messages whose `scheduled_at` has passed.
|
|
pub fn start_scheduled_dispatcher(self: std::sync::Arc<Self>) {
|
|
tokio::spawn(async move {
|
|
tracing::info!("Scheduled message dispatcher started (interval: 30s)");
|
|
loop {
|
|
tokio::time::sleep(Duration::from_secs(30)).await;
|
|
|
|
match self.process_due_scheduled().await {
|
|
Ok(count) => {
|
|
if count > 0 {
|
|
tracing::info!(count, "Dispatched scheduled messages");
|
|
}
|
|
}
|
|
Err(e) => {
|
|
tracing::error!(error = %e, "Scheduled message dispatch failed");
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/// Fetch and dispatch all due scheduled messages.
|
|
async fn process_due_scheduled(&self) -> crate::ImksResult<usize> {
|
|
let due = self.repo.get_due_scheduled().await?;
|
|
let mut dispatched = 0;
|
|
|
|
for scheduled in due {
|
|
let input = CreateMessageInput {
|
|
channel_id: scheduled.channel_id,
|
|
author_id: scheduled.author_id,
|
|
thread_id: scheduled.thread_id,
|
|
reply_to_message_id: scheduled.reply_to_message_id,
|
|
message_type: "text".into(),
|
|
body: scheduled.body.clone(),
|
|
metadata: scheduled.metadata.clone(),
|
|
system: false,
|
|
};
|
|
|
|
match self.repo.create(&input).await {
|
|
Ok(message) => {
|
|
self.repo
|
|
.mark_scheduled_sent(scheduled.id, message.id)
|
|
.await?;
|
|
|
|
if let Some(ns) = self.namespaces.get_namespace("/") {
|
|
ns.emit_to_room(
|
|
&scheduled.channel_id.to_string(),
|
|
"message:new",
|
|
serde_json::to_value(&message).unwrap_or_default(),
|
|
)
|
|
.await;
|
|
}
|
|
dispatched += 1;
|
|
}
|
|
Err(e) => {
|
|
tracing::error!(scheduled_id = %scheduled.id, error = %e, "Failed to send scheduled message");
|
|
self.repo
|
|
.mark_scheduled_failed(scheduled.id, &e.to_string())
|
|
.await?;
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(dispatched)
|
|
}
|
|
}
|