Skip to content

Commit

Permalink
Context menus (bwmarrin#978)
Browse files Browse the repository at this point in the history
* Interactions: context menus

* Example for message context menus

* Added flags to followups

* Example for user context menus

* Godoc fix

* Rebase fix

* Update message types to reflect new separations

Co-authored-by: Carson Hoffman <c@rsonhoffman.com>
  • Loading branch information
2 people authored and jccit committed Sep 14, 2021
1 parent f39d983 commit 3634c1a
Show file tree
Hide file tree
Showing 3 changed files with 254 additions and 9 deletions.
222 changes: 222 additions & 0 deletions examples/context_menus/main.go
@@ -0,0 +1,222 @@
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")
)

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)
}
}

func searchLink(message, format, sep string) string {
return fmt.Sprintf(format, strings.Join(
strings.Split(
message,
" ",
),
sep,
))
}

var (
commands = []discordgo.ApplicationCommand{
{
Name: "rickroll-em",
Type: discordgo.UserApplicationCommand,
},
{
Name: "google-it",
Type: discordgo.MessageApplicationCommand,
},
{
Name: "stackoverflow-it",
Type: discordgo.MessageApplicationCommand,
},
{
Name: "godoc-it",
Type: discordgo.MessageApplicationCommand,
},
{
Name: "discordjs-it",
Type: discordgo.MessageApplicationCommand,
},
{
Name: "discordpy-it",
Type: discordgo.MessageApplicationCommand,
},
}
commandsHandlers = map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){
"rickroll-em": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "Operation rickroll has begun",
Flags: 1 << 6,
},
})
if err != nil {
panic(err)
}

ch, err := s.UserChannelCreate(
i.ApplicationCommandData().TargetID,
)
if err != nil {
_, err = s.FollowupMessageCreate(*AppID, i.Interaction, true, &discordgo.WebhookParams{
Content: fmt.Sprintf("Mission failed. Cannot send a message to this user: %q", err.Error()),
Flags: 1 << 6,
})
if err != nil {
panic(err)
}
}
_, err = s.ChannelMessageSend(
ch.ID,
fmt.Sprintf("%s sent you this: https://youtu.be/dQw4w9WgXcQ", i.Member.Mention()),
)
if err != nil {
panic(err)
}
},
"google-it": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: searchLink(
i.ApplicationCommandData().Resolved.Messages[i.ApplicationCommandData().TargetID].Content,
"https://google.com/search?q=%s", "+"),
Flags: 1 << 6,
},
})
if err != nil {
panic(err)
}
},
"stackoverflow-it": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: searchLink(
i.ApplicationCommandData().Resolved.Messages[i.ApplicationCommandData().TargetID].Content,
"https://stackoverflow.com/search?q=%s", "+"),
Flags: 1 << 6,
},
})
if err != nil {
panic(err)
}
},
"godoc-it": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: searchLink(
i.ApplicationCommandData().Resolved.Messages[i.ApplicationCommandData().TargetID].Content,
"https://pkg.go.dev/search?q=%s", "+"),
Flags: 1 << 6,
},
})
if err != nil {
panic(err)
}
},
"discordjs-it": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: searchLink(
i.ApplicationCommandData().Resolved.Messages[i.ApplicationCommandData().TargetID].Content,
"https://discord.js.org/#/docs/main/stable/search?query=%s", "+"),
Flags: 1 << 6,
},
})
if err != nil {
panic(err)
}
},
"discordpy-it": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: searchLink(
i.ApplicationCommandData().Resolved.Messages[i.ApplicationCommandData().TargetID].Content,
"https://discordpy.readthedocs.io/en/stable/search.html?q=%s", "+"),
Flags: 1 << 6,
},
})
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) {
if h, ok := commandsHandlers[i.ApplicationCommandData().Name]; ok {
h(s, i)
}
})

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)
}
}

}
38 changes: 30 additions & 8 deletions interactions.go
Expand Up @@ -15,14 +15,30 @@ import (
// InteractionDeadline is the time allowed to respond to an interaction.
const InteractionDeadline = time.Second * 3

// ApplicationCommandType represents the type of application command.
type ApplicationCommandType uint8

// Application command types
const (
// ChatApplicationCommand is default command type. They are slash commands (i.e. called directly from the chat).
ChatApplicationCommand ApplicationCommandType = 1
// UserApplicationCommand adds command to user context menu.
UserApplicationCommand ApplicationCommandType = 2
// MessageApplicationCommand adds command to message context menu.
MessageApplicationCommand ApplicationCommandType = 3
)

// ApplicationCommand represents an application's slash command.
type ApplicationCommand struct {
ID string `json:"id,omitempty"`
ApplicationID string `json:"application_id,omitempty"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Version string `json:"version,omitempty"`
Options []*ApplicationCommandOption `json:"options"`
ID string `json:"id,omitempty"`
ApplicationID string `json:"application_id,omitempty"`
Type ApplicationCommandType `json:"type,omitempty"`
Name string `json:"name"`
// NOTE: Chat commands only. Otherwise it mustn't be set.
Description string `json:"description,omitempty"`
Version string `json:"version,omitempty"`
// NOTE: Chat commands only. Otherwise it mustn't be set.
Options []*ApplicationCommandOption `json:"options"`
}

// ApplicationCommandOptionType indicates the type of a slash command's option.
Expand Down Expand Up @@ -226,17 +242,23 @@ type ApplicationCommandInteractionData struct {
ID string `json:"id"`
Name string `json:"name"`
Resolved *ApplicationCommandInteractionDataResolved `json:"resolved"`
Options []*ApplicationCommandInteractionDataOption `json:"options"`

// Slash command options
Options []*ApplicationCommandInteractionDataOption `json:"options"`
// Target (user/message) id on which context menu command was called.
// The details are stored in Resolved according to command type.
TargetID string `json:"target_id"`
}

// ApplicationCommandInteractionDataResolved contains resolved data for command arguments.
// ApplicationCommandInteractionDataResolved contains resolved data of command execution.
// Partial Member objects are missing user, deaf and mute fields.
// Partial Channel objects only have id, name, type and permissions fields.
type ApplicationCommandInteractionDataResolved struct {
Users map[string]*User `json:"users"`
Members map[string]*Member `json:"members"`
Roles map[string]*Role `json:"roles"`
Channels map[string]*Channel `json:"channels"`
Messages map[string]*Message `json:"messages"`
}

// Type returns the type of interaction data.
Expand Down
3 changes: 2 additions & 1 deletion message.go
Expand Up @@ -38,7 +38,8 @@ const (
MessageTypeGuildDiscoveryDisqualified MessageType = 14
MessageTypeGuildDiscoveryRequalified MessageType = 15
MessageTypeReply MessageType = 19
MessageTypeApplicationCommand MessageType = 20
MessageTypeChatInputCommand MessageType = 20
MessageTypeContextMenuCommand MessageType = 23
)

// A Message stores all data related to a specific Discord message.
Expand Down

0 comments on commit 3634c1a

Please sign in to comment.