forked from pothtonswer/discordmuffin
feat: modal interactions and text input component
This commit is contained in:
parent
092735083d
commit
09e3d894b7
3 changed files with 256 additions and 1 deletions
|
@ -13,6 +13,7 @@ const (
|
||||||
ActionsRowComponent ComponentType = 1
|
ActionsRowComponent ComponentType = 1
|
||||||
ButtonComponent ComponentType = 2
|
ButtonComponent ComponentType = 2
|
||||||
SelectMenuComponent ComponentType = 3
|
SelectMenuComponent ComponentType = 3
|
||||||
|
InputTextComponent ComponentType = 4
|
||||||
)
|
)
|
||||||
|
|
||||||
// MessageComponent is a base interface for all message components.
|
// MessageComponent is a base interface for all message components.
|
||||||
|
@ -42,6 +43,8 @@ func (umc *unmarshalableMessageComponent) UnmarshalJSON(src []byte) error {
|
||||||
umc.MessageComponent = &Button{}
|
umc.MessageComponent = &Button{}
|
||||||
case SelectMenuComponent:
|
case SelectMenuComponent:
|
||||||
umc.MessageComponent = &SelectMenu{}
|
umc.MessageComponent = &SelectMenu{}
|
||||||
|
case InputTextComponent:
|
||||||
|
umc.MessageComponent = &InputText{}
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unknown component type: %d", v.Type)
|
return fmt.Errorf("unknown component type: %d", v.Type)
|
||||||
}
|
}
|
||||||
|
@ -195,3 +198,42 @@ func (m SelectMenu) MarshalJSON() ([]byte, error) {
|
||||||
Type: m.Type(),
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
// Text styles
|
||||||
|
const (
|
||||||
|
TextStyleShort TextStyleType = 1
|
||||||
|
TextStyleParagraph TextStyleType = 2
|
||||||
|
)
|
||||||
|
|
159
examples/modals/main.go
Normal file
159
examples/modals/main.go
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -113,6 +113,7 @@ const (
|
||||||
InteractionApplicationCommand InteractionType = 2
|
InteractionApplicationCommand InteractionType = 2
|
||||||
InteractionMessageComponent InteractionType = 3
|
InteractionMessageComponent InteractionType = 3
|
||||||
InteractionApplicationCommandAutocomplete InteractionType = 4
|
InteractionApplicationCommandAutocomplete InteractionType = 4
|
||||||
|
InteractionModalSubmit InteractionType = 5
|
||||||
)
|
)
|
||||||
|
|
||||||
func (t InteractionType) String() string {
|
func (t InteractionType) String() string {
|
||||||
|
@ -123,6 +124,8 @@ func (t InteractionType) String() string {
|
||||||
return "ApplicationCommand"
|
return "ApplicationCommand"
|
||||||
case InteractionMessageComponent:
|
case InteractionMessageComponent:
|
||||||
return "MessageComponent"
|
return "MessageComponent"
|
||||||
|
case InteractionModalSubmit:
|
||||||
|
return "ModalSubmit"
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("InteractionType(%d)", t)
|
return fmt.Sprintf("InteractionType(%d)", t)
|
||||||
}
|
}
|
||||||
|
@ -137,8 +140,8 @@ type Interaction struct {
|
||||||
|
|
||||||
// The message on which interaction was used.
|
// 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.
|
// 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.
|
// The member who invoked this interaction.
|
||||||
// NOTE: this field is only filled when the slash command was invoked in a guild;
|
// 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.
|
// if it was invoked in a DM, the `User` field will be filled instead.
|
||||||
|
@ -186,6 +189,13 @@ func (i *Interaction) UnmarshalJSON(raw []byte) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
i.Data = v
|
i.Data = v
|
||||||
|
case InteractionModalSubmit:
|
||||||
|
v := ModalSubmitInteractionData{}
|
||||||
|
err = json.Unmarshal(tmp.Data, &v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
i.Data = v
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -208,6 +218,15 @@ func (i Interaction) ApplicationCommandData() (data ApplicationCommandInteractio
|
||||||
return i.Data.(ApplicationCommandInteractionData)
|
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.
|
// InteractionData is a common interface for all types of interaction data.
|
||||||
type InteractionData interface {
|
type InteractionData interface {
|
||||||
Type() InteractionType
|
Type() InteractionType
|
||||||
|
@ -256,6 +275,36 @@ func (MessageComponentInteractionData) Type() InteractionType {
|
||||||
return InteractionMessageComponent
|
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.
|
// ApplicationCommandInteractionDataOption represents an option of a slash command.
|
||||||
type ApplicationCommandInteractionDataOption struct {
|
type ApplicationCommandInteractionDataOption struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
@ -398,6 +447,8 @@ const (
|
||||||
InteractionResponseUpdateMessage InteractionResponseType = 7
|
InteractionResponseUpdateMessage InteractionResponseType = 7
|
||||||
// InteractionApplicationCommandAutocompleteResult shows autocompletion results. Autocomplete interaction only.
|
// InteractionApplicationCommandAutocompleteResult shows autocompletion results. Autocomplete interaction only.
|
||||||
InteractionApplicationCommandAutocompleteResult InteractionResponseType = 8
|
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.
|
// InteractionResponse represents a response for an interaction event.
|
||||||
|
@ -418,6 +469,9 @@ type InteractionResponseData struct {
|
||||||
|
|
||||||
// NOTE: autocomplete interaction only.
|
// NOTE: autocomplete interaction only.
|
||||||
Choices []*ApplicationCommandOptionChoice `json:"choices,omitempty"`
|
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
|
// VerifyInteraction implements message verification of the discord interactions api
|
||||||
|
|
Loading…
Reference in a new issue