use std::time::Duration;
use serde::Deserialize;
use crate::error::{Error, Result};
use crate::types::{CommandRegistration, MessagePayload};
pub const API_BASE: &str = "https://discord.com/api/v10";
const REQUEST_TIMEOUT: Duration = Duration::from_secs(10);
const USER_AGENT: &str = concat!("autumn-plugin-discord/", env!("CARGO_PKG_VERSION"));
#[derive(Debug, Clone, Deserialize)]
pub struct ApplicationInfo {
pub id: String,
pub name: String,
}
#[derive(Clone)]
pub struct DiscordClient {
inner: reqwest::Client,
base_url: String,
application_id: String,
token: String,
}
impl std::fmt::Debug for DiscordClient {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DiscordClient")
.field("base_url", &self.base_url)
.field("application_id", &self.application_id)
.field("token", &"<redacted>")
.finish_non_exhaustive()
}
}
impl DiscordClient {
pub fn new(base_url: impl Into<String>, token: impl Into<String>) -> Result<Self> {
let token = token.into();
if token.trim().is_empty() {
return Err(Error::Config("bot token is empty".to_owned()));
}
let inner = reqwest::Client::builder()
.timeout(REQUEST_TIMEOUT)
.user_agent(USER_AGENT)
.build()
.map_err(Error::Http)?;
Ok(Self {
inner,
base_url: base_url.into().trim_end_matches('/').to_owned(),
application_id: String::new(),
token,
})
}
#[must_use]
pub fn with_application_id(mut self, application_id: impl Into<String>) -> Self {
self.application_id = application_id.into();
self
}
pub async fn check_token(&self) -> Result<ApplicationInfo> {
let response = self
.inner
.get(format!("{}/applications/@me", self.base_url))
.bearer_auth(&self.token)
.send()
.await
.map_err(Error::Http)?;
parse_response(response).await
}
pub async fn register_commands(
&self,
commands: &[CommandRegistration],
) -> Result<Vec<serde_json::Value>> {
if self.application_id.trim().is_empty() {
return Err(Error::Config(
"application_id is missing; cannot register commands".to_owned(),
));
}
let response = self
.inner
.put(format!(
"{}/applications/{}/commands",
self.base_url, self.application_id
))
.bearer_auth(&self.token)
.json(commands)
.send()
.await
.map_err(Error::Http)?;
parse_response(response).await
}
pub async fn create_followup(
&self,
interaction_token: &str,
message: &MessagePayload,
) -> Result<serde_json::Value> {
if self.application_id.trim().is_empty() {
return Err(Error::Config(
"application_id is missing; cannot send follow-ups".to_owned(),
));
}
let response = self
.inner
.post(format!(
"{}/webhooks/{}/{interaction_token}",
self.base_url, self.application_id
))
.bearer_auth(&self.token)
.json(message)
.send()
.await
.map_err(Error::Http)?;
parse_response(response).await
}
}
async fn parse_response<T>(response: reqwest::Response) -> Result<T>
where
T: serde::de::DeserializeOwned,
{
let status = response.status();
if status.is_success() {
let bytes = response.bytes().await.map_err(Error::Http)?;
serde_json::from_slice::<T>(&bytes).map_err(Error::Json)
} else {
let code = status.as_u16();
let body = response.text().await.unwrap_or_default();
let truncated: String = body.chars().take(500).collect();
Err(Error::DiscordApi {
status: code,
body: truncated,
})
}
}
#[cfg(test)]
mod tests;