1mod action_row;
6mod file_upload;
7mod label;
8mod select_menu;
9mod text_display;
10mod text_input;
11
12pub use self::{
13 action_row::ModalInteractionActionRow,
14 file_upload::ModalInteractionFileUpload,
15 label::ModalInteractionLabel,
16 select_menu::{
17 ModalInteractionChannelSelect, ModalInteractionMentionableSelect,
18 ModalInteractionRoleSelect, ModalInteractionStringSelect, ModalInteractionUserSelect,
19 },
20 text_display::ModalInteractionTextDisplay,
21 text_input::ModalInteractionTextInput,
22};
23use crate::application::interaction::InteractionDataResolved;
24use crate::application::interaction::modal::select_menu::ModalInteractionSelectMenu;
25use crate::channel::message::component::ComponentType;
26use crate::id::Id;
27use crate::id::marker::{ChannelMarker, GenericMarker, RoleMarker, UserMarker};
28use serde::{
29 Deserialize, Serialize, Serializer,
30 de::{Deserializer, Error as DeError, IgnoredAny, MapAccess, Visitor},
31 ser::SerializeStruct,
32};
33use serde_value::{DeserializerError, Value};
34use std::fmt::Formatter;
35
36#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
43pub struct ModalInteractionData {
44 pub components: Vec<ModalInteractionComponent>,
46 pub custom_id: String,
52 #[serde(skip_serializing_if = "Option::is_none")]
54 pub resolved: Option<InteractionDataResolved>,
55}
56
57#[derive(Clone, Debug, Eq, PartialEq)]
59pub enum ModalInteractionComponent {
60 Label(ModalInteractionLabel),
62 ActionRow(ModalInteractionActionRow),
64 StringSelect(ModalInteractionStringSelect),
66 UserSelect(ModalInteractionUserSelect),
68 RoleSelect(ModalInteractionRoleSelect),
70 MentionableSelect(ModalInteractionMentionableSelect),
72 ChannelSelect(ModalInteractionChannelSelect),
74 TextInput(ModalInteractionTextInput),
76 TextDisplay(ModalInteractionTextDisplay),
78 FileUpload(ModalInteractionFileUpload),
80 Unknown(u8),
82}
83
84impl ModalInteractionComponent {
85 pub fn kind(&self) -> ComponentType {
87 match self {
88 ModalInteractionComponent::Label(_) => ComponentType::Label,
89 ModalInteractionComponent::ActionRow(_) => ComponentType::ActionRow,
90 ModalInteractionComponent::StringSelect(_) => ComponentType::TextSelectMenu,
91 ModalInteractionComponent::UserSelect(_) => ComponentType::UserSelectMenu,
92 ModalInteractionComponent::RoleSelect(_) => ComponentType::RoleSelectMenu,
93 ModalInteractionComponent::MentionableSelect(_) => ComponentType::MentionableSelectMenu,
94 ModalInteractionComponent::ChannelSelect(_) => ComponentType::ChannelSelectMenu,
95 ModalInteractionComponent::TextInput(_) => ComponentType::TextInput,
96 ModalInteractionComponent::TextDisplay(_) => ComponentType::TextDisplay,
97 ModalInteractionComponent::FileUpload(_) => ComponentType::FileUpload,
98 ModalInteractionComponent::Unknown(unknown) => ComponentType::from(*unknown),
99 }
100 }
101}
102
103impl From<ModalInteractionLabel> for ModalInteractionComponent {
104 fn from(label: ModalInteractionLabel) -> Self {
105 Self::Label(label)
106 }
107}
108
109impl From<ModalInteractionActionRow> for ModalInteractionComponent {
110 fn from(action_row: ModalInteractionActionRow) -> Self {
111 Self::ActionRow(action_row)
112 }
113}
114
115impl From<ModalInteractionStringSelect> for ModalInteractionComponent {
116 fn from(select: ModalInteractionStringSelect) -> Self {
117 Self::StringSelect(select)
118 }
119}
120
121impl From<ModalInteractionUserSelect> for ModalInteractionComponent {
122 fn from(select: ModalInteractionUserSelect) -> Self {
123 Self::UserSelect(select)
124 }
125}
126
127impl From<ModalInteractionRoleSelect> for ModalInteractionComponent {
128 fn from(select: ModalInteractionRoleSelect) -> Self {
129 Self::RoleSelect(select)
130 }
131}
132
133impl From<ModalInteractionMentionableSelect> for ModalInteractionComponent {
134 fn from(select: ModalInteractionMentionableSelect) -> Self {
135 Self::MentionableSelect(select)
136 }
137}
138
139impl From<ModalInteractionChannelSelect> for ModalInteractionComponent {
140 fn from(select: ModalInteractionChannelSelect) -> Self {
141 Self::ChannelSelect(select)
142 }
143}
144
145impl From<ModalInteractionTextInput> for ModalInteractionComponent {
146 fn from(text_input: ModalInteractionTextInput) -> Self {
147 Self::TextInput(text_input)
148 }
149}
150
151impl From<ModalInteractionTextDisplay> for ModalInteractionComponent {
152 fn from(text_display: ModalInteractionTextDisplay) -> Self {
153 Self::TextDisplay(text_display)
154 }
155}
156
157impl From<ModalInteractionFileUpload> for ModalInteractionComponent {
158 fn from(file_upload: ModalInteractionFileUpload) -> Self {
159 Self::FileUpload(file_upload)
160 }
161}
162
163impl<'de> Deserialize<'de> for ModalInteractionComponent {
164 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
165 where
166 D: Deserializer<'de>,
167 {
168 deserializer.deserialize_any(ModalInteractionDataComponentVisitor)
169 }
170}
171
172#[derive(Debug, Deserialize)]
173#[serde(field_identifier, rename_all = "snake_case")]
174enum Field {
175 Component,
176 Components,
177 CustomId,
178 Id,
179 Type,
180 Value,
181 Values,
182}
183
184struct ModalInteractionDataComponentVisitor;
185
186impl<'de> Visitor<'de> for ModalInteractionDataComponentVisitor {
187 type Value = ModalInteractionComponent;
188
189 fn expecting(&self, f: &mut Formatter) -> std::fmt::Result {
190 f.write_str("struct ModalInteractionDataComponent")
191 }
192
193 #[allow(clippy::too_many_lines)]
194 fn visit_map<V: MapAccess<'de>>(self, mut map: V) -> Result<Self::Value, V::Error> {
195 fn deserialize_select_menu<'de, ValueType: Deserialize<'de>, Error: DeError>(
196 id: i32,
197 custom_id: Option<String>,
198 values: Option<Vec<Value>>,
199 ) -> Result<ModalInteractionSelectMenu<ValueType>, Error> {
200 let custom_id = custom_id.ok_or_else(|| DeError::missing_field("custom_id"))?;
201 let values = values
202 .ok_or_else(|| DeError::missing_field("values"))?
203 .into_iter()
204 .map(Value::deserialize_into)
205 .collect::<Result<_, _>>()
206 .map_err(DeserializerError::into_error)?;
207
208 Ok(ModalInteractionSelectMenu {
209 id,
210 custom_id,
211 values,
212 })
213 }
214
215 let mut id: Option<i32> = None;
217 let mut kind: Option<ComponentType> = None;
218 let mut custom_id: Option<String> = None;
219 let mut values: Option<Vec<Value>> = None;
220 let mut components: Option<Vec<ModalInteractionComponent>> = None;
221 let mut component: Option<ModalInteractionComponent> = None;
222 let mut value: Option<String> = None;
223
224 loop {
225 let key = match map.next_key() {
226 Ok(Some(key)) => key,
227 Ok(None) => break,
228 Err(_) => {
229 map.next_value::<IgnoredAny>()?;
230
231 continue;
232 }
233 };
234
235 match key {
236 Field::Component => {
237 if component.is_some() {
238 return Err(DeError::duplicate_field("component"));
239 }
240
241 component = Some(map.next_value()?);
242 }
243 Field::Components => {
244 if components.is_some() {
245 return Err(DeError::duplicate_field("components"));
246 }
247
248 components = Some(map.next_value()?);
249 }
250 Field::CustomId => {
251 if custom_id.is_some() {
252 return Err(DeError::duplicate_field("custom_id"));
253 }
254
255 custom_id = Some(map.next_value()?);
256 }
257 Field::Id => {
258 if id.is_some() {
259 return Err(DeError::duplicate_field("id"));
260 }
261
262 id = Some(map.next_value()?);
263 }
264 Field::Type => {
265 if kind.is_some() {
266 return Err(DeError::duplicate_field("kind"));
267 }
268
269 kind = Some(map.next_value()?);
270 }
271 Field::Value => {
272 if value.is_some() {
273 return Err(DeError::duplicate_field("value"));
274 }
275
276 value = Some(map.next_value()?);
277 }
278 Field::Values => {
279 if values.is_some() {
280 return Err(DeError::duplicate_field("values"));
281 }
282
283 values = Some(map.next_value()?);
284 }
285 }
286 }
287
288 let kind = kind.ok_or_else(|| DeError::missing_field("type"))?;
289 let id = id.ok_or_else(|| DeError::missing_field("id"))?;
290
291 Ok(match kind {
292 ComponentType::ActionRow => {
293 let components = components.ok_or_else(|| DeError::missing_field("components"))?;
294
295 Self::Value::ActionRow(ModalInteractionActionRow { id, components })
296 }
297 ComponentType::TextSelectMenu => {
298 Self::Value::StringSelect(deserialize_select_menu::<String, _>(
299 id, custom_id, values,
300 )?)
301 }
302 ComponentType::UserSelectMenu => {
303 Self::Value::UserSelect(deserialize_select_menu::<Id<UserMarker>, _>(
304 id, custom_id, values,
305 )?)
306 }
307 ComponentType::RoleSelectMenu => {
308 Self::Value::RoleSelect(deserialize_select_menu::<Id<RoleMarker>, _>(
309 id, custom_id, values,
310 )?)
311 }
312 ComponentType::MentionableSelectMenu => Self::Value::MentionableSelect(
313 deserialize_select_menu::<Id<GenericMarker>, _>(id, custom_id, values)?,
314 ),
315 ComponentType::ChannelSelectMenu => Self::Value::ChannelSelect(
316 deserialize_select_menu::<Id<ChannelMarker>, _>(id, custom_id, values)?,
317 ),
318 ComponentType::TextInput => {
319 let custom_id = custom_id.ok_or_else(|| DeError::missing_field("custom_id"))?;
320 let value = value.ok_or_else(|| DeError::missing_field("value"))?;
321
322 Self::Value::TextInput(ModalInteractionTextInput {
323 custom_id,
324 id,
325 value,
326 })
327 }
328 ComponentType::TextDisplay => {
329 Self::Value::TextDisplay(ModalInteractionTextDisplay { id })
330 }
331 ComponentType::Label => {
332 let component = component.ok_or_else(|| DeError::missing_field("component"))?;
333
334 Self::Value::Label(ModalInteractionLabel {
335 id,
336 component: Box::new(component),
337 })
338 }
339 ComponentType::FileUpload => {
340 let custom_id = custom_id.ok_or_else(|| DeError::missing_field("custom_id"))?;
341 let values = values
342 .ok_or_else(|| DeError::missing_field("values"))?
343 .into_iter()
344 .map(Value::deserialize_into)
345 .collect::<Result<_, _>>()
346 .map_err(DeserializerError::into_error)?;
347
348 Self::Value::FileUpload(ModalInteractionFileUpload {
349 id,
350 custom_id,
351 values,
352 })
353 }
354 ComponentType::Button
355 | ComponentType::Section
356 | ComponentType::Thumbnail
357 | ComponentType::MediaGallery
358 | ComponentType::File
359 | ComponentType::Separator
360 | ComponentType::Container
361 | ComponentType::Unknown(_) => Self::Value::Unknown(kind.into()),
362 })
363 }
364}
365
366impl Serialize for ModalInteractionComponent {
367 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
368 fn serialize_select_menu<State: SerializeStruct, ValueType: Serialize>(
369 state: &mut State,
370 select: &ModalInteractionSelectMenu<ValueType>,
371 ) -> Result<(), <State as SerializeStruct>::Error> {
372 state.serialize_field("custom_id", &select.custom_id)?;
373 state.serialize_field("id", &select.id)?;
374 state.serialize_field("values", &select.values)?;
375 Ok(())
376 }
377
378 #[allow(clippy::match_same_arms)]
379 let len = match self {
380 ModalInteractionComponent::Label(_) => 3,
385 ModalInteractionComponent::ActionRow(_) => 3,
390 ModalInteractionComponent::StringSelect(_)
396 | ModalInteractionComponent::UserSelect(_)
397 | ModalInteractionComponent::RoleSelect(_)
398 | ModalInteractionComponent::MentionableSelect(_)
399 | ModalInteractionComponent::ChannelSelect(_) => 4,
400 ModalInteractionComponent::TextInput(_) => 4,
406 ModalInteractionComponent::TextDisplay(_) => 2,
410 ModalInteractionComponent::FileUpload(_) => 4,
416 ModalInteractionComponent::Unknown(_) => 1,
419 };
420
421 let mut state = serializer.serialize_struct("ModalInteractionComponent", len)?;
422 state.serialize_field("type", &self.kind())?;
423
424 match self {
425 ModalInteractionComponent::Label(label) => {
426 state.serialize_field("id", &label.id)?;
427 state.serialize_field("component", &label.component)?;
428 }
429 ModalInteractionComponent::ActionRow(action_row) => {
430 state.serialize_field("id", &action_row.id)?;
431 state.serialize_field("components", &action_row.components)?;
432 }
433 ModalInteractionComponent::StringSelect(select) => {
434 serialize_select_menu(&mut state, select)?;
435 }
436 ModalInteractionComponent::UserSelect(select) => {
437 serialize_select_menu(&mut state, select)?;
438 }
439 ModalInteractionComponent::RoleSelect(select) => {
440 serialize_select_menu(&mut state, select)?;
441 }
442 ModalInteractionComponent::MentionableSelect(select) => {
443 serialize_select_menu(&mut state, select)?;
444 }
445 ModalInteractionComponent::ChannelSelect(select) => {
446 serialize_select_menu(&mut state, select)?;
447 }
448 ModalInteractionComponent::TextInput(text_input) => {
449 state.serialize_field("custom_id", &text_input.custom_id)?;
450 state.serialize_field("id", &text_input.id)?;
451 state.serialize_field("value", &text_input.value)?;
452 }
453 ModalInteractionComponent::TextDisplay(text_display) => {
454 state.serialize_field("id", &text_display.id)?;
455 }
456 ModalInteractionComponent::FileUpload(file_upload) => {
457 state.serialize_field("custom_id", &file_upload.custom_id)?;
458 state.serialize_field("id", &file_upload.id)?;
459 state.serialize_field("values", &file_upload.values)?;
460 }
461 ModalInteractionComponent::Unknown(_) => {}
465 }
466
467 state.end()
468 }
469}
470
471#[cfg(test)]
472mod tests {
473 use super::*;
474 use crate::application::interaction::InteractionChannel;
475 use crate::channel::ChannelType;
476 use crate::guild::Permissions;
477 use serde_test::Token;
478 use static_assertions::{assert_fields, assert_impl_all};
479 use std::collections::HashMap;
480 use std::fmt::Debug;
481
482 assert_fields!(ModalInteractionData: custom_id, components);
483 assert_impl_all!(
484 ModalInteractionData: Clone,
485 Debug,
486 Deserialize<'static>,
487 PartialEq,
488 Send,
489 Serialize,
490 Sync
491 );
492
493 assert_impl_all!(
494 ModalInteractionComponent: Clone,
495 Debug,
496 Deserialize<'static>,
497 Eq,
498 PartialEq,
499 Send,
500 Serialize,
501 Sync
502 );
503
504 fn label_tokens(id: i32, component_tokens: &[Token]) -> Vec<Token> {
505 let mut label_tokens = vec![
506 Token::Struct {
507 name: "ModalInteractionComponent",
508 len: 3,
509 },
510 Token::String("type"),
511 Token::U8(ComponentType::Label.into()),
512 Token::String("id"),
513 Token::I32(id),
514 Token::String("component"),
515 ];
516 label_tokens.extend_from_slice(component_tokens);
517 label_tokens.push(Token::StructEnd);
518
519 label_tokens
520 }
521
522 #[test]
523 fn modal_action_rows() {
524 let value = ModalInteractionData {
525 custom_id: "test-modal".to_owned(),
526 components: vec![ModalInteractionComponent::ActionRow(
527 ModalInteractionActionRow {
528 id: 1,
529 components: vec![ModalInteractionComponent::TextInput(
530 ModalInteractionTextInput {
531 custom_id: "the-data-id".to_owned(),
532 id: 2,
533 value: "input value".to_owned(),
534 },
535 )],
536 },
537 )],
538 resolved: None,
539 };
540
541 serde_test::assert_tokens(
542 &value,
543 &[
544 Token::Struct {
545 name: "ModalInteractionData",
546 len: 2,
547 },
548 Token::String("components"),
549 Token::Seq { len: Some(1) },
550 Token::Struct {
551 name: "ModalInteractionComponent",
552 len: 3,
553 },
554 Token::String("type"),
555 Token::U8(ComponentType::ActionRow.into()),
556 Token::String("id"),
557 Token::I32(1),
558 Token::String("components"),
559 Token::Seq { len: Some(1) },
560 Token::Struct {
561 name: "ModalInteractionComponent",
562 len: 4,
563 },
564 Token::String("type"),
565 Token::U8(ComponentType::TextInput.into()),
566 Token::String("custom_id"),
567 Token::String("the-data-id"),
568 Token::String("id"),
569 Token::I32(2),
570 Token::String("value"),
571 Token::String("input value"),
572 Token::StructEnd,
573 Token::SeqEnd,
574 Token::StructEnd,
575 Token::SeqEnd,
576 Token::String("custom_id"),
577 Token::String("test-modal"),
578 Token::StructEnd,
579 ],
580 );
581 }
582
583 #[test]
584 #[allow(clippy::too_many_lines)]
585 fn modal_labels() {
586 let value = ModalInteractionData {
587 custom_id: "test-modal".to_owned(),
588 components: vec![
589 ModalInteractionComponent::Label(ModalInteractionLabel {
590 id: 1,
591 component: Box::new(ModalInteractionComponent::TextInput(
592 ModalInteractionTextInput {
593 custom_id: "the-text-input-id".to_owned(),
594 id: 2,
595 value: "input value".to_owned(),
596 },
597 )),
598 }),
599 ModalInteractionComponent::Label(ModalInteractionLabel {
600 id: 3,
601 component: Box::new(ModalInteractionComponent::TextDisplay(
602 ModalInteractionTextDisplay { id: 4 },
603 )),
604 }),
605 ModalInteractionComponent::Label(ModalInteractionLabel {
606 id: 5,
607 component: Box::new(ModalInteractionComponent::ChannelSelect(
608 ModalInteractionChannelSelect {
609 id: 6,
610 custom_id: "the-channel-select-id".to_owned(),
611 values: vec![Id::new(42)],
612 },
613 )),
614 }),
615 ],
616 resolved: Some(InteractionDataResolved {
617 attachments: HashMap::new(),
618 channels: HashMap::from([(
619 Id::new(42),
620 InteractionChannel {
621 id: Id::new(42),
622 kind: ChannelType::GuildText,
623 name: "the-channel-name".to_owned(),
624 parent_id: None,
625 permissions: Permissions::empty(),
626 thread_metadata: None,
627 },
628 )]),
629 members: HashMap::new(),
630 messages: HashMap::new(),
631 roles: HashMap::new(),
632 users: HashMap::new(),
633 }),
634 };
635
636 let text_input_tokens = [
637 Token::Struct {
638 name: "ModalInteractionComponent",
639 len: 4,
640 },
641 Token::String("type"),
642 Token::U8(ComponentType::TextInput.into()),
643 Token::String("custom_id"),
644 Token::String("the-text-input-id"),
645 Token::String("id"),
646 Token::I32(2),
647 Token::String("value"),
648 Token::String("input value"),
649 Token::StructEnd,
650 ];
651
652 let text_display_tokens = [
653 Token::Struct {
654 name: "ModalInteractionComponent",
655 len: 2,
656 },
657 Token::String("type"),
658 Token::U8(ComponentType::TextDisplay.into()),
659 Token::String("id"),
660 Token::I32(4),
661 Token::StructEnd,
662 ];
663
664 let channel_select_tokens = [
665 Token::Struct {
666 name: "ModalInteractionComponent",
667 len: 4,
668 },
669 Token::String("type"),
670 Token::U8(ComponentType::ChannelSelectMenu.into()),
671 Token::String("custom_id"),
672 Token::String("the-channel-select-id"),
673 Token::String("id"),
674 Token::I32(6),
675 Token::String("values"),
676 Token::Seq { len: Some(1) },
677 Token::NewtypeStruct { name: "Id" },
678 Token::String("42"),
679 Token::SeqEnd,
680 Token::StructEnd,
681 ];
682
683 let mut all_tokens = vec![
684 Token::Struct {
685 name: "ModalInteractionData",
686 len: 3,
687 },
688 Token::String("components"),
689 Token::Seq { len: Some(3) },
690 ];
691
692 all_tokens.extend_from_slice(&label_tokens(1, &text_input_tokens));
693 all_tokens.extend_from_slice(&label_tokens(3, &text_display_tokens));
694 all_tokens.extend_from_slice(&label_tokens(5, &channel_select_tokens));
695
696 all_tokens.extend_from_slice(&[
697 Token::SeqEnd,
698 Token::String("custom_id"),
699 Token::String("test-modal"),
700 Token::String("resolved"),
701 Token::Some,
702 Token::Struct {
703 name: "InteractionDataResolved",
704 len: 1,
705 },
706 Token::String("channels"),
707 Token::Map { len: Some(1) },
708 Token::NewtypeStruct { name: "Id" },
709 Token::String("42"),
710 Token::Struct {
711 name: "InteractionChannel",
712 len: 4,
713 },
714 Token::String("id"),
715 Token::NewtypeStruct { name: "Id" },
716 Token::String("42"),
717 Token::String("type"),
718 Token::U8(0),
719 Token::String("name"),
720 Token::String("the-channel-name"),
721 Token::String("permissions"),
722 Token::String("0"),
723 Token::StructEnd,
724 Token::MapEnd,
725 Token::StructEnd,
726 Token::StructEnd,
727 ]);
728
729 serde_test::assert_tokens(&value, &all_tokens);
730 }
731
732 #[test]
733 fn modal_file_upload() {
734 let value = ModalInteractionData {
735 custom_id: "test-modal".to_owned(),
736 components: vec![ModalInteractionComponent::FileUpload(
737 ModalInteractionFileUpload {
738 id: 42,
739 custom_id: "file-upload".to_owned(),
740 values: vec![Id::new(1), Id::new(2)],
741 },
742 )],
743 resolved: None,
746 };
747
748 serde_test::assert_tokens(
749 &value,
750 &[
751 Token::Struct {
752 name: "ModalInteractionData",
753 len: 2,
754 },
755 Token::String("components"),
756 Token::Seq { len: Some(1) },
757 Token::Struct {
758 name: "ModalInteractionComponent",
759 len: 4,
760 },
761 Token::String("type"),
762 Token::U8(ComponentType::FileUpload.into()),
763 Token::String("custom_id"),
764 Token::String("file-upload"),
765 Token::String("id"),
766 Token::I32(42),
767 Token::String("values"),
768 Token::Seq { len: Some(2) },
769 Token::NewtypeStruct { name: "Id" },
770 Token::Str("1"),
771 Token::NewtypeStruct { name: "Id" },
772 Token::Str("2"),
773 Token::SeqEnd,
774 Token::StructEnd,
775 Token::SeqEnd,
776 Token::String("custom_id"),
777 Token::String("test-modal"),
778 Token::StructEnd,
779 ],
780 )
781 }
782}