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

Context menus #978

Merged
merged 8 commits into from Aug 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -197,17 +213,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