rain_engine_channels/
discord.rs1use crate::{ChannelAdapter, ChannelConfig};
8use async_trait::async_trait;
9use rain_engine_client::RainEngineClient;
10use serde::Deserialize;
11use tracing::{error, info, warn};
12
13#[derive(Debug, Clone)]
14pub struct DiscordAdapter {
15 token: String,
16 client: reqwest::Client,
17 engine_client: RainEngineClient,
18 config: ChannelConfig,
19}
20
21impl DiscordAdapter {
22 pub fn new(token: String, config: ChannelConfig) -> Self {
23 Self {
24 engine_client: RainEngineClient::new(&config.runtime_url)
25 .expect("failed to init client"),
26 client: reqwest::Client::new(),
27 token,
28 config,
29 }
30 }
31
32 fn session_id(&self, channel_id: &str) -> String {
33 format!(
34 "{}-discord-{}",
35 self.config.default_session_prefix, channel_id
36 )
37 }
38
39 async fn send_message(&self, channel_id: &str, content: &str) -> Result<(), reqwest::Error> {
40 self.client
41 .post(format!(
42 "https://discord.com/api/v10/channels/{channel_id}/messages"
43 ))
44 .header("Authorization", format!("Bot {}", self.token))
45 .json(&serde_json::json!({ "content": content }))
46 .send()
47 .await?;
48 Ok(())
49 }
50}
51
52#[derive(Debug, Deserialize)]
53struct GatewayInfo {
54 url: String,
55}
56
57#[derive(Debug, Deserialize)]
58#[allow(dead_code)]
59struct GatewayEvent {
60 op: u8,
61 #[serde(default)]
62 t: Option<String>,
63 #[serde(default)]
64 d: Option<serde_json::Value>,
65 #[serde(default)]
66 s: Option<i64>,
67}
68
69#[async_trait]
70impl ChannelAdapter for DiscordAdapter {
71 fn name(&self) -> &str {
72 "discord"
73 }
74
75 async fn run(&self, cancel: tokio_util::sync::CancellationToken) {
76 info!("Discord adapter started");
77
78 let gateway_url = match self
80 .client
81 .get("https://discord.com/api/v10/gateway")
82 .header("Authorization", format!("Bot {}", self.token))
83 .send()
84 .await
85 {
86 Ok(resp) => match resp.json::<GatewayInfo>().await {
87 Ok(info) => format!("{}?v=10&encoding=json", info.url),
88 Err(err) => {
89 error!("Failed to parse gateway URL: {err}");
90 return;
91 }
92 },
93 Err(err) => {
94 error!("Failed to get Discord gateway: {err}");
95 return;
96 }
97 };
98
99 info!(url = %gateway_url, "Connecting to Discord gateway");
100
101 warn!(
105 "Discord adapter: using REST polling fallback. For production, add WebSocket support."
106 );
107
108 let _last_message_id: Option<String> = None;
109
110 loop {
111 if cancel.is_cancelled() {
112 info!("Discord adapter shutting down");
113 return;
114 }
115
116 tokio::select! {
118 _ = cancel.cancelled() => return,
119 _ = tokio::time::sleep(std::time::Duration::from_secs(5)) => {}
120 }
121
122 }
126 }
127}
128
129impl DiscordAdapter {
132 pub async fn handle_message(
133 &self,
134 channel_id: &str,
135 author_id: &str,
136 content: &str,
137 is_bot: bool,
138 ) {
139 if is_bot {
140 return; }
142
143 let actor_id = format!("discord:{author_id}");
144 let session_id = self.session_id(channel_id);
145
146 info!(channel_id, actor = %actor_id, "Discord message received");
147
148 match self
149 .engine_client
150 .send_human_input(&actor_id, &session_id, content)
151 .await
152 {
153 Ok(result) => {
154 let reply = result
155 .outcome
156 .response
157 .as_deref()
158 .unwrap_or("*(no response)*");
159 if let Err(err) = self.send_message(channel_id, reply).await {
160 error!("Failed to send Discord reply: {err}");
161 }
162 }
163 Err(err) => {
164 error!("Engine request failed: {err}");
165 let _ = self
166 .send_message(channel_id, "⚠️ Engine error, please try again.")
167 .await;
168 }
169 }
170 }
171}