use std::{
cell::Cell,
fmt::{self, Display, Write},
};
use crate::types::{
AllowedMentions, Attachment, Component, File, InteractionResponse, MessageCallbackData,
MessageComponentData, MessageFlags, ModalCallbackData, PartialChannel, PartialEmoji, Role,
Snowflake, User,
};
use super::{
CommandOutput, ComponentCtx, Extras, HandlerError, IntoResponse, ModalChoice, ModalCtx, Update,
custom_id::CustomIdError, limits,
};
#[derive(Debug, Clone)]
#[must_use]
pub struct View {
pub(crate) components: Vec<Component>,
pub(crate) extras: Extras,
}
impl Default for View {
fn default() -> Self {
Self::new(Vec::new())
}
}
impl View {
pub fn new(components: Vec<Component>) -> Self {
Self {
components,
extras: Extras::default(),
}
}
pub fn components(&self) -> &[Component] {
&self.components
}
pub fn files(&self) -> &[File] {
self.extras.files()
}
pub fn flags(&self) -> MessageFlags {
self.extras.flags()
}
pub fn ephemeral(mut self) -> Self {
self.extras.inner().flags |= MessageFlags::Ephemeral;
self
}
pub fn suppress_notifications(mut self) -> Self {
self.extras.inner().flags |= MessageFlags::SuppressNotifications;
self
}
pub fn allowed_mentions(mut self, allowed_mentions: AllowedMentions) -> Self {
self.extras.inner().allowed_mentions = Some(allowed_mentions);
self
}
pub fn file(mut self, file: File) -> Self {
self.extras.inner().files.push(file);
self
}
#[cfg_attr(not(feature = "rest"), allow(dead_code))]
pub(crate) fn mentions(&self) -> Option<&AllowedMentions> {
self.extras.mentions()
}
#[cfg_attr(not(feature = "rest"), allow(dead_code))]
pub(crate) fn message_flags(&self, ephemeral: bool) -> MessageFlags {
let flags = MessageFlags::IsComponentsV2 | self.extras.flags();
if ephemeral {
flags
} else {
flags - MessageFlags::Ephemeral
}
}
pub fn check(&self) -> Result<(), ViewError> {
if let Some(error) = self.extras.custom_id_error() {
return Err(ViewError::CustomId(error.clone()));
}
limits::check_message(&self.components)
}
pub fn into_components(self) -> Result<Vec<Component>, CustomIdError> {
match self.extras.into_inner().error {
Some(error) => Err(error),
None => Ok(self.components),
}
}
pub fn into_message_data(self) -> Result<MessageCallbackData, ViewError> {
self.check()?;
let mut data = MessageCallbackData::components_v2(self.components);
let extras = self.extras.into_inner();
data.flags = Some(MessageFlags::IsComponentsV2 | extras.flags);
data.allowed_mentions = extras.allowed_mentions;
data.files = extras.files;
Ok(data)
}
}
impl From<Vec<Component>> for View {
fn from(components: Vec<Component>) -> Self {
Self::new(components)
}
}
impl IntoResponse for View {
fn into_response(self) -> InteractionResponse {
self.try_into_response()
.unwrap_or_else(|error| error.response())
}
fn try_into_response(self) -> Result<InteractionResponse, HandlerError> {
self.into_message_data()
.map(InteractionResponse::ChannelMessageWithSource)
.map_err(HandlerError::new)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ViewError {
CustomId(CustomIdError),
Limit {
what: &'static str,
min: usize,
max: usize,
found: usize,
},
Duplicate {
what: &'static str,
value: String,
},
}
impl Display for ViewError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ViewError::CustomId(error) => error.fmt(f),
ViewError::Limit {
what,
min: 0,
max,
found,
} => write!(f, "{found} {what}, Discord allows at most {max}"),
ViewError::Limit {
what,
min,
max,
found,
} => write!(f, "{found} {what}, Discord allows {min} to {max}"),
ViewError::Duplicate { what, value } => {
write!(
f,
"{what} {value:?} repeats, Discord requires each to be unique"
)
}
}
}
}
impl std::error::Error for ViewError {}
impl From<CustomIdError> for ViewError {
fn from(error: CustomIdError) -> Self {
ViewError::CustomId(error)
}
}
#[derive(Debug, Clone)]
#[must_use]
pub struct ModalView {
pub(crate) data: ModalCallbackData,
pub(crate) error: Option<CustomIdError>,
}
impl ModalView {
pub fn data(&self) -> &ModalCallbackData {
&self.data
}
pub fn check(&self) -> Result<(), ViewError> {
if let Some(error) = &self.error {
return Err(ViewError::CustomId(error.clone()));
}
limits::check_modal(&self.data)
}
pub fn into_data(self) -> Result<ModalCallbackData, CustomIdError> {
match self.error {
Some(error) => Err(error),
None => Ok(self.data),
}
}
}
impl IntoResponse for ModalView {
fn into_response(self) -> InteractionResponse {
self.try_into_response()
.unwrap_or_else(|error| error.response())
}
fn try_into_response(self) -> Result<InteractionResponse, HandlerError> {
self.check().map_err(HandlerError::new)?;
Ok(InteractionResponse::Modal(self.data))
}
}
#[diagnostic::on_unimplemented(
message = "`{Self}` is not a View",
label = "not a View",
note = "a View is `rsx!`'s output or a #[view]'s Props"
)]
pub trait IntoView {
fn into_view(self) -> View;
}
impl IntoView for View {
fn into_view(self) -> View {
self
}
}
#[diagnostic::on_unimplemented(
message = "`{Self}` is not media",
note = "use a URL (&str or String), a File to upload with the message, or an &Attachment"
)]
pub trait IntoMedia {
fn into_media(self, extras: &mut Extras) -> String;
}
impl IntoMedia for &str {
fn into_media(self, _extras: &mut Extras) -> String {
self.to_string()
}
}
impl IntoMedia for String {
fn into_media(self, _extras: &mut Extras) -> String {
self
}
}
impl IntoMedia for &String {
fn into_media(self, _extras: &mut Extras) -> String {
self.clone()
}
}
impl IntoMedia for File {
fn into_media(self, extras: &mut Extras) -> String {
let url = format!("attachment://{}", self.name);
extras.inner().files.push(self);
url
}
}
impl IntoMedia for &Attachment {
fn into_media(self, _extras: &mut Extras) -> String {
self.url.clone()
}
}
#[diagnostic::on_unimplemented(
message = "`{Self}` cannot be placed in text",
note = "use &str, String, a number, or an Option of one; call `.to_string()` on other Display types"
)]
pub trait IntoText {
fn push_text(self, text: &mut String);
}
impl IntoText for &str {
fn push_text(self, text: &mut String) {
text.push_str(self);
}
}
impl IntoText for String {
fn push_text(self, text: &mut String) {
text.push_str(&self);
}
}
impl IntoText for &String {
fn push_text(self, text: &mut String) {
text.push_str(self);
}
}
impl<T: IntoText> IntoText for Option<T> {
fn push_text(self, text: &mut String) {
if let Some(value) = self {
value.push_text(text);
}
}
}
macro_rules! text {
($($ty:ty),*) => {$(
impl IntoText for $ty {
fn push_text(self, text: &mut String) {
let _ = write!(text, "{self}");
}
}
)*};
}
text!(
char,
u8,
u16,
u32,
u64,
u128,
usize,
i8,
i16,
i32,
i64,
i128,
isize,
f32,
f64,
Snowflake,
std::fmt::Arguments<'_>
);
#[diagnostic::on_unimplemented(
message = "`{Self}` is not a valid value for this attribute",
note = "the attribute expects `{T}`"
)]
pub trait IntoAttr<T> {
fn into_attr(self) -> T;
}
impl<T> IntoAttr<T> for T {
fn into_attr(self) -> T {
self
}
}
impl<T> IntoAttr<Option<T>> for T {
fn into_attr(self) -> Option<T> {
Some(self)
}
}
impl IntoAttr<String> for &str {
fn into_attr(self) -> String {
self.to_string()
}
}
impl IntoAttr<Option<String>> for &str {
fn into_attr(self) -> Option<String> {
Some(self.to_string())
}
}
impl IntoAttr<String> for &String {
fn into_attr(self) -> String {
self.clone()
}
}
impl IntoAttr<Option<String>> for &String {
fn into_attr(self) -> Option<String> {
Some(self.clone())
}
}
impl IntoAttr<Option<PartialEmoji>> for &str {
fn into_attr(self) -> Option<PartialEmoji> {
Some(PartialEmoji {
id: None,
name: Some(self.to_string()),
animated: None,
})
}
}
#[diagnostic::on_unimplemented(message = "`{Self}` is not a message View")]
pub trait Rerender {
fn rerender(self) -> Result<InteractionResponse, HandlerError>;
}
impl Rerender for View {
fn rerender(self) -> Result<InteractionResponse, HandlerError> {
Update(self).try_into_response()
}
}
impl<T: Rerender, E: Display + 'static> Rerender for Result<T, E> {
fn rerender(self) -> Result<InteractionResponse, HandlerError> {
self.map_err(super::response::into_handler_error)?
.rerender()
}
}
#[doc(hidden)]
pub struct HandlerOutput<T>(Cell<Option<T>>);
impl<T> HandlerOutput<T> {
pub fn new(output: T) -> Self {
Self(Cell::new(Some(output)))
}
fn take(&self) -> T {
self.0.take().expect("handler output answered once")
}
}
#[doc(hidden)]
pub trait ViaRerender {
fn answer(&self) -> Result<InteractionResponse, HandlerError>;
}
impl<T: Rerender> ViaRerender for &HandlerOutput<T> {
fn answer(&self) -> Result<InteractionResponse, HandlerError> {
self.take().rerender()
}
}
#[doc(hidden)]
pub trait ViaOutput {
fn answer(&self) -> Result<InteractionResponse, HandlerError>;
}
impl<T: CommandOutput> ViaOutput for HandlerOutput<T> {
fn answer(&self) -> Result<InteractionResponse, HandlerError> {
self.take().into_result()
}
}
#[derive(Debug, Clone, Copy)]
pub struct ViewInfo {
pub prefix: &'static str,
pub handled: bool,
pub views: &'static [ViewInfo],
}
pub trait ViewDefinition {
const INFO: ViewInfo;
}
pub trait ViewHandler<S>: ViewDefinition {
fn component(
_ctx: ComponentCtx<'_, S>,
_fields: Option<&str>,
) -> impl Future<Output = Option<Result<InteractionResponse, HandlerError>>> + Send {
async { None }
}
fn modal(
_ctx: ModalCtx<'_, S>,
_fields: Option<&str>,
) -> impl Future<Output = Option<Result<InteractionResponse, HandlerError>>> + Send {
async { None }
}
}
#[diagnostic::on_unimplemented(
message = "`{Self}` cannot hold select values",
note = "use String, Snowflake, User, Role, PartialChannel, Attachment, or a #[derive(Choice)] enum, optionally in Option or Vec"
)]
pub trait SelectValue: Sized {
const MANY: bool = false;
fn extract(data: &MessageComponentData) -> Result<Self, String>;
}
fn selected(data: &MessageComponentData) -> &[String] {
data.values.as_deref().unwrap_or_default()
}
macro_rules! select_value {
($ty:ty, |$value:ident, $data:ident| $parse:expr) => {
impl SelectValue for $ty {
fn extract(data: &MessageComponentData) -> Result<Self, String> {
let parse =
|$value: &str, $data: &MessageComponentData| -> Result<$ty, String> { $parse };
match selected(data).first() {
Some(value) => parse(value, data),
None => Err("nothing selected".to_string()),
}
}
}
impl SelectValue for Option<$ty> {
fn extract(data: &MessageComponentData) -> Result<Self, String> {
match selected(data).first() {
Some(_) => <$ty as SelectValue>::extract(data).map(Some),
None => Ok(None),
}
}
}
impl SelectValue for Vec<$ty> {
const MANY: bool = true;
fn extract(data: &MessageComponentData) -> Result<Self, String> {
let parse =
|$value: &str, $data: &MessageComponentData| -> Result<$ty, String> { $parse };
selected(data)
.iter()
.map(|value| parse(value, data))
.collect()
}
}
};
}
select_value!(String, |value, _data| Ok(value.to_string()));
select_value!(Snowflake, |value, _data| value
.parse()
.map_err(|_| format!("`{value}` is not an id")));
select_value!(User, |value, data| resolve(value, data, |r| r
.users
.as_ref()));
select_value!(Role, |value, data| resolve(value, data, |r| r
.roles
.as_ref()));
select_value!(PartialChannel, |value, data| resolve(value, data, |r| r
.channels
.as_ref()));
select_value!(Attachment, |value, data| resolve(value, data, |r| r
.attachments
.as_ref()));
fn resolve<T: Clone>(
value: &str,
data: &MessageComponentData,
get: impl Fn(&crate::types::ResolvedData) -> Option<&crate::types::SnowflakeMap<T>>,
) -> Result<T, String> {
let id: Snowflake = value
.parse()
.map_err(|_| format!("`{value}` is not an id"))?;
data.resolved
.as_ref()
.and_then(get)
.and_then(|map| map.get(&id))
.cloned()
.ok_or_else(|| format!("{id} is not in the resolved data"))
}
fn choice<C: ModalChoice>(value: &str) -> Result<C, String> {
C::from_modal_value(value).ok_or_else(|| format!("`{value}` is not a choice"))
}
impl<C: ModalChoice> SelectValue for C {
fn extract(data: &MessageComponentData) -> Result<Self, String> {
match selected(data).first() {
Some(value) => choice(value),
None => Err("nothing selected".to_string()),
}
}
}
impl<C: ModalChoice> SelectValue for Option<C> {
fn extract(data: &MessageComponentData) -> Result<Self, String> {
selected(data)
.first()
.map(|value| choice(value))
.transpose()
}
}
impl<C: ModalChoice> SelectValue for Vec<C> {
const MANY: bool = true;
fn extract(data: &MessageComponentData) -> Result<Self, String> {
selected(data).iter().map(|value| choice(value)).collect()
}
}
#[doc(hidden)]
pub fn select_value<T: SelectValue>(data: &MessageComponentData) -> Result<T, HandlerError> {
T::extract(data).map_err(|e| HandlerError::invalid_option(&data.custom_id, e))
}
#[doc(hidden)]
pub fn modal_value<T: super::ModalValue>(
data: &crate::types::ModalSubmitData,
name: &str,
) -> Result<T, HandlerError> {
T::extract(data.find(name), data).map_err(|e| HandlerError::invalid_option(name, e))
}
#[doc(hidden)]
pub fn choice_options<C: ModalChoice>() -> &'static [(&'static str, &'static str)] {
C::OPTIONS
}
#[doc(hidden)]
pub const fn required<T: super::ModalValue>() -> Option<bool> {
if T::REQUIRED { None } else { Some(false) }
}