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
TextInputComponent ComponentType = 4
)

// 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 TextInputComponent:
umc.MessageComponent = &TextInput{}
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(),
})
}

// TextInput represents text input component.
type TextInput struct {
CustomID string `json:"custom_id"`
Label string `json:"label"`
Style TextInputStyle `json:"style"`
Placeholder string `json:"placeholder,omitempty"`
Value string `json:"value,omitempty"`
Required bool `json:"required,omitempty"`
MinLength int `json:"min_length,omitempty"`
MaxLength int `json:"max_length,omitempty"`
}

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

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

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

// TextInputStyle is style of text in TextInput component.
type TextInputStyle uint

// Text styles
const (
TextInputShort TextInputStyle = 1
TextInputParagraph TextInputStyle = 2
)
160 changes: 160 additions & 0 deletions examples/modals/main.go
@@ -0,0 +1,160 @@
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.TextInput{
CustomID: "opinion",
Label: "What is your opinion on them?",
Style: discordgo.TextInputShort,
Placeholder: "Don't be shy, share your opinion with us",
Required: true,
MaxLength: 300,
MinLength: 10,
},
},
},
discordgo.ActionsRow{
Components: []discordgo.MessageComponent{
discordgo.TextInput{
CustomID: "suggestions",
Label: "What would you suggest to improve them?",
Style: discordgo.TextInputParagraph,
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.TextInput).Value,
data.Components[1].(*discordgo.ActionsRow).Components[0].(*discordgo.TextInput).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: 56 additions & 0 deletions 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 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 inner 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,11 @@ type InteractionResponseData struct {

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

// NOTE: modal interaction only.

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

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