Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Modals #1049

Merged
merged 11 commits into from Feb 15, 2022
42 changes: 42 additions & 0 deletions components.go
Expand Up @@ -13,6 +13,7 @@ const (
ActionsRowComponent ComponentType = 1
ButtonComponent ComponentType = 2
SelectMenuComponent ComponentType = 3
InputTextComponent ComponentType = 4
FedorLap2006 marked this conversation as resolved.
Show resolved Hide resolved
)

// MessageComponent is a base interface for all message components.
Expand Down Expand Up @@ -42,6 +43,8 @@ func (umc *unmarshalableMessageComponent) UnmarshalJSON(src []byte) error {
umc.MessageComponent = &Button{}
case SelectMenuComponent:
umc.MessageComponent = &SelectMenu{}
case InputTextComponent:
umc.MessageComponent = &InputText{}
FedorLap2006 marked this conversation as resolved.
Show resolved Hide resolved
default:
return fmt.Errorf("unknown component type: %d", v.Type)
}
Expand Down Expand Up @@ -195,3 +198,42 @@ func (m SelectMenu) MarshalJSON() ([]byte, error) {
Type: m.Type(),
})
}

// InputText represents text input component.
type InputText struct {
CustomID string `json:"custom_id,omitempty"`
Label string `json:"label"`
Style TextStyleType `json:"style"`
Placeholder string `json:"placeholder,omitempty"`
Value string `json:"value,omitempty"`
Required bool `json:"required"`
MinLength int `json:"min_length"`
MaxLength int `json:"max_length,omitempty"`
FedorLap2006 marked this conversation as resolved.
Show resolved Hide resolved
}

// Type is a method to get the type of a component.
func (InputText) Type() ComponentType {
return InputTextComponent
}

// MarshalJSON is a method for marshaling InputText to a JSON object.
func (m InputText) MarshalJSON() ([]byte, error) {
type inputText InputText

return json.Marshal(struct {
inputText
Type ComponentType `json:"type"`
}{
inputText: inputText(m),
Type: m.Type(),
})
}

// TextStyleType is style of text in InputText component.
type TextStyleType uint
FedorLap2006 marked this conversation as resolved.
Show resolved Hide resolved

// Text styles
const (
TextStyleShort TextStyleType = 1
TextStyleParagraph TextStyleType = 2
)
159 changes: 159 additions & 0 deletions examples/modals/main.go
@@ -0,0 +1,159 @@
package main

import (
"flag"
"fmt"
"log"
"os"
"os/signal"
"strings"

"github.com/bwmarrin/discordgo"
)

// Bot parameters
var (
GuildID = flag.String("guild", "", "Test guild ID")
BotToken = flag.String("token", "", "Bot access token")
AppID = flag.String("app", "", "Application ID")
Cleanup = flag.Bool("cleanup", true, "Cleanup of commands")
ResultsChannel = flag.String("results", "", "Channel where send survey results to")
)

var s *discordgo.Session

func init() {
flag.Parse()
}

func init() {
var err error
s, err = discordgo.New("Bot " + *BotToken)
if err != nil {
log.Fatalf("Invalid bot parameters: %v", err)
}
}

var (
commands = []discordgo.ApplicationCommand{
{
Name: "modals-survey",
Description: "Take a survey about modals",
},
}
commandsHandlers = map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){
"modals-survey": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseModal,
Data: &discordgo.InteractionResponseData{
CustomID: "modals_survey_" + i.Interaction.Member.User.ID,
Title: "Modals survey",
Components: []discordgo.MessageComponent{
discordgo.ActionsRow{
Components: []discordgo.MessageComponent{
discordgo.InputText{
CustomID: "opinion",
Label: "What is your opinion on them?",
Style: discordgo.TextStyleShort,
Placeholder: "Don't be shy, share your opinion with us",
Required: true,
MaxLength: 300,
},
},
},
discordgo.ActionsRow{
Components: []discordgo.MessageComponent{
discordgo.InputText{
CustomID: "suggestions",
Label: "What would you suggest to improve them?",
Style: discordgo.TextStyleParagraph,
Required: false,
MaxLength: 2000,
},
},
},
},
},
})
if err != nil {
panic(err)
}
},
}
)

func main() {
s.AddHandler(func(s *discordgo.Session, r *discordgo.Ready) {
log.Println("Bot is up!")
})

s.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) {
switch i.Type {
case discordgo.InteractionApplicationCommand:
if h, ok := commandsHandlers[i.ApplicationCommandData().Name]; ok {
h(s, i)
}
case discordgo.InteractionModalSubmit:
err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "Thank you for taking your time to fill this survey",
Flags: 1 << 6,
},
})
if err != nil {
panic(err)
}
data := i.ModalSubmitData()

if !strings.HasPrefix(data.CustomID, "modals_survey") {
return
}

userid := strings.Split(data.CustomID, "_")[2]
_, err = s.ChannelMessageSend(*ResultsChannel, fmt.Sprintf(
"Feedback received. From <@%s>\n\n**Opinion**:\n%s\n\n**Suggestions**:\n%s",
userid,
data.Components[0].(*discordgo.ActionsRow).Components[0].(*discordgo.InputText).Value,
data.Components[1].(*discordgo.ActionsRow).Components[0].(*discordgo.InputText).Value,
))
if err != nil {
panic(err)
}
}
})

cmdIDs := make(map[string]string, len(commands))

for _, cmd := range commands {
rcmd, err := s.ApplicationCommandCreate(*AppID, *GuildID, &cmd)
if err != nil {
log.Fatalf("Cannot create slash command %q: %v", cmd.Name, err)
}

cmdIDs[rcmd.ID] = rcmd.Name
}

err := s.Open()
if err != nil {
log.Fatalf("Cannot open the session: %v", err)
}
defer s.Close()

stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt)
<-stop
log.Println("Graceful shutdown")

if !*Cleanup {
return
}

for id, name := range cmdIDs {
err := s.ApplicationCommandDelete(*AppID, *GuildID, id)
if err != nil {
log.Fatalf("Cannot delete slash command %q: %v", name, err)
}
}

}
56 changes: 55 additions & 1 deletion interactions.go
Expand Up @@ -113,6 +113,7 @@ const (
InteractionApplicationCommand InteractionType = 2
InteractionMessageComponent InteractionType = 3
InteractionApplicationCommandAutocomplete InteractionType = 4
InteractionModalSubmit InteractionType = 5
)

func (t InteractionType) String() string {
Expand All @@ -123,6 +124,8 @@ func (t InteractionType) String() string {
return "ApplicationCommand"
case InteractionMessageComponent:
return "MessageComponent"
case InteractionModalSubmit:
return "ModalSubmit"
}
return fmt.Sprintf("InteractionType(%d)", t)
}
Expand All @@ -137,8 +140,8 @@ type Interaction struct {

// The message on which interaction was used.
// NOTE: this field is only filled when a button click triggered the interaction. Otherwise it will be nil.
Message *Message `json:"message"`

Message *Message `json:"message"`
// The member who invoked this interaction.
// NOTE: this field is only filled when the slash command was invoked in a guild;
// if it was invoked in a DM, the `User` field will be filled instead.
Expand Down Expand Up @@ -186,6 +189,13 @@ func (i *Interaction) UnmarshalJSON(raw []byte) error {
return err
}
i.Data = v
case InteractionModalSubmit:
v := ModalSubmitInteractionData{}
err = json.Unmarshal(tmp.Data, &v)
if err != nil {
return err
}
i.Data = v
}
return nil
}
Expand All @@ -208,6 +218,15 @@ func (i Interaction) ApplicationCommandData() (data ApplicationCommandInteractio
return i.Data.(ApplicationCommandInteractionData)
}

// ModalSubmitData is helper function to assert the innter InteractionData to ModalSubmitInteractionData.
// Make sure to check that the Type of the interaction is InteractionModalSubmit before calling.
func (i Interaction) ModalSubmitData() (data ModalSubmitInteractionData) {
if i.Type != InteractionModalSubmit {
panic("ModalSubmitData called on interaction of type " + i.Type.String())
}
return i.Data.(ModalSubmitInteractionData)
}

// InteractionData is a common interface for all types of interaction data.
type InteractionData interface {
Type() InteractionType
Expand Down Expand Up @@ -256,6 +275,36 @@ func (MessageComponentInteractionData) Type() InteractionType {
return InteractionMessageComponent
}

// ModalSubmitInteractionData contains the data of modal submit interaction.
type ModalSubmitInteractionData struct {
CustomID string `json:"custom_id"`
Components []MessageComponent `json:"-"`
}

// Type returns the type of interaction data.
func (ModalSubmitInteractionData) Type() InteractionType {
return InteractionModalSubmit
}

// UnmarshalJSON is a helper function to correctly unmarshal Components.
func (d *ModalSubmitInteractionData) UnmarshalJSON(data []byte) error {
type modalSubmitInteractionData ModalSubmitInteractionData
var v struct {
modalSubmitInteractionData
RawComponents []unmarshalableMessageComponent `json:"components"`
}
err := json.Unmarshal(data, &v)
if err != nil {
return err
}
*d = ModalSubmitInteractionData(v.modalSubmitInteractionData)
d.Components = make([]MessageComponent, len(v.RawComponents))
for i, v := range v.RawComponents {
d.Components[i] = v.MessageComponent
}
return err
}

// ApplicationCommandInteractionDataOption represents an option of a slash command.
type ApplicationCommandInteractionDataOption struct {
Name string `json:"name"`
Expand Down Expand Up @@ -398,6 +447,8 @@ const (
InteractionResponseUpdateMessage InteractionResponseType = 7
// InteractionApplicationCommandAutocompleteResult shows autocompletion results. Autocomplete interaction only.
InteractionApplicationCommandAutocompleteResult InteractionResponseType = 8
// InteractionResponseModal is for responding to an interaction with a modal window.
InteractionResponseModal InteractionResponseType = 9
)

// InteractionResponse represents a response for an interaction event.
Expand All @@ -418,6 +469,9 @@ type InteractionResponseData struct {

// NOTE: autocomplete interaction only.
Choices []*ApplicationCommandOptionChoice `json:"choices,omitempty"`

CustomID string `json:"custom_id,omitempty"`
Title string `json:"title,omitempty"`
}

// VerifyInteraction implements message verification of the discord interactions api
Expand Down