Files
imks/svc/component.rs
T
zhenyi 0dbac480ae feat(telemetry): integrate OpenTelemetry observability stack with health metrics
- 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
2026-06-11 13:53:29 +08:00

98 lines
3.5 KiB
Rust

//! Interactive component event handlers on `MessageService`.
//!
//! Handles button clicks and select menu interactions on message components.
//! When a user clicks a button, the server updates the component state and
//! broadcasts the interaction to the channel.
use std::sync::Arc;
use uuid::Uuid;
use crate::ImksError;
use crate::socket::socket::Socket;
use super::message::MessageService;
impl MessageService {
/// Handle `component:interact` — a user clicked a button or selected from a menu.
pub async fn interact_component(
&self,
socket: Arc<Socket>,
data: &serde_json::Value,
) -> crate::ImksResult<()> {
let user_id = self.user_id(&socket)?;
let arr = data
.as_array()
.and_then(|a| a.first())
.ok_or_else(|| ImksError::InvalidInput("Expected [payload] array".into()))?;
let component_id: Uuid = Self::parse_field(arr, "component_id")?;
let custom_id: String = Self::parse_field(arr, "custom_id")?;
let message_id: Uuid = Self::parse_field(arr, "message_id")?;
let channel_id: Uuid = Self::parse_field(arr, "channel_id")?;
// Get current components to verify the interaction is valid
let components = self.repo.get_components(message_id).await?;
let component = components.iter().find(|c| c.id == component_id);
if component.is_none() {
return Err(ImksError::NotFound(format!("component {component_id}")));
}
// Broadcast the interaction event to all clients in the channel.
// The actual action (e.g., approve/deny) is handled by the bot/webhook
// that listens for this event. The server just relays and disables the
// component to prevent double-clicks.
if let Some(ns) = self.namespaces.get_namespace(&socket.namespace) {
ns.emit_to_room(
&channel_id.to_string(),
"component:interaction",
serde_json::json!({
"component_id": component_id.to_string(),
"custom_id": custom_id,
"message_id": message_id.to_string(),
"user_id": user_id.to_string(),
"channel_id": channel_id.to_string(),
}),
)
.await;
}
tracing::info!(%component_id, %user_id, %custom_id, "Component interaction");
Ok(())
}
/// Handle `component:update` — update a component's state (e.g., disable after interaction).
pub async fn update_component(
&self,
socket: Arc<Socket>,
data: &serde_json::Value,
) -> crate::ImksResult<()> {
let arr = data
.as_array()
.and_then(|a| a.first())
.ok_or_else(|| ImksError::InvalidInput("Expected [payload] array".into()))?;
let component_id: Uuid = Self::parse_field(arr, "component_id")?;
let label: Option<String> = Self::parse_optional(arr, "label")?;
let disabled: bool = Self::parse_optional(arr, "disabled")?.unwrap_or(true);
if let Some(updated) = self
.repo
.update_component(component_id, label.as_deref(), disabled)
.await?
&& let Some(ns) = self.namespaces.get_namespace(&socket.namespace)
{
ns.emit_to_room(
&updated.message_id.to_string(),
"component:updated",
serde_json::to_value(&updated).unwrap_or_default(),
)
.await;
}
Ok(())
}
}