Formulario de tickets
Un sistema de soporte en tres pasos: un mensaje con botón, un modal con el formulario y un resumen del ticket.
-
El panel. Un mensaje fijo en el canal de soporte:
Vista previa payload.json {"flags": 32768,"components": [{"type": 17,"accent_color": 5793266,"components": [{"type": 9,"components": [{"type": 10,"content": "## 🎫 Soporte\n¿Tienes un problema? Abre un ticket y te ayudaremos."}],"accessory": {"type": 2,"style": 1,"label": "Abrir ticket","custom_id": "abrir_ticket"}}]}]} -
El modal. Al pulsar el botón, el bot responde con este modal:
Vista previa Nuevo ticketCategoría*Elige una categoríaDescribe el problema*Capturas (opcional)Arrastra archivos o haz clic para subirmodal.json {"type": 9,"data": {"custom_id": "ticket_modal","title": "Nuevo ticket","components": [{"type": 18,"label": "Categoría","component": {"type": 3,"custom_id": "categoria","placeholder": "Elige una categoría","options": [{"label": "Problema técnico","value": "tecnico","emoji": {"name": "🛠️"}},{"label": "Reportar usuario","value": "reporte","emoji": {"name": "🚨"}},{"label": "Otra cosa","value": "otro","emoji": {"name": "💬"}}]}},{"type": 18,"label": "Describe el problema","component": {"type": 4,"custom_id": "descripcion","style": 2,"min_length": 20,"max_length": 1500}},{"type": 18,"label": "Capturas (opcional)","component": {"type": 19,"custom_id": "capturas","required": false,"min_values": 0,"max_values": 3}}]}} -
El resumen. Al enviar el modal, el bot crea el ticket y responde con un resumen:
Vista previa payload.json {"flags": 32768,"components": [{"type": 17,"accent_color": 16705372,"components": [{"type": 10,"content": "## Ticket #0042 · 🛠️ Problema técnico"},{"type": 10,"content": "-# Abierto por <@123456789012345678> <t:1793620800:R>"},{"type": 14},{"type": 10,"content": "> El bot no responde al comando /perfil desde ayer."},{"type": 1,"components": [{"type": 2,"style": 3,"label": "Atender","custom_id": "ticket_atender:42"},{"type": 2,"style": 4,"label": "Cerrar","custom_id": "ticket_cerrar:42"}]}]}]}
- Al recibir
custom_id: "abrir_ticket"responde con el JSON del modal (type: 9). - Al recibir el
MODAL_SUBMITconcustom_id: "ticket_modal", lee los valores y responde contype: 4y el resumen.
client.on(Events.InteractionCreate, async (interaction) => { // Paso 2: abrir el modal if (interaction.isButton() && interaction.customId === 'abrir_ticket') { await interaction.showModal(ticketModal); // el objeto "data" del modal de arriba return; }
// Paso 3: recibir el formulario if (interaction.isModalSubmit() && interaction.customId === 'ticket_modal') { const categoria = interaction.fields.getStringSelectValues('categoria')[0]; const descripcion = interaction.fields.getTextInputValue('descripcion');
const summary = new ContainerBuilder() .setAccentColor(0xfee75c) .addTextDisplayComponents( (text) => text.setContent(`## Ticket · ${categoria}`), (text) => text.setContent(`-# Abierto por ${interaction.user}`), ) .addSeparatorComponents((separator) => separator) .addTextDisplayComponents((text) => text.setContent(`> ${descripcion}`));
await interaction.reply({ flags: MessageFlags.IsComponentsV2, components: [summary] }); }});class TicketModal(discord.ui.Modal, title="Nuevo ticket"): categoria = discord.ui.Label( text="Categoría", component=discord.ui.Select( placeholder="Elige una categoría", options=[ discord.SelectOption(label="Problema técnico", value="tecnico", emoji="🛠️"), discord.SelectOption(label="Reportar usuario", value="reporte", emoji="🚨"), discord.SelectOption(label="Otra cosa", value="otro", emoji="💬"), ], ), ) descripcion = discord.ui.Label( text="Describe el problema", component=discord.ui.TextInput(style=discord.TextStyle.paragraph, min_length=20, max_length=1500), ) capturas = discord.ui.Label( text="Capturas (opcional)", component=discord.ui.FileUpload(required=False, min_values=0, max_values=3), )
async def on_submit(self, interaction: discord.Interaction): view = discord.ui.LayoutView() view.add_item( discord.ui.Container( discord.ui.TextDisplay(f"## Ticket · {self.categoria.component.values[0]}"), discord.ui.TextDisplay(f"-# Abierto por {interaction.user.mention}"), discord.ui.Separator(), discord.ui.TextDisplay(f"> {self.descripcion.component.value}"), accent_colour=0xFEE75C, ) ) await interaction.response.send_message(view=view)
# En el callback del botón "abrir_ticket":# await interaction.response.send_modal(TicketModal())s.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) { switch i.Type { case discordgo.InteractionMessageComponent: if i.MessageComponentData().CustomID == "abrir_ticket" { s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseModal, Data: ticketModalData(), // devuelve *discordgo.InteractionResponseData con el modal }) }
case discordgo.InteractionModalSubmit: data := i.ModalSubmitData() if data.CustomID != "ticket_modal" { return } var categoria, descripcion string for _, c := range data.Components { label, ok := c.(*discordgo.Label) if !ok { continue } switch input := label.Component.(type) { case *discordgo.SelectMenu: if len(input.Values) > 0 { categoria = input.Values[0] } case *discordgo.TextInput: descripcion = input.Value } }
color := 0xFEE75C s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ Flags: discordgo.MessageFlagsIsComponentsV2, Components: []discordgo.MessageComponent{ discordgo.Container{ AccentColor: &color, Components: []discordgo.MessageComponent{ discordgo.TextDisplay{Content: "## Ticket · " + categoria}, discordgo.Separator{}, discordgo.TextDisplay{Content: "> " + descripcion}, }, }, }, }, }) }})