Skip to main content

agentmux_srv/messaging/
mod.rs

1// Copyright 2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Messaging bridge framework — common types shared by all platform bridges.
5//!
6//! Each bridge (Discord, Telegram, Slack, …) runs as a background tokio task.
7//! Inbound messages are injected into the reactive bus; outbound messages
8//! arrive via HTTP endpoint or MCP tool.
9
10pub mod discord;
11pub mod slack;
12pub mod telegram;
13pub mod whatsapp;
14
15use serde::{Deserialize, Serialize};
16
17// ── Common message types ───────────────────────────────────────────────────
18
19/// A message received from a messaging platform, normalized for reactive bus injection.
20/// Constructed in Phase 2 when per-message metadata is surfaced to the frontend.
21#[allow(dead_code)]
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct InboundMsg {
24    pub platform: String,
25    pub platform_msg_id: String,
26    pub from_id: String,
27    pub from_name: String,
28    pub channel_id: String,
29    pub text: String,
30    #[serde(default)]
31    pub attachments: Vec<String>,
32    pub received_at: u64,
33}
34
35/// A message to send to a messaging platform.
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct OutboundMsg {
38    pub text: String,
39    /// Target channel. Empty → use the bridge's default channel.
40    #[serde(default)]
41    pub channel_id: String,
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub reply_to: Option<String>,
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub embed: Option<MsgEmbed>,
46    /// Telegram-specific: if set, edit this existing message instead of
47    /// sending a new one (streaming-output simulation, spec §2.3). Ignored
48    /// by platforms that don't support editing (e.g. Discord in v1).
49    #[serde(default, skip_serializing_if = "Option::is_none")]
50    pub edit_message_id: Option<i64>,
51    /// Slack-specific: raw Block Kit `blocks` array, passed through verbatim
52    /// to `chat.postMessage` (escape hatch — see
53    /// `SPEC_MESSAGING_INTEGRATION_SLACK_2026_07_07.md` §7, §11.1). Block
54    /// Kit doesn't map onto `MsgEmbed` (no shared shape with Discord's flat
55    /// embed record), so it gets its own field here rather than a forked
56    /// per-platform outbound type. Harmless no-op for platforms that don't
57    /// support it. `text` is still always sent alongside `blocks` — Slack
58    /// requires it as the notification/accessibility fallback.
59    #[serde(default, skip_serializing_if = "Option::is_none")]
60    pub blocks: Option<Vec<serde_json::Value>>,
61}
62
63/// Rich embed for platforms that support structured output (Discord, Slack).
64#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct MsgEmbed {
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub title: Option<String>,
68    pub description: String,
69    /// Platform-specific color integer (Discord: 0xRRGGBB).
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub color: Option<u32>,
72    #[serde(default)]
73    pub fields: Vec<EmbedField>,
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub footer: Option<String>,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct EmbedField {
80    pub name: String,
81    pub value: String,
82    #[serde(default)]
83    pub inline: bool,
84}
85
86// ── Bridge health ──────────────────────────────────────────────────────────
87
88#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
89#[serde(rename_all = "snake_case")]
90pub enum BridgeStatus {
91    Connected,
92    Connecting,
93    Disconnected,
94    Error,
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct BridgeHealth {
99    pub platform: String,
100    pub status: BridgeStatus,
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub latency_ms: Option<u32>,
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub last_event_at: Option<u64>,
105    pub reconnect_count: u32,
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub error: Option<String>,
108}
109
110impl BridgeHealth {
111    pub fn connecting(platform: &str) -> Self {
112        BridgeHealth {
113            platform: platform.to_string(),
114            status: BridgeStatus::Connecting,
115            latency_ms: None,
116            last_event_at: None,
117            reconnect_count: 0,
118            error: None,
119        }
120    }
121}
122
123// ── Common bridge interface ────────────────────────────────────────────────
124
125/// Common interface implemented by every platform bridge (Discord, Telegram, …).
126///
127/// Bridges are singletons accessed via `get()`-style static accessors on the
128/// concrete type (see `DiscordBridge::get()`, `TelegramBridge::get()`) for the
129/// call sites that know their platform statically (e.g. platform-specific HTTP
130/// handlers). This trait exists for call sites that need to treat bridges
131/// polymorphically — today, only `handle_status`'s aggregation loop.
132pub trait MessagingBridge: Send + Sync {
133    /// Current connection/health snapshot. Cheap, non-blocking (reads an
134    /// `Arc<Mutex<BridgeHealth>>` guarded by a short-lived lock).
135    fn health(&self) -> BridgeHealth;
136}