From b4675518f37e24ee8adad9e66ce49c2f89ffd546 Mon Sep 17 00:00:00 2001 From: zekro Date: Wed, 31 Aug 2022 07:20:56 +0000 Subject: [PATCH 1/5] add ken interface for better mocking abilities --- iken.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 iken.go diff --git a/iken.go b/iken.go new file mode 100644 index 0000000..43cf944 --- /dev/null +++ b/iken.go @@ -0,0 +1,12 @@ +package ken + +import "github.com/bwmarrin/discordgo" + +type IKen interface { + Components() *ComponentHandler + GetCommandInfo(keyTransformer ...KeyTransformerFunc) (cis CommandInfoList) + RegisterCommands(cmds ...Command) (err error) + RegisterMiddlewares(mws ...interface{}) (err error) + Session() *discordgo.Session + Unregister() (err error) +} From b420366ead4d89b40712d1aad8a90fa2f8fef80d Mon Sep 17 00:00:00 2001 From: zekro Date: Wed, 31 Aug 2022 07:23:27 +0000 Subject: [PATCH 2/5] added support for modals [#10] --- component.go | 15 +++++ componentbuilder.go | 14 +---- components.go | 78 +++++++++++++++++++++-- context.go | 80 +++++++++++++++++++++++- examples/components/commands/modal.go | 89 +++++++++++++++++++++++++++ examples/components/main.go | 5 +- go.mod | 1 + go.sum | 2 + info.go | 6 +- util/reflect.go | 18 ++++++ 10 files changed, 287 insertions(+), 21 deletions(-) create mode 100644 component.go create mode 100644 examples/components/commands/modal.go create mode 100644 util/reflect.go diff --git a/component.go b/component.go new file mode 100644 index 0000000..9a859e0 --- /dev/null +++ b/component.go @@ -0,0 +1,15 @@ +package ken + +import ( + "github.com/bwmarrin/discordgo" + "github.com/zekrotja/ken/util" +) + +type MessageComponent struct { + discordgo.MessageComponent +} + +func (t MessageComponent) GetValue() string { + val, _ := util.GetFieldValue(t.MessageComponent, "Value") + return val +} diff --git a/componentbuilder.go b/componentbuilder.go index 8f3135b..88fb4d4 100644 --- a/componentbuilder.go +++ b/componentbuilder.go @@ -1,9 +1,8 @@ package ken import ( - "reflect" - "github.com/bwmarrin/discordgo" + "github.com/zekrotja/ken/util" ) // ComponentAssembler helps to build the message @@ -228,15 +227,8 @@ func (t *ComponentBuilder) Build() (unreg func() error, err error) { } func getCustomId(component discordgo.MessageComponent) string { - componentValue := reflect.ValueOf(component) - customIdValue := componentValue.FieldByName("CustomID") - - var customId string - if customIdValue.IsValid() { - customId = customIdValue.String() - } - - return customId + val, _ := util.GetFieldValue(component, "CustomID") + return val } func removeComponentRecursive(components []discordgo.MessageComponent, customKey string) []discordgo.MessageComponent { diff --git a/components.go b/components.go index 0ddcde3..5ee73bf 100644 --- a/components.go +++ b/components.go @@ -16,6 +16,8 @@ import ( // the execution of the handler. type ComponentHandlerFunc func(ctx ComponentContext) bool +type ModalHandlerFunc func(ctx ModalContext) bool + // ComponentHandler keeps a registry of component handler // callbacks to be executed when a given component has // been interacted with. @@ -23,10 +25,12 @@ type ComponentHandler struct { ken *Ken unregisterFunc func() - mtx sync.RWMutex - handlers map[string]ComponentHandlerFunc + mtx sync.RWMutex + handlers map[string]ComponentHandlerFunc + modalHandlers map[string]ModalHandlerFunc - ctxPool sync.Pool + ctxPool sync.Pool + modalCtxPool sync.Pool } // NewComponentHandler returns a new instance of @@ -37,12 +41,18 @@ func NewComponentHandler(ken *Ken) *ComponentHandler { t.ken = ken t.handlers = make(map[string]ComponentHandlerFunc) + t.modalHandlers = make(map[string]ModalHandlerFunc) t.unregisterFunc = t.ken.s.AddHandler(t.handle) t.ctxPool = sync.Pool{ New: func() interface{} { return &ComponentCtx{} }, } + t.modalCtxPool = sync.Pool{ + New: func() interface{} { + return &ModalCtx{} + }, + } return &t } @@ -110,11 +120,43 @@ func (t *ComponentHandler) UnregisterDiscordHandler() { t.unregisterFunc() } -func (t *ComponentHandler) handle(_ *discordgo.Session, e *discordgo.InteractionCreate) { - if e.Type != discordgo.InteractionMessageComponent { +func (t *ComponentHandler) registerModalHandler(customId string, handler ModalHandlerFunc) func() { + t.mtx.Lock() + defer t.mtx.Unlock() + t.modalHandlers[customId] = func(ctx ModalContext) bool { + ok := handler(ctx) + if ok { + t.unregisterModalhandler(customId) + } + return ok + } + + return func() { + t.unregisterModalhandler(customId) + } +} + +func (t *ComponentHandler) unregisterModalhandler(customId ...string) { + if len(customId) == 0 { return } + t.mtx.Lock() + defer t.mtx.Unlock() + for _, id := range customId { + delete(t.modalHandlers, id) + } +} + +func (t *ComponentHandler) handle(_ *discordgo.Session, e *discordgo.InteractionCreate) { + switch e.Type { + case discordgo.InteractionMessageComponent: + t.handleMessageComponent(e) + case discordgo.InteractionModalSubmit: + t.handleModalSubmit(e) + } +} +func (t *ComponentHandler) handleMessageComponent(e *discordgo.InteractionCreate) { data := e.MessageComponentData() t.mtx.RLock() @@ -139,3 +181,29 @@ func (t *ComponentHandler) handle(_ *discordgo.Session, e *discordgo.Interaction handler(ctx) } + +func (t *ComponentHandler) handleModalSubmit(e *discordgo.InteractionCreate) { + data := e.ModalSubmitData() + + t.mtx.RLock() + handler, ok := t.modalHandlers[data.CustomID] + t.mtx.RUnlock() + + if !ok { + return + } + + ctx := t.modalCtxPool.Get().(*ModalCtx) + ctx.Data = data + ctx.Ephemeral = false + ctx.Event = e + ctx.Session = t.ken.s + ctx.Ken = t.ken + ctx.responded = false + + defer func() { + t.modalCtxPool.Put(ctx) + }() + + handler(ctx) +} diff --git a/context.go b/context.go index 539603e..46cc0c3 100644 --- a/context.go +++ b/context.go @@ -2,6 +2,7 @@ package ken import ( "github.com/bwmarrin/discordgo" + "github.com/rs/xid" ) // ContextResponder defines the implementation of an @@ -357,8 +358,12 @@ type ComponentContext interface { ContextResponder GetData() discordgo.MessageComponentInteractionData + OpenModal( + title string, + content string, + build func(b ComponentAssembler), + ) (<-chan ModalContext, error) } - type ComponentCtx struct { CtxResponder @@ -370,3 +375,76 @@ var _ ComponentContext = (*ComponentCtx)(nil) func (c *ComponentCtx) GetData() discordgo.MessageComponentInteractionData { return c.Data } + +func (c *ComponentCtx) OpenModal( + title string, + content string, + build func(b ComponentAssembler), +) (<-chan ModalContext, error) { + b := newComponentAssembler() + build(b) + + modalId := xid.New().String() + err := c.Respond(&discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseModal, + Data: &discordgo.InteractionResponseData{ + CustomID: modalId, + Title: title, + Content: content, + Components: b.components, + }, + }) + if err != nil { + return nil, err + } + + cCtx := make(chan ModalContext, 1) + + c.Ken.componentHandler.registerModalHandler(modalId, func(ctx ModalContext) bool { + cCtx <- ctx + return true + }) + + return cCtx, nil +} + +type ModalContext interface { + ContextResponder + + GetData() discordgo.ModalSubmitInteractionData + GetComponentByID(customId string) MessageComponent +} + +type ModalCtx struct { + CtxResponder + + Data discordgo.ModalSubmitInteractionData +} + +var _ ModalContext = (*ModalCtx)(nil) + +func (c *ModalCtx) GetData() discordgo.ModalSubmitInteractionData { + return c.Data +} + +func (c *ModalCtx) GetComponentByID(customId string) MessageComponent { + return MessageComponent{getComponentByID(customId, c.GetData().Components)} +} + +func getComponentByID( + customId string, + comps []discordgo.MessageComponent, +) discordgo.MessageComponent { + for _, comp := range comps { + if row, ok := comp.(*discordgo.ActionsRow); ok { + found := getComponentByID(customId, row.Components) + if found != nil { + return found + } + } + if customId == getCustomId(comp) { + return comp + } + } + return nil +} diff --git a/examples/components/commands/modal.go b/examples/components/commands/modal.go new file mode 100644 index 0000000..43ede87 --- /dev/null +++ b/examples/components/commands/modal.go @@ -0,0 +1,89 @@ +package commands + +import ( + "fmt" + + "github.com/bwmarrin/discordgo" + "github.com/zekrotja/ken" +) + +type ModalCommand struct{} + +var ( + _ ken.SlashCommand = (*TestCommand)(nil) + _ ken.DmCapable = (*TestCommand)(nil) +) + +func (c *ModalCommand) Name() string { + return "modal" +} + +func (c *ModalCommand) Description() string { + return "Modal Test Command" +} + +func (c *ModalCommand) Version() string { + return "1.0.0" +} + +func (c *ModalCommand) Type() discordgo.ApplicationCommandType { + return discordgo.ChatApplicationCommand +} + +func (c *ModalCommand) Options() []*discordgo.ApplicationCommandOption { + return []*discordgo.ApplicationCommandOption{} +} + +func (c *ModalCommand) IsDmCapable() bool { + return false +} + +func (c *ModalCommand) Run(ctx *ken.Ctx) (err error) { + if err = ctx.Defer(); err != nil { + return + } + + fum := ctx.FollowUpEmbed(&discordgo.MessageEmbed{ + Description: "How are you?", + }) + if fum.HasError() { + return fum.Error + } + + _, err = fum.AddComponents(). + AddActionsRow(func(b ken.ComponentAssembler) { + b.Add(discordgo.Button{ + CustomID: "open-modal", + Label: "Write it!", + Style: discordgo.PrimaryButton, + }, func(ctx ken.ComponentContext) bool { + cCtx, err := ctx.OpenModal("Hello world", "Lorem ipsum ...", func(b ken.ComponentAssembler) { + b.AddActionsRow(func(b ken.ComponentAssembler) { + b.Add(discordgo.TextInput{ + CustomID: "text-input", + Label: "How are you?", + Style: discordgo.TextInputShort, + Required: true, + MaxLength: 1000, + }, nil) + }) + }) + + if err != nil { + fmt.Println("Error:", err) + return false + } + + embCtx := <-cCtx + + resp := embCtx.GetComponentByID("text-input").GetValue() + embCtx.RespondEmbed(&discordgo.MessageEmbed{ + Description: fmt.Sprintf(`"%s" - ok, thats cool`, resp), + }) + return true + }) + }, true). + Build() + + return err +} diff --git a/examples/components/main.go b/examples/components/main.go index 964a5d1..0262ad6 100644 --- a/examples/components/main.go +++ b/examples/components/main.go @@ -31,7 +31,10 @@ func main() { }) must(err) - must(k.RegisterCommands(new(commands.TestCommand))) + must(k.RegisterCommands( + new(commands.TestCommand), + new(commands.ModalCommand), + )) defer k.Unregister() diff --git a/go.mod b/go.mod index 108be9b..0d2926a 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/go-redis/redis/v8 v8.11.5 // indirect + github.com/rs/xid v1.4.0 github.com/zekroTJA/timedmap v1.4.0 go.opentelemetry.io/otel v1.9.0 // indirect go.opentelemetry.io/otel/metric v0.31.0 // indirect diff --git a/go.sum b/go.sum index fe312dc..16a5824 100644 --- a/go.sum +++ b/go.sum @@ -53,6 +53,8 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY= +github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= diff --git a/info.go b/info.go index f87cd76..add5376 100644 --- a/info.go +++ b/info.go @@ -37,7 +37,7 @@ func mustToJson(v interface{}) string { return string(d) } -type keyTransformerFunc func(string) string +type KeyTransformerFunc func(string) string // GetCommandInfo returns a list with information about all // registered commands. @@ -52,7 +52,7 @@ type keyTransformerFunc func(string) string // If you want to disable this behavior, you can set // Config.DisableCommandInfoCache to true on intializing // Ken. -func (k *Ken) GetCommandInfo(keyTransformer ...keyTransformerFunc) (cis CommandInfoList) { +func (k *Ken) GetCommandInfo(keyTransformer ...KeyTransformerFunc) (cis CommandInfoList) { kt := func(v string) string { return v } @@ -73,7 +73,7 @@ func (k *Ken) GetCommandInfo(keyTransformer ...keyTransformerFunc) (cis CommandI return } -func (k *Ken) collectCommandInfo(kt keyTransformerFunc) (cis CommandInfoList) { +func (k *Ken) collectCommandInfo(kt KeyTransformerFunc) (cis CommandInfoList) { cis = make(CommandInfoList, 0, len(k.cmds)) for _, cmd := range k.cmds { typ := reflect.TypeOf(cmd) diff --git a/util/reflect.go b/util/reflect.go new file mode 100644 index 0000000..7b8b569 --- /dev/null +++ b/util/reflect.go @@ -0,0 +1,18 @@ +package util + +import "reflect" + +func GetFieldValue(v interface{}, fieldName string) (value string, ok bool) { + val := reflect.ValueOf(v) + if val.Kind() == reflect.Ptr { + val = val.Elem() + } + fieldValue := val.FieldByName(fieldName) + + if fieldValue.IsValid() { + value = fieldValue.String() + ok = true + } + + return value, ok +} From 7e1b4ebeb2996efe1114ae95c49b8af3670e1611 Mon Sep 17 00:00:00 2001 From: zekro Date: Wed, 31 Aug 2022 08:02:55 +0000 Subject: [PATCH 3/5] shift context to be used primarily via interface --- README.md | 2 +- command.go | 2 +- component.go | 8 + components.go | 24 +- context.go | 275 +++++++++++------- examples/basic/commands/test.go | 2 +- examples/cmdinfo/commands/test.go | 2 +- examples/components/commands/modal.go | 2 +- examples/components/commands/test.go | 2 +- examples/guildscoped/commands/guild1.go | 2 +- examples/guildscoped/commands/guild2.go | 2 +- examples/help/commands/test.go | 2 +- examples/middlewares/commands/kick.go | 4 +- .../middlewares/middlewares/permissions.go | 4 +- examples/ratelimit/commands/test.go | 2 +- examples/subcommands/commands/subs.go | 2 +- examples/usermsgcommands/commands/delete.go | 6 +- examples/usermsgcommands/commands/info.go | 2 +- ken.go | 10 +- middlewares/ratelimit/ratelimit.go | 2 +- middlewares/ratelimit/v2/ratelimit.go | 2 +- 21 files changed, 218 insertions(+), 141 deletions(-) diff --git a/README.md b/README.md index 4f2c30e..539627d 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ func (c *TestCommand) Options() []*discordgo.ApplicationCommandOption { return []*discordgo.ApplicationCommandOption{} } -func (c *TestCommand) Run(ctx *ken.Ctx) (err error) { +func (c *TestCommand) Run(ctx ken.Context) (err error) { err = ctx.Respond(&discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ diff --git a/command.go b/command.go index d246058..a0c8444 100644 --- a/command.go +++ b/command.go @@ -27,7 +27,7 @@ type Command interface { // When something goes wrong during command // execution, you can return an error which is // then handled by Ken's OnCommandError handler. - Run(ctx *Ctx) (err error) + Run(ctx Context) (err error) } // GuildScopedCommand can be implemented by your diff --git a/component.go b/component.go index 9a859e0..0524c40 100644 --- a/component.go +++ b/component.go @@ -10,6 +10,14 @@ type MessageComponent struct { } func (t MessageComponent) GetValue() string { + if t.MessageComponent == nil { + return "" + } + val, _ := util.GetFieldValue(t.MessageComponent, "Value") return val } + +func (t MessageComponent) IsEmpty() bool { + return t.MessageComponent == nil +} diff --git a/components.go b/components.go index 5ee73bf..95f39f1 100644 --- a/components.go +++ b/components.go @@ -45,12 +45,12 @@ func NewComponentHandler(ken *Ken) *ComponentHandler { t.unregisterFunc = t.ken.s.AddHandler(t.handle) t.ctxPool = sync.Pool{ New: func() interface{} { - return &ComponentCtx{} + return &componentCtx{} }, } t.modalCtxPool = sync.Pool{ New: func() interface{} { - return &ModalCtx{} + return &modalCtx{} }, } @@ -167,12 +167,12 @@ func (t *ComponentHandler) handleMessageComponent(e *discordgo.InteractionCreate return } - ctx := t.ctxPool.Get().(*ComponentCtx) + ctx := t.ctxPool.Get().(*componentCtx) ctx.Data = data - ctx.Ephemeral = false - ctx.Event = e - ctx.Session = t.ken.s - ctx.Ken = t.ken + ctx.ephemeral = false + ctx.event = e + ctx.session = t.ken.s + ctx.ken = t.ken ctx.responded = false defer func() { @@ -193,12 +193,12 @@ func (t *ComponentHandler) handleModalSubmit(e *discordgo.InteractionCreate) { return } - ctx := t.modalCtxPool.Get().(*ModalCtx) + ctx := t.modalCtxPool.Get().(*modalCtx) ctx.Data = data - ctx.Ephemeral = false - ctx.Event = e - ctx.Session = t.ken.s - ctx.Ken = t.ken + ctx.ephemeral = false + ctx.event = e + ctx.session = t.ken.s + ctx.ken = t.ken ctx.responded = false defer func() { diff --git a/context.go b/context.go index 46cc0c3..495637a 100644 --- a/context.go +++ b/context.go @@ -10,16 +10,65 @@ import ( // to the interaction, to set the ephemeral state and // to retrieve the nested session and event. type ContextResponder interface { + + // Respond to an interaction event with the given + // interaction response payload. + // + // When an interaction has already been responded to, + // the response will be edited instead on execution. Respond(r *discordgo.InteractionResponse) (err error) + + // RespondEmbed is shorthand for Respond with an + // embed payload as passed. RespondEmbed(emb *discordgo.MessageEmbed) (err error) + + // RespondError is shorthand for RespondEmbed with an + // error embed as message with the passed content and + // title. RespondError(content, title string) (err error) + + // FollowUp creates a follow up message to the + // interaction event and returns a FollowUpMessage + // object containing the created message as well as + // an error instance, if an error occurred. + // + // This way it allows to be chained in one call with + // subsequent FollowUpMessage method calls. FollowUp(wait bool, data *discordgo.WebhookParams) (fum *FollowUpMessage) + + // FollowUpEmbed is shorthand for FollowUp with an + // embed payload as passed. FollowUpEmbed(emb *discordgo.MessageEmbed) (fum *FollowUpMessage) + + // FollowUpError is shorthand for FollowUpEmbed with an + // error embed as message with the passed content and + // title. FollowUpError(content, title string) (fum *FollowUpMessage) + + // Defer is shorthand for Respond with an InteractionResponse + // of the type InteractionResponseDeferredChannelMessageWithSource. + // + // It should be used when the interaction response can not be + // instantly returned. Defer() (err error) + + // GetEphemeral returns the current emphemeral state + // of the command invokation. GetEphemeral() bool + + // SetEphemeral sets the emphemeral state of the command + // invokation. + // + // Ephemeral can be set to true which will + // send all subsequent command responses + // only to the user which invoked the command. SetEphemeral(v bool) + + // GetSession returns the current Discordgo session instance. GetSession() *discordgo.Session + + // GetEvent returns the InteractionCreate event instance which + // invoked the interaction command. GetEvent() *discordgo.InteractionCreate } @@ -27,46 +76,69 @@ type ContextResponder interface { // command context passed to the command handler. type Context interface { ContextResponder + ObjectProvider - Get(key string) (v interface{}) + // Channel tries to fetch the channel object from the contained + // channel ID using the specified state manager. Channel() (*discordgo.Channel, error) + + // Channel tries to fetch the guild object from the contained + // guild ID using the specified state manager. Guild() (*discordgo.Guild, error) + + // User returns the User object of the executor either from + // the events User object or from the events Member object. User() (u *discordgo.User) + + // Options returns the application command data options + // with additional functionality methods. Options() CommandOptions + + // SlashCommand returns the contexts Command as a + // SlashCommand interface. SlashCommand() (cmd SlashCommand, ok bool) + + // UserCommand returns the contexts Command as a + // UserCommand interface. UserCommand() (cmd UserCommand, ok bool) + + // MessageCommand returns the contexts Command as a + // MessageCommand interface. MessageCommand() (cmd MessageCommand, ok bool) + + // HandleSubCommands takes a list of sub command handles. + // When the command is executed, the options are scanned + // for the sib command calls by their names. If one of + // the registered sub commands has been called, the specified + // handler function is executed. + // + // If the call occured, the passed handler function is + // getting passed the scoped sub command Ctx. + // + // The SubCommandCtx passed must not be stored or used + // after command execution. HandleSubCommands(handler ...SubCommandHandler) (err error) + + // GetKen returns the root instance of Ken. GetKen() *Ken + + // GetCommand returns the command instance called. GetCommand() Command } -// CtxResponder provides functionailities to respond +// ctxResponder provides functionailities to respond // to an interaction. -type CtxResponder struct { +type ctxResponder struct { responded bool - - // Ken keeps a reference to the main Ken instance. - Ken *Ken - // Session holds the discordgo session instance. - Session *discordgo.Session - // Event provides the InteractionCreate event - // instance. - Event *discordgo.InteractionCreate - // Ephemeral can be set to true which will - // send all subsequent command responses - // only to the user which invoked the command. - Ephemeral bool + ken *Ken + session *discordgo.Session + event *discordgo.InteractionCreate + ephemeral bool } -var _ ContextResponder = (*CtxResponder)(nil) +var _ ContextResponder = (*ctxResponder)(nil) -// Respond to an interaction event with the given -// interaction response payload. -// -// When an interaction has already been responded to, -// the response will be edited instead on execution. -func (c *CtxResponder) Respond(r *discordgo.InteractionResponse) (err error) { +func (c *ctxResponder) Respond(r *discordgo.InteractionResponse) (err error) { if r.Data == nil { r.Data = new(discordgo.InteractionResponseData) } @@ -75,7 +147,7 @@ func (c *CtxResponder) Respond(r *discordgo.InteractionResponse) (err error) { if r == nil || r.Data == nil { return } - _, err = c.Session.InteractionResponseEdit(c.Event.Interaction, &discordgo.WebhookEdit{ + _, err = c.GetSession().InteractionResponseEdit(c.event.Interaction, &discordgo.WebhookEdit{ Content: &r.Data.Content, Embeds: &r.Data.Embeds, Components: &r.Data.Components, @@ -83,7 +155,7 @@ func (c *CtxResponder) Respond(r *discordgo.InteractionResponse) (err error) { AllowedMentions: r.Data.AllowedMentions, }) } else { - err = c.Session.InteractionRespond(c.Event.Interaction, r) + err = c.GetSession().InteractionRespond(c.event.Interaction, r) c.responded = err == nil if err != nil { _ = err @@ -92,11 +164,9 @@ func (c *CtxResponder) Respond(r *discordgo.InteractionResponse) (err error) { return } -// RespondEmbed is shorthand for Respond with an -// embed payload as passed. -func (c *CtxResponder) RespondEmbed(emb *discordgo.MessageEmbed) (err error) { +func (c *ctxResponder) RespondEmbed(emb *discordgo.MessageEmbed) (err error) { if emb.Color <= 0 { - emb.Color = c.Ken.opt.EmbedColors.Default + emb.Color = c.ken.opt.EmbedColors.Default } return c.Respond(&discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, @@ -108,39 +178,27 @@ func (c *CtxResponder) RespondEmbed(emb *discordgo.MessageEmbed) (err error) { }) } -// RespondError is shorthand for RespondEmbed with an -// error embed as message with the passed content and -// title. -func (c *CtxResponder) RespondError(content, title string) (err error) { +func (c *ctxResponder) RespondError(content, title string) (err error) { return c.RespondEmbed(&discordgo.MessageEmbed{ Description: content, Title: title, - Color: c.Ken.opt.EmbedColors.Error, + Color: c.ken.opt.EmbedColors.Error, }) } -// FollowUp creates a follow up message to the -// interaction event and returns a FollowUpMessage -// object containing the created message as well as -// an error instance, if an error occurred. -// -// This way it allows to be chained in one call with -// subsequent FollowUpMessage method calls. -func (c *CtxResponder) FollowUp(wait bool, data *discordgo.WebhookParams) (fum *FollowUpMessage) { +func (c *ctxResponder) FollowUp(wait bool, data *discordgo.WebhookParams) (fum *FollowUpMessage) { data.Flags = c.messageFlags(data.Flags) fum = &FollowUpMessage{ - ken: c.Ken, - i: c.Event.Interaction, + ken: c.ken, + i: c.event.Interaction, } - fum.Message, fum.Error = c.Session.FollowupMessageCreate(c.Event.Interaction, wait, data) + fum.Message, fum.Error = c.GetSession().FollowupMessageCreate(c.event.Interaction, wait, data) return } -// FollowUpEmbed is shorthand for FollowUp with an -// embed payload as passed. -func (c *CtxResponder) FollowUpEmbed(emb *discordgo.MessageEmbed) (fum *FollowUpMessage) { +func (c *ctxResponder) FollowUpEmbed(emb *discordgo.MessageEmbed) (fum *FollowUpMessage) { if emb.Color <= 0 { - emb.Color = c.Ken.opt.EmbedColors.Default + emb.Color = c.ken.opt.EmbedColors.Default } return c.FollowUp(true, &discordgo.WebhookParams{ Embeds: []*discordgo.MessageEmbed{ @@ -149,59 +207,40 @@ func (c *CtxResponder) FollowUpEmbed(emb *discordgo.MessageEmbed) (fum *FollowUp }) } -// FollowUpError is shorthand for FollowUpEmbed with an -// error embed as message with the passed content and -// title. -func (c *CtxResponder) FollowUpError(content, title string) (fum *FollowUpMessage) { +func (c *ctxResponder) FollowUpError(content, title string) (fum *FollowUpMessage) { return c.FollowUpEmbed(&discordgo.MessageEmbed{ Description: content, Title: title, - Color: c.Ken.opt.EmbedColors.Error, + Color: c.ken.opt.EmbedColors.Error, }) } -// Defer is shorthand for Respond with an InteractionResponse -// of the type InteractionResponseDeferredChannelMessageWithSource. -// -// It should be used when the interaction response can not be -// instantly returned. -func (c *CtxResponder) Defer() (err error) { +func (c *ctxResponder) Defer() (err error) { err = c.Respond(&discordgo.InteractionResponse{ Type: discordgo.InteractionResponseDeferredChannelMessageWithSource, }) return } -// GetEphemeral returns the current emphemeral state -// of the command invokation. -func (c *CtxResponder) GetEphemeral() bool { - return c.Ephemeral +func (c *ctxResponder) GetEphemeral() bool { + return c.ephemeral } -// SetEphemeral sets the emphemeral state of the command -// invokation. -// -// Ephemeral can be set to true which will -// send all subsequent command responses -// only to the user which invoked the command. -func (c *CtxResponder) SetEphemeral(v bool) { - c.Ephemeral = v +func (c *ctxResponder) SetEphemeral(v bool) { + c.ephemeral = v } -// GetSession returns the current Discordgo session instance. -func (c *CtxResponder) GetSession() *discordgo.Session { - return c.Session +func (c *ctxResponder) GetSession() *discordgo.Session { + return c.session } -// GetEvent returns the InteractionCreate event instance which -// invoked the interaction command. -func (c *CtxResponder) GetEvent() *discordgo.InteractionCreate { - return c.Event +func (c *ctxResponder) GetEvent() *discordgo.InteractionCreate { + return c.event } -func (c *CtxResponder) messageFlags(p discordgo.MessageFlags) (f discordgo.MessageFlags) { +func (c *ctxResponder) messageFlags(p discordgo.MessageFlags) (f discordgo.MessageFlags) { f = p - if c.Ephemeral { + if c.ephemeral { f |= discordgo.MessageFlagsEphemeral } return @@ -214,7 +253,7 @@ func (c *CtxResponder) messageFlags(p discordgo.MessageFlags) (f discordgo.Messa // after command execution. type Ctx struct { ObjectMap - CtxResponder + ctxResponder // Command provides the called command instance. Command Command @@ -233,8 +272,8 @@ func newCtx() *Ctx { // dependency provider, if available. When no object was found in // either of both maps, nil is returned. func (c *Ctx) Get(key string) (v interface{}) { - if v = c.ObjectMap.Get(key); v == nil && c.Ken.opt.DependencyProvider != nil { - v = c.Ken.opt.DependencyProvider.Get(key) + if v = c.ObjectMap.Get(key); v == nil && c.ken.opt.DependencyProvider != nil { + v = c.ken.opt.DependencyProvider.Get(key) } return } @@ -242,21 +281,21 @@ func (c *Ctx) Get(key string) (v interface{}) { // Channel tries to fetch the channel object from the contained // channel ID using the specified state manager. func (c *Ctx) Channel() (*discordgo.Channel, error) { - return c.Ken.opt.State.Channel(c.Session, c.Event.ChannelID) + return c.ken.opt.State.Channel(c.session, c.event.ChannelID) } // Channel tries to fetch the guild object from the contained // guild ID using the specified state manager. func (c *Ctx) Guild() (*discordgo.Guild, error) { - return c.Ken.opt.State.Guild(c.Session, c.Event.GuildID) + return c.ken.opt.State.Guild(c.session, c.event.GuildID) } // User returns the User object of the executor either from // the events User object or from the events Member object. func (c *Ctx) User() (u *discordgo.User) { - u = c.Event.User - if u == nil && c.Event.Member != nil { - u = c.Event.Member.User + u = c.event.User + if u == nil && c.event.Member != nil { + u = c.event.Member.User } return } @@ -264,7 +303,7 @@ func (c *Ctx) User() (u *discordgo.User) { // Options returns the application command data options // with additional functionality methods. func (c *Ctx) Options() CommandOptions { - return c.Event.ApplicationCommandData().Options + return c.event.ApplicationCommandData().Options } // SlashCommand returns the contexts Command as a @@ -334,11 +373,11 @@ func (c *Ctx) HandleSubCommands(handler ...SubCommandHandler) (err error) { continue } - ctx := c.Ken.subCtxPool.Get().(*SubCommandCtx) + ctx := c.ken.subCtxPool.Get().(*SubCommandCtx) ctx.Ctx = c ctx.SubCommandName = h.Name err = h.Run(ctx) - c.Ken.subCtxPool.Put(ctx) + c.ken.subCtxPool.Put(ctx) break } return @@ -346,7 +385,7 @@ func (c *Ctx) HandleSubCommands(handler ...SubCommandHandler) (err error) { // GetKen returns the root instance of Ken. func (c *Ctx) GetKen() *Ken { - return c.Ken + return c.ken } // GetCommand returns the command instance called. @@ -354,29 +393,41 @@ func (c *Ctx) GetCommand() Command { return c.Command } +// ComponentContext gives access to the underlying +// MessageComponentInteractionData and gives the +// ability to open a Modal afterwards. type ComponentContext interface { ContextResponder + // GetData returns the underlying + // MessageComponentInteractionData. GetData() discordgo.MessageComponentInteractionData + + // OpenModal opens a new modal with the given + // title, content and components built with the + // passed build function. A channel is returned + // which will receive a ModalContext when the user + // has interacted with the modal. OpenModal( title string, content string, build func(b ComponentAssembler), ) (<-chan ModalContext, error) } -type ComponentCtx struct { - CtxResponder + +type componentCtx struct { + ctxResponder Data discordgo.MessageComponentInteractionData } -var _ ComponentContext = (*ComponentCtx)(nil) +var _ ComponentContext = (*componentCtx)(nil) -func (c *ComponentCtx) GetData() discordgo.MessageComponentInteractionData { +func (c *componentCtx) GetData() discordgo.MessageComponentInteractionData { return c.Data } -func (c *ComponentCtx) OpenModal( +func (c *componentCtx) OpenModal( title string, content string, build func(b ComponentAssembler), @@ -400,7 +451,7 @@ func (c *ComponentCtx) OpenModal( cCtx := make(chan ModalContext, 1) - c.Ken.componentHandler.registerModalHandler(modalId, func(ctx ModalContext) bool { + c.ken.componentHandler.registerModalHandler(modalId, func(ctx ModalContext) bool { cCtx <- ctx return true }) @@ -408,26 +459,44 @@ func (c *ComponentCtx) OpenModal( return cCtx, nil } +// ModalContext provides access to the underlying +// ModalSubmitInteractionData and some utility +// methods to access component data from the +// response. type ModalContext interface { ContextResponder + // GetData returns the underlying + // ModalSubmitInteractionData. GetData() discordgo.ModalSubmitInteractionData + + // GetComponentByID tries to find a message component + // by CustomID in the response data and returns it + // wrapped into MessageComponent. + // + // The returned MessageComponent will contain a nil + // value for the wrapped discordgo.MessageComponent + // if it could not be found in the response. + // + // Subsequent method calls to MessageComponent will + // not fail though to ensure the ability to chain + // method calls. GetComponentByID(customId string) MessageComponent } -type ModalCtx struct { - CtxResponder +type modalCtx struct { + ctxResponder Data discordgo.ModalSubmitInteractionData } -var _ ModalContext = (*ModalCtx)(nil) +var _ ModalContext = (*modalCtx)(nil) -func (c *ModalCtx) GetData() discordgo.ModalSubmitInteractionData { +func (c modalCtx) GetData() discordgo.ModalSubmitInteractionData { return c.Data } -func (c *ModalCtx) GetComponentByID(customId string) MessageComponent { +func (c modalCtx) GetComponentByID(customId string) MessageComponent { return MessageComponent{getComponentByID(customId, c.GetData().Components)} } diff --git a/examples/basic/commands/test.go b/examples/basic/commands/test.go index ca5f1ae..9c7b862 100644 --- a/examples/basic/commands/test.go +++ b/examples/basic/commands/test.go @@ -43,7 +43,7 @@ func (c *TestCommand) IsDmCapable() bool { return true } -func (c *TestCommand) Run(ctx *ken.Ctx) (err error) { +func (c *TestCommand) Run(ctx ken.Context) (err error) { val := ctx.Options().GetByName("pog").BoolValue() msg := "not poggers" diff --git a/examples/cmdinfo/commands/test.go b/examples/cmdinfo/commands/test.go index 5f5cfa2..666c3e0 100644 --- a/examples/cmdinfo/commands/test.go +++ b/examples/cmdinfo/commands/test.go @@ -52,7 +52,7 @@ func (c *TestCommand) ExtraInt() int { return 2 } -func (c *TestCommand) Run(ctx *ken.Ctx) (err error) { +func (c *TestCommand) Run(ctx ken.Context) (err error) { val := ctx.Options().GetByName("pog").BoolValue() msg := "not poggers" diff --git a/examples/components/commands/modal.go b/examples/components/commands/modal.go index 43ede87..0057ad5 100644 --- a/examples/components/commands/modal.go +++ b/examples/components/commands/modal.go @@ -38,7 +38,7 @@ func (c *ModalCommand) IsDmCapable() bool { return false } -func (c *ModalCommand) Run(ctx *ken.Ctx) (err error) { +func (c *ModalCommand) Run(ctx ken.Context) (err error) { if err = ctx.Defer(); err != nil { return } diff --git a/examples/components/commands/test.go b/examples/components/commands/test.go index b006914..e2d699d 100644 --- a/examples/components/commands/test.go +++ b/examples/components/commands/test.go @@ -44,7 +44,7 @@ func (c *TestCommand) IsDmCapable() bool { return true } -func (c *TestCommand) Run(ctx *ken.Ctx) (err error) { +func (c *TestCommand) Run(ctx ken.Context) (err error) { if err = ctx.Defer(); err != nil { return } diff --git a/examples/guildscoped/commands/guild1.go b/examples/guildscoped/commands/guild1.go index 8d031c0..5361e6b 100644 --- a/examples/guildscoped/commands/guild1.go +++ b/examples/guildscoped/commands/guild1.go @@ -36,7 +36,7 @@ func (c *Guild1Command) Options() []*discordgo.ApplicationCommandOption { return []*discordgo.ApplicationCommandOption{} } -func (c *Guild1Command) Run(ctx *ken.Ctx) (err error) { +func (c *Guild1Command) Run(ctx ken.Context) (err error) { err = ctx.Respond(&discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ diff --git a/examples/guildscoped/commands/guild2.go b/examples/guildscoped/commands/guild2.go index 47e9a7d..c91ee12 100644 --- a/examples/guildscoped/commands/guild2.go +++ b/examples/guildscoped/commands/guild2.go @@ -36,7 +36,7 @@ func (c *Guild2Command) Options() []*discordgo.ApplicationCommandOption { return []*discordgo.ApplicationCommandOption{} } -func (c *Guild2Command) Run(ctx *ken.Ctx) (err error) { +func (c *Guild2Command) Run(ctx ken.Context) (err error) { err = ctx.Respond(&discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ diff --git a/examples/help/commands/test.go b/examples/help/commands/test.go index 060d329..c4165d8 100644 --- a/examples/help/commands/test.go +++ b/examples/help/commands/test.go @@ -57,7 +57,7 @@ func (C *TestCommand) Help(ctx *ken.SubCommandCtx) (emb *discordgo.MessageEmbed, return } -func (c *TestCommand) Run(ctx *ken.Ctx) (err error) { +func (c *TestCommand) Run(ctx ken.Context) (err error) { err = ctx.HandleSubCommands( ken.SubCommandHandler{"pog", c.pog}, ) diff --git a/examples/middlewares/commands/kick.go b/examples/middlewares/commands/kick.go index 585c811..0d985af 100644 --- a/examples/middlewares/commands/kick.go +++ b/examples/middlewares/commands/kick.go @@ -52,7 +52,7 @@ func (c *KickCommand) RequiresRole() string { return "Admin" } -func (c *KickCommand) Run(ctx *ken.Ctx) (err error) { +func (c *KickCommand) Run(ctx ken.Context) (err error) { if err = ctx.Defer(); err != nil { return } @@ -60,7 +60,7 @@ func (c *KickCommand) Run(ctx *ken.Ctx) (err error) { user := ctx.Options().GetByName("member").UserValue(ctx) reason := ctx.Options().GetByName("reason").StringValue() - if err = ctx.Session.GuildMemberDeleteWithReason(ctx.Event.GuildID, user.ID, reason); err != nil { + if err = ctx.GetSession().GuildMemberDeleteWithReason(ctx.GetEvent().GuildID, user.ID, reason); err != nil { return } diff --git a/examples/middlewares/middlewares/permissions.go b/examples/middlewares/middlewares/permissions.go index e91c521..a639243 100644 --- a/examples/middlewares/middlewares/permissions.go +++ b/examples/middlewares/middlewares/permissions.go @@ -33,13 +33,13 @@ func (c *PermissionsMiddleware) Before(ctx *ken.Ctx) (next bool, err error) { return } - guildRoles, err := ctx.Session.GuildRoles(ctx.Event.GuildID) + guildRoles, err := ctx.GetSession().GuildRoles(ctx.GetEvent().GuildID) if err != nil { return } roleLoop: - for _, rid := range ctx.Event.Member.Roles { + for _, rid := range ctx.GetEvent().Member.Roles { for _, r := range guildRoles { if rid == r.ID && r.Name == cmd.RequiresRole() { next = true diff --git a/examples/ratelimit/commands/test.go b/examples/ratelimit/commands/test.go index 4caa27b..1a1c356 100644 --- a/examples/ratelimit/commands/test.go +++ b/examples/ratelimit/commands/test.go @@ -59,7 +59,7 @@ func (c *TestCommand) IsLimiterGlobal() bool { return false } -func (c *TestCommand) Run(ctx *ken.Ctx) (err error) { +func (c *TestCommand) Run(ctx ken.Context) (err error) { val := ctx.Options().GetByName("pog").BoolValue() msg := "not poggers" diff --git a/examples/subcommands/commands/subs.go b/examples/subcommands/commands/subs.go index 5684a68..454d4df 100644 --- a/examples/subcommands/commands/subs.go +++ b/examples/subcommands/commands/subs.go @@ -65,7 +65,7 @@ func (c *SubsCommand) IsDmCapable() bool { return true } -func (c *SubsCommand) Run(ctx *ken.Ctx) (err error) { +func (c *SubsCommand) Run(ctx ken.Context) (err error) { err = ctx.HandleSubCommands( ken.SubCommandHandler{"one", c.one}, ken.SubCommandHandler{"two", c.two}, diff --git a/examples/usermsgcommands/commands/delete.go b/examples/usermsgcommands/commands/delete.go index 571414b..f7d0599 100644 --- a/examples/usermsgcommands/commands/delete.go +++ b/examples/usermsgcommands/commands/delete.go @@ -21,13 +21,13 @@ func (c *DeleteMessageCommand) Description() string { return "Delete the selected message" } -func (c *DeleteMessageCommand) Run(ctx *ken.Ctx) (err error) { +func (c *DeleteMessageCommand) Run(ctx ken.Context) (err error) { var msg *discordgo.Message - for _, msg = range ctx.Event.ApplicationCommandData().Resolved.Messages { + for _, msg = range ctx.GetEvent().ApplicationCommandData().Resolved.Messages { break } - if err = ctx.Session.ChannelMessageDelete(msg.ChannelID, msg.ID); err != nil { + if err = ctx.GetSession().ChannelMessageDelete(msg.ChannelID, msg.ID); err != nil { return } diff --git a/examples/usermsgcommands/commands/info.go b/examples/usermsgcommands/commands/info.go index 1530ec1..286f7db 100644 --- a/examples/usermsgcommands/commands/info.go +++ b/examples/usermsgcommands/commands/info.go @@ -21,7 +21,7 @@ func (c *InfoUserCommand) Description() string { return "Display user information." } -func (c *InfoUserCommand) Run(ctx *ken.Ctx) (err error) { +func (c *InfoUserCommand) Run(ctx ken.Context) (err error) { err = ctx.RespondEmbed(&discordgo.MessageEmbed{ Description: ctx.User().String(), }) diff --git a/ken.go b/ken.go index c9456c3..6b6d90f 100644 --- a/ken.go +++ b/ken.go @@ -340,14 +340,14 @@ func (k *Ken) onInteractionCreate(s *discordgo.Session, e *discordgo.Interaction defer k.ctxPool.Put(ctx) ctx.Purge() ctx.responded = false - ctx.Ken = k - ctx.Session = s - ctx.Event = e + ctx.ken = k + ctx.session = s + ctx.event = e ctx.Command = cmd - ctx.Ephemeral = false + ctx.ephemeral = false if rpCmd, ok := cmd.(ResponsePolicyCommand); ok { - ctx.Ephemeral = rpCmd.ResponsePolicy().Ephemeral + ctx.ephemeral = rpCmd.ResponsePolicy().Ephemeral } if ch.Type == discordgo.ChannelTypeDM || ch.Type == discordgo.ChannelTypeGroupDM { diff --git a/middlewares/ratelimit/ratelimit.go b/middlewares/ratelimit/ratelimit.go index b87123c..9d621aa 100644 --- a/middlewares/ratelimit/ratelimit.go +++ b/middlewares/ratelimit/ratelimit.go @@ -50,7 +50,7 @@ func (m *Middleware) Before(ctx *ken.Ctx) (next bool, err error) { if ch.Type == discordgo.ChannelTypeDM || ch.Type == discordgo.ChannelTypeGroupDM { guildID = "__dm__" } else { - guildID = ctx.Event.GuildID + guildID = ctx.GetEvent().GuildID } } diff --git a/middlewares/ratelimit/v2/ratelimit.go b/middlewares/ratelimit/v2/ratelimit.go index 9503052..c701a35 100644 --- a/middlewares/ratelimit/v2/ratelimit.go +++ b/middlewares/ratelimit/v2/ratelimit.go @@ -59,7 +59,7 @@ func (m *Middleware) Before(ctx *ken.Ctx) (next bool, err error) { if ch.Type == discordgo.ChannelTypeDM || ch.Type == discordgo.ChannelTypeGroupDM { guildID = "__dm__" } else { - guildID = ctx.Event.GuildID + guildID = ctx.GetEvent().GuildID } } From 0b65cd40c9880f4ff9aafdfe32022f6e84febc58 Mon Sep 17 00:00:00 2001 From: zekro Date: Wed, 31 Aug 2022 09:03:51 +0000 Subject: [PATCH 4/5] make SubCommandContext available via interface --- context.go | 36 ++++++++++----- examples/components/main.go | 1 - examples/help/commands/test.go | 4 +- .../{components => modals}/commands/modal.go | 4 +- examples/modals/main.go | 45 +++++++++++++++++++ examples/subcommands/commands/subs.go | 4 +- ken.go | 2 +- middlewares/cmdhelp/cmdhelp.go | 2 +- middlewares/cmdhelp/command.go | 2 +- 9 files changed, 78 insertions(+), 22 deletions(-) rename examples/{components => modals}/commands/modal.go (95%) create mode 100644 examples/modals/main.go diff --git a/context.go b/context.go index 495637a..12b83d2 100644 --- a/context.go +++ b/context.go @@ -331,28 +331,40 @@ func (c *Ctx) MessageCommand() (cmd MessageCommand, ok bool) { // to handle sub command calls. type SubCommandHandler struct { Name string - Run func(ctx *SubCommandCtx) error + Run func(ctx SubCommandContext) error } -// SubCommandCtx wraps the current command Ctx and -// with the called sub command name and scopes the -// command options to the options of the called -// sub command. +// SubCommandContext wraps the current command +// Context and with the called sub command name +// and scopes the command options to the +// options of the called sub command. // // The SubCommandCtx must not be stored or used // after command execution. -type SubCommandCtx struct { +type SubCommandContext interface { + Context + + // GetSubCommandName returns the sub command + // name which has been invoked. + GetSubCommandName() string +} + +type subCommandCtx struct { *Ctx - SubCommandName string + subCommandName string } -var _ Context = (*SubCommandCtx)(nil) +var _ SubCommandContext = (*subCommandCtx)(nil) // Options returns the options array of the called // sub command. -func (c *SubCommandCtx) Options() CommandOptions { - return c.Ctx.Options().GetByName(c.SubCommandName).Options +func (c *subCommandCtx) Options() CommandOptions { + return c.Ctx.Options().GetByName(c.subCommandName).Options +} + +func (c *subCommandCtx) GetSubCommandName() string { + return c.subCommandName } // HandleSubCommands takes a list of sub command handles. @@ -373,9 +385,9 @@ func (c *Ctx) HandleSubCommands(handler ...SubCommandHandler) (err error) { continue } - ctx := c.ken.subCtxPool.Get().(*SubCommandCtx) + ctx := c.ken.subCtxPool.Get().(*subCommandCtx) ctx.Ctx = c - ctx.SubCommandName = h.Name + ctx.subCommandName = h.Name err = h.Run(ctx) c.ken.subCtxPool.Put(ctx) break diff --git a/examples/components/main.go b/examples/components/main.go index 0262ad6..d335979 100644 --- a/examples/components/main.go +++ b/examples/components/main.go @@ -33,7 +33,6 @@ func main() { must(k.RegisterCommands( new(commands.TestCommand), - new(commands.ModalCommand), )) defer k.Unregister() diff --git a/examples/help/commands/test.go b/examples/help/commands/test.go index c4165d8..4a8d7b9 100644 --- a/examples/help/commands/test.go +++ b/examples/help/commands/test.go @@ -49,7 +49,7 @@ func (c *TestCommand) IsDmCapable() bool { return true } -func (C *TestCommand) Help(ctx *ken.SubCommandCtx) (emb *discordgo.MessageEmbed, err error) { +func (C *TestCommand) Help(ctx ken.SubCommandContext) (emb *discordgo.MessageEmbed, err error) { emb = &discordgo.MessageEmbed{ Color: 0x00ff00, Description: "This is a help message that describes how to use this command!", @@ -65,7 +65,7 @@ func (c *TestCommand) Run(ctx ken.Context) (err error) { return } -func (c *TestCommand) pog(ctx *ken.SubCommandCtx) (err error) { +func (c *TestCommand) pog(ctx ken.SubCommandContext) (err error) { return ctx.Respond(&discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ diff --git a/examples/components/commands/modal.go b/examples/modals/commands/modal.go similarity index 95% rename from examples/components/commands/modal.go rename to examples/modals/commands/modal.go index 0057ad5..a33f489 100644 --- a/examples/components/commands/modal.go +++ b/examples/modals/commands/modal.go @@ -10,8 +10,8 @@ import ( type ModalCommand struct{} var ( - _ ken.SlashCommand = (*TestCommand)(nil) - _ ken.DmCapable = (*TestCommand)(nil) + _ ken.SlashCommand = (*ModalCommand)(nil) + _ ken.DmCapable = (*ModalCommand)(nil) ) func (c *ModalCommand) Name() string { diff --git a/examples/modals/main.go b/examples/modals/main.go new file mode 100644 index 0000000..96d1d69 --- /dev/null +++ b/examples/modals/main.go @@ -0,0 +1,45 @@ +package main + +import ( + "os" + "os/signal" + "syscall" + + "github.com/bwmarrin/discordgo" + "github.com/zekrotja/ken" + "github.com/zekrotja/ken/examples/modals/commands" + "github.com/zekrotja/ken/store" +) + +func must(err error) { + if err != nil { + panic(err) + } +} + +func main() { + token := os.Getenv("TOKEN") + + session, err := discordgo.New("Bot " + token) + if err != nil { + panic(err) + } + defer session.Close() + + k, err := ken.New(session, ken.Options{ + CommandStore: store.NewDefault(), + }) + must(err) + + must(k.RegisterCommands( + new(commands.ModalCommand), + )) + + defer k.Unregister() + + must(session.Open()) + + sc := make(chan os.Signal, 1) + signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill) + <-sc +} diff --git a/examples/subcommands/commands/subs.go b/examples/subcommands/commands/subs.go index 454d4df..15f7dbc 100644 --- a/examples/subcommands/commands/subs.go +++ b/examples/subcommands/commands/subs.go @@ -74,7 +74,7 @@ func (c *SubsCommand) Run(ctx ken.Context) (err error) { return } -func (c *SubsCommand) one(ctx *ken.SubCommandCtx) (err error) { +func (c *SubsCommand) one(ctx ken.SubCommandContext) (err error) { if err = ctx.Defer(); err != nil { return } @@ -85,7 +85,7 @@ func (c *SubsCommand) one(ctx *ken.SubCommandCtx) (err error) { return } -func (c *SubsCommand) two(ctx *ken.SubCommandCtx) (err error) { +func (c *SubsCommand) two(ctx ken.SubCommandContext) (err error) { var arg int if argV, ok := ctx.Options().GetByNameOptional("arg"); ok { arg = int(argV.IntValue()) diff --git a/ken.go b/ken.go index 6b6d90f..1c025ba 100644 --- a/ken.go +++ b/ken.go @@ -102,7 +102,7 @@ func New(s *discordgo.Session, options ...Options) (k *Ken, err error) { }, subCtxPool: sync.Pool{ New: func() interface{} { - return &SubCommandCtx{} + return &subCommandCtx{} }, }, mwBefore: make([]MiddlewareBefore, 0), diff --git a/middlewares/cmdhelp/cmdhelp.go b/middlewares/cmdhelp/cmdhelp.go index 3473c3e..8fcff04 100644 --- a/middlewares/cmdhelp/cmdhelp.go +++ b/middlewares/cmdhelp/cmdhelp.go @@ -52,7 +52,7 @@ func (m *Middleware) Before(ctx *ken.Ctx) (next bool, err error) { func (m *Middleware) subCmdHandler(cmd HelpProvider, executed chan<- bool) ken.SubCommandHandler { return ken.SubCommandHandler{ Name: m.subCommandName, - Run: func(ctx *ken.SubCommandCtx) (err error) { + Run: func(ctx ken.SubCommandContext) (err error) { executed <- true emb, err := cmd.Help(ctx) if err != nil { diff --git a/middlewares/cmdhelp/command.go b/middlewares/cmdhelp/command.go index 0263099..329119a 100644 --- a/middlewares/cmdhelp/command.go +++ b/middlewares/cmdhelp/command.go @@ -8,5 +8,5 @@ import ( // HelpProvider defines a command which provides // help content. type HelpProvider interface { - Help(ctx *ken.SubCommandCtx) (emb *discordgo.MessageEmbed, err error) + Help(ctx ken.SubCommandContext) (emb *discordgo.MessageEmbed, err error) } From 0dbc8d11fdc660ac9525ae5b49c4ee2f15296488 Mon Sep 17 00:00:00 2001 From: zekro Date: Wed, 31 Aug 2022 10:45:22 +0000 Subject: [PATCH 5/5] udpate changelog --- CHANGELOG.md | 60 +++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 50 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4494ca4..f8f13cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,18 +1,58 @@ [VERSION] -## Changes +**Warning:** This update of ken introduces a lot of API changes. Please update carefully. -- Fix a bug where the wrapped handler would not be registered in the build wrapper. -- The `ComponentHandlerFunc` now returns a boolean to indicate success of execution. -- The `Ken` instance now has a method `Session` which returns the wrapped Discordgo `Session` - instance. -- The `Build` method of the `ComponentBuilder` now also returns a function to remove components - from the message as well as unregister the handlers. -- The `Unregister` method of the `ComponentHandler` now can take one or more `customId`s to - be unregistered at once. +## Modals [#10] + +You can now open modals in the component handler function. The `ComponentContext` now has a +method `OpenModal` which can be used to open a modal in Discord on interaction with the +message component. + +```go +// ComponentContext gives access to the underlying +// MessageComponentInteractionData and gives the +// ability to open a Modal afterwards. +type ComponentContext interface { + ContextResponder + + // GetData returns the underlying + // MessageComponentInteractionData. + GetData() discordgo.MessageComponentInteractionData + + // OpenModal opens a new modal with the given + // title, content and components built with the + // passed build function. A channel is returned + // which will receive a ModalContext when the user + // has interacted with the modal. + OpenModal( + title string, + content string, + build func(b ComponentAssembler), + ) (<-chan ModalContext, error) +} +``` + +Please take a look at the [**modals example**](examples/modals) to see further details on +how to use modals with ken. + +## Breaking API Changes + +A lot of breaking changes have been introduced to use more interfaces instead of struct +instances which allows better testability using mocks. + +The `Run` method of the `Command` interface now is getting passed a `Context` interface instead +of a reference to an instance of `Ctx`. This also means, if you are directly accessing `Session` +or `Event` for example from the `Ctx` instance, you need to change it to accessing these via the +available getter methods (`GetSession` or `GetEvent` for example). + +The `SubCommandHandler` now also passes an interface `SubCommandContext` to +the `Run` instead of a reference to an instance of `SubCommandCtx`. + +The access to `CtxResponder`, `SubCommandCtx`, `ComponentCtx` and `ModalCtx` are now private +for a cleaner API. ## Update ``` go get -v -u github.com/zekrotja/ken@[VERSION] -``` \ No newline at end of file +```