//! Interactive message components — maps to `message_component` table. //! //! Discord-style interactive elements attached to messages: buttons, select //! menus, etc. Each component belongs to a message and has its own layout row. //! Clicking a component emits an interaction event back to the bot/webhook. use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; /// An interactive component attached to a message (button, select menu, etc.). /// /// Maps to the `message_component` table. Each component belongs to a message /// and has a layout row/position. User interactions emit callbacks. #[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] pub struct MessageComponent { pub id: Uuid, pub message_id: Uuid, /// Layout row within the message (0-based). pub row: i32, /// Position within the row (0-based). pub position: i32, /// "button" | "select_menu" | "text_input" pub component_type: String, /// Unique identifier sent back in interaction callbacks. pub custom_id: String, /// Display label. pub label: Option, /// Emoji shown on the button (unicode or `:name:id`). pub emoji: Option, /// Button style: "primary" | "secondary" | "success" | "danger" | "link" pub style: Option, /// URL for link-style buttons. pub url: Option, /// Whether the component is disabled. pub disabled: bool, /// Placeholder text for select menus. pub placeholder: Option, /// Min/max selections for select menus. pub min_values: Option, pub max_values: Option, /// Options for select menus, stored as JSON array. pub options: Option, pub created_at: DateTime, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] #[serde(rename_all = "snake_case")] pub enum ComponentType { #[default] Button, SelectMenu, TextInput, } impl ComponentType { pub fn as_str(&self) -> &'static str { match self { Self::Button => "button", Self::SelectMenu => "select_menu", Self::TextInput => "text_input", } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_component_serialize() { let c = MessageComponent { id: Uuid::now_v7(), message_id: Uuid::now_v7(), row: 0, position: 0, component_type: ComponentType::Button.as_str().into(), custom_id: "btn_approve".into(), label: Some("Approve".into()), emoji: Some("✅".into()), style: Some("success".into()), url: None, disabled: false, placeholder: None, min_values: None, max_values: None, options: None, created_at: Utc::now(), }; let json = serde_json::to_value(&c).unwrap(); assert_eq!(json["component_type"], "button"); assert_eq!(json["style"], "success"); } }