#[cfg(test)]
mod tests;
use std::{
error,
fmt::{self, Display, Formatter},
sync::{
Arc,
atomic::{AtomicBool, AtomicU64, Ordering},
},
};
use twilight_http::{Client, Response, client::InteractionClient, response::DeserializeBodyError};
use twilight_model::{
channel::Message,
http::interaction::InteractionResponse,
id::{
Id,
marker::{ApplicationMarker, InteractionMarker, MessageMarker},
},
};
use twilight_validate::message::MessageValidationError;
#[cfg(feature = "respond_on_delay")]
use {std::time::Duration, tokio::time::sleep};
#[cfg(doc)]
use crate::builder::InteractionResponseBuilder;
#[derive(Debug)]
pub enum Error {
DeserializeBody(DeserializeBodyError),
Http(twilight_http::Error),
LastMessageNotTracked,
MessageValidation(MessageValidationError),
}
impl Display for Error {
#[expect(
clippy::min_ident_chars,
reason = "identifier is the default for trait"
)]
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::DeserializeBody(err) => err.fmt(f),
Self::Http(err) => err.fmt(f),
Self::MessageValidation(err) => err.fmt(f),
Self::LastMessageNotTracked => {
f.write_str("tried to return the last message when it isn't tracked")
}
}
}
}
impl From<DeserializeBodyError> for Error {
fn from(err: DeserializeBodyError) -> Self {
Self::DeserializeBody(err)
}
}
impl From<twilight_http::Error> for Error {
fn from(err: twilight_http::Error) -> Self {
Self::Http(err)
}
}
impl From<MessageValidationError> for Error {
fn from(err: MessageValidationError) -> Self {
Self::MessageValidation(err)
}
}
impl error::Error for Error {}
#[derive(Debug)]
pub enum FollowupResponse {
Deserialized(Message),
None,
NotDeserialized(Response<Message>),
}
impl FollowupResponse {
pub async fn model(self) -> Result<Option<Message>, Error> {
match self {
Self::Deserialized(message) => Ok(Some(message)),
Self::NotDeserialized(message) => Ok(Some(message.model().await?)),
Self::None => Ok(None),
}
}
}
#[derive(Clone, Debug)]
pub struct InteractionHandle {
application_id: Id<ApplicationMarker>,
client: Arc<Client>,
id: Id<InteractionMarker>,
is_last_message_tracked: bool,
is_responded: Arc<AtomicBool>,
last_message_id: Arc<AtomicU64>,
token: String,
}
impl InteractionHandle {
const LOAD_ORDERING: Ordering = Ordering::Acquire;
const STORE_ORDERING: Ordering = Ordering::Release;
#[must_use]
pub fn client(&self) -> InteractionClient<'_> {
self.client.interaction(self.application_id)
}
async fn create_followup(
&self,
response: InteractionResponse,
) -> Result<Response<Message>, Error> {
let interaction_client = self.client();
let mut create_followup = interaction_client.create_followup(&self.token);
let Some(data) = response.data else {
return Ok(create_followup.await?);
};
if let Some(attachments) = &data.attachments {
create_followup = create_followup.attachments(attachments);
}
if let Some(components) = &data.components {
create_followup = create_followup.components(components);
}
if let Some(content) = &data.content {
create_followup = create_followup.content(content);
}
if let Some(embeds) = &data.embeds {
create_followup = create_followup.embeds(embeds);
}
if let Some(flags) = data.flags {
create_followup = create_followup.flags(flags);
}
if let Some(tts) = data.tts {
create_followup = create_followup.tts(tts);
}
Ok(create_followup.await?)
}
fn is_responded(&self) -> bool {
self.is_responded.load(Self::LOAD_ORDERING)
}
pub fn last_message_id(&self) -> Result<Option<Id<MessageMarker>>, Error> {
if !self.is_last_message_tracked {
return Err(Error::LastMessageNotTracked);
}
let message_id = self.last_message_id.load(Self::LOAD_ORDERING);
if message_id == 0 {
Ok(None)
} else {
Ok(Some(Id::new(message_id)))
}
}
#[must_use]
pub fn new(
client: Arc<Client>,
application_id: Id<ApplicationMarker>,
interaction_id: Id<InteractionMarker>,
token: String,
) -> Self {
Self {
application_id,
client,
id: interaction_id,
token,
is_responded: Arc::new(AtomicBool::new(false)),
last_message_id: Arc::new(AtomicU64::new(0)),
is_last_message_tracked: false,
}
}
pub async fn respond(&self, response: InteractionResponse) -> Result<FollowupResponse, Error> {
if self.is_responded() {
let followup_response = self.create_followup(response).await?;
if self.is_last_message_tracked {
let message = followup_response.model().await?;
self.set_last_message_id(message.id.get());
Ok(FollowupResponse::Deserialized(message))
} else {
Ok(FollowupResponse::NotDeserialized(followup_response))
}
} else {
self.client()
.create_response(self.id, &self.token, &response)
.await?;
self.set_is_responded(true);
Ok(FollowupResponse::None)
}
}
#[cfg(feature = "respond_on_delay")]
#[must_use]
pub fn respond_on_delay(self, response: InteractionResponse, delay: Duration) -> Self {
let handle = self.clone();
tokio::spawn(async move {
sleep(delay).await;
if !self.is_responded() {
drop(self.respond(response).await);
}
});
handle
}
fn set_is_responded(&self, value: bool) {
self.is_responded.store(value, Self::STORE_ORDERING);
}
fn set_last_message_id(&self, value: u64) {
self.last_message_id.store(value, Self::STORE_ORDERING);
}
#[must_use]
pub const fn track_last_message(mut self) -> Self {
self.is_last_message_tracked = true;
self
}
pub async fn update_last(
&self,
response: InteractionResponse,
) -> Result<Option<Response<Message>>, Error> {
let interaction_client = self.client();
if let Some(last_message_id) = self.last_message_id()? {
let mut update_followup =
interaction_client.update_followup(&self.token, last_message_id);
let Some(data) = response.data else {
return Ok(Some(update_followup.await?));
};
if let Some(attachments) = &data.attachments {
update_followup = update_followup.attachments(attachments);
}
update_followup = update_followup.components(data.components.as_deref());
update_followup = update_followup.content(data.content.as_deref());
update_followup = update_followup.embeds(data.embeds.as_deref());
Ok(Some(update_followup.await?))
} else {
self.respond(response).await?;
Ok(None)
}
}
}