Quellcode durchsuchen

Context menus (#978)

* 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>
Fedor Lapshin vor 2 Jahren
Ursprung
Commit
9d9602318a
3 geänderte Dateien mit 254 neuen und 9 gelöschten Zeilen
  1. 222 0
      examples/context_menus/main.go
  2. 30 8
      interactions.go
  3. 2 1
      message.go

+ 222 - 0
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)
+		}
+	}
+
+}

+ 30 - 8
interactions.go

@@ -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.
@@ -197,10 +213,15 @@ 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 {
@@ -208,6 +229,7 @@ type ApplicationCommandInteractionDataResolved struct {
 	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.

+ 2 - 1
message.go

@@ -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.