Skip to content

Commit

Permalink
Merge pull request #1049 from FedorLap2006/modals
Browse files Browse the repository at this point in the history
Modals
  • Loading branch information
FedorLap2006 committed Feb 15, 2022
2 parents d355415 + a731868 commit 82c5aae
Show file tree
Hide file tree
Showing 3 changed files with 258 additions and 0 deletions.
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 @@ -192,6 +195,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 @@ -214,6 +224,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 @@ -262,6 +281,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 @@ -404,6 +453,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 @@ -424,6 +475,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

0 comments on commit 82c5aae

Please sign in to comment.