Skip to main content

agentmux_srv/messaging/discord/
rest.rs

1// Copyright 2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Discord REST API client — message send only.
5//!
6//! Rate limiting: Discord returns X-RateLimit-* headers. We log 429s and
7//! return an error; the caller (gateway loop) logs a warning. Full rate-limit
8//! header handling is deferred to Phase 2.
9
10use crate::messaging::{EmbedField, MsgEmbed, OutboundMsg};
11
12use super::types::{DiscordEmbed, DiscordEmbedField, DiscordEmbedFooter, SendMessageBody};
13
14const DISCORD_API: &str = "https://discord.com/api/v10";
15
16/// POST /channels/{channel_id}/messages
17pub async fn send_message(
18    http: &reqwest::Client,
19    token: &str,
20    channel_id: &str,
21    msg: &OutboundMsg,
22) -> Result<(), String> {
23    let embeds = msg.embed.as_ref().map(to_discord_embed).into_iter().collect();
24
25    let body = SendMessageBody {
26        content: msg.text.clone(),
27        embeds,
28    };
29
30    let url = format!("{}/channels/{}/messages", DISCORD_API, channel_id);
31    let resp = http
32        .post(&url)
33        .header("Authorization", format!("Bot {}", token))
34        .json(&body)
35        .send()
36        .await
37        .map_err(|e| format!("discord rest: http error: {e}"))?;
38
39    let status = resp.status();
40    if status == reqwest::StatusCode::TOO_MANY_REQUESTS {
41        let body = resp.text().await.unwrap_or_default();
42        return Err(format!("discord rest: rate limited: {body}"));
43    }
44    if !status.is_success() {
45        let body = resp.text().await.unwrap_or_default();
46        return Err(format!("discord rest: {status}: {body}"));
47    }
48
49    Ok(())
50}
51
52fn to_discord_embed(embed: &MsgEmbed) -> DiscordEmbed {
53    DiscordEmbed {
54        title: embed.title.clone(),
55        description: embed.description.clone(),
56        color: embed.color,
57        fields: embed.fields.iter().map(to_discord_field).collect(),
58        footer: embed
59            .footer
60            .as_ref()
61            .map(|t| DiscordEmbedFooter { text: t.clone() }),
62    }
63}
64
65fn to_discord_field(f: &EmbedField) -> DiscordEmbedField {
66    DiscordEmbedField {
67        name: f.name.clone(),
68        value: f.value.clone(),
69        inline: f.inline,
70    }
71}