github.com/starshine-sys/bcr@v0.21.0/context_slash.go (about) 1 package bcr 2 3 import ( 4 "fmt" 5 "time" 6 7 "emperror.dev/errors" 8 9 "github.com/diamondburned/arikawa/v3/api" 10 "github.com/diamondburned/arikawa/v3/discord" 11 "github.com/diamondburned/arikawa/v3/gateway" 12 "github.com/diamondburned/arikawa/v3/state" 13 "github.com/diamondburned/arikawa/v3/utils/json/option" 14 "github.com/diamondburned/arikawa/v3/utils/sendpart" 15 ) 16 17 // Contexter is the type passed to (*Command).SlashCommand. 18 // This includes basic methods implemented by both Context and SlashContext; for all of their respective methods and fields, convert the Contexter to a Context or SlashContext. 19 // As SlashCommand will be called with a basic *Context if Command is nil, this allows for some code deduplication if the command only uses basic methods. 20 type Contexter interface { 21 Flags 22 23 // SendX sends a message without returning the created discord.Message 24 SendX(string, ...discord.Embed) error 25 SendfX(string, ...interface{}) error 26 27 // Send sends a message, returning the created message. 28 Send(string, ...discord.Embed) (*discord.Message, error) 29 Sendf(string, ...interface{}) (*discord.Message, error) 30 31 SendComponents([]discord.Component, string, ...discord.Embed) (*discord.Message, error) 32 33 // SendFiles sends a message with attachments 34 SendFiles(string, ...sendpart.File) error 35 36 // SendEphemeral sends an ephemeral message (or falls back to a normal message without slash commands) 37 SendEphemeral(string, ...discord.Embed) error 38 39 EditOriginal(api.EditInteractionResponseData) (*discord.Message, error) 40 41 // Session returns this context's *state.State 42 Session() *state.State 43 // User returns this context's Author 44 User() discord.User 45 // GetGuild returns this context's Guild 46 GetGuild() *discord.Guild 47 // GetChannel returns this context's Channel 48 GetChannel() *discord.Channel 49 // GetParentChannel returns this context's ParentChannel 50 GetParentChannel() *discord.Channel 51 // GetMember returns this context's Member 52 GetMember() *discord.Member 53 54 // ButtonPages paginates a slice of embeds using buttons 55 ButtonPages(embeds []discord.Embed, timeout time.Duration) (msg *discord.Message, rmFunc func(), err error) 56 ButtonPagesWithComponents(embeds []discord.Embed, timeout time.Duration, components []discord.Component) (msg *discord.Message, rmFunc func(), err error) 57 58 // ConfirmButton confirms a prompt with buttons or "yes"/"no" messages. 59 ConfirmButton(userID discord.UserID, data ConfirmData) (yes, timeout bool) 60 } 61 62 var _ Contexter = (*SlashContext)(nil) 63 64 // SlashContext is the Contexter passed to a slash command function. 65 type SlashContext struct { 66 CommandID discord.CommandID 67 CommandName string 68 CommandOptions []gateway.InteractionOption 69 70 InteractionID discord.InteractionID 71 InteractionToken string 72 73 Command *Command 74 Router *Router 75 State *state.State 76 Guild *discord.Guild 77 78 Author discord.User 79 Member *discord.Member 80 81 Channel *discord.Channel 82 ParentChannel *discord.Channel 83 84 // Event is the original raw event 85 Event *gateway.InteractionCreateEvent 86 87 AdditionalParams map[string]interface{} 88 } 89 90 // Session returns this SlashContext's state. 91 func (ctx *SlashContext) Session() *state.State { 92 return ctx.State 93 } 94 95 // Errors related to slash contexts 96 var ( 97 ErrNotCommand = errors.New("not a command interaction") 98 ) 99 100 // NewSlashContext creates a new slash command context. 101 func (r *Router) NewSlashContext(ic *gateway.InteractionCreateEvent) (*SlashContext, error) { 102 var err error 103 104 if ic.Type != gateway.CommandInteraction { 105 return nil, ErrNotCommand 106 } 107 108 sc := &SlashContext{ 109 Router: r, 110 Event: ic, 111 CommandName: ic.Data.Name, 112 CommandID: ic.Data.ID, 113 CommandOptions: ic.Data.Options, 114 InteractionID: ic.ID, 115 InteractionToken: ic.Token, 116 AdditionalParams: map[string]interface{}{}, 117 } 118 119 if ic.Member != nil { 120 sc.Member = ic.Member 121 sc.Author = ic.Member.User 122 } else { 123 sc.Author = *ic.User 124 } 125 126 state, _ := r.StateFromGuildID(ic.GuildID) 127 sc.State = state 128 129 // get guild 130 if ic.GuildID.IsValid() { 131 sc.Guild, err = sc.State.Guild(ic.GuildID) 132 if err != nil { 133 return sc, ErrGuild 134 } 135 sc.Guild.Roles, err = sc.State.Roles(ic.GuildID) 136 if err != nil { 137 return sc, ErrGuild 138 } 139 } 140 141 // get the channel 142 sc.Channel, err = sc.State.Channel(ic.ChannelID) 143 if err != nil { 144 return sc, ErrChannel 145 } 146 147 if sc.Thread() { 148 sc.ParentChannel, err = sc.State.Channel(sc.Channel.ParentID) 149 if err != nil { 150 return sc, ErrChannel 151 } 152 } 153 154 return sc, nil 155 } 156 157 // Thread returns true if the context is in a thread channel. 158 // If this function returns true, ctx.ParentChannel will be non-nil. 159 func (ctx *SlashContext) Thread() bool { 160 return ctx.Channel.Type == discord.GuildNewsThread || ctx.Channel.Type == discord.GuildPublicThread || ctx.Channel.Type == discord.GuildPrivateThread 161 } 162 163 // SendX sends a message without returning the created discord.Message 164 func (ctx *SlashContext) SendX(content string, embeds ...discord.Embed) (err error) { 165 data := api.InteractionResponse{ 166 Type: api.MessageInteractionWithSource, 167 Data: &api.InteractionResponseData{ 168 AllowedMentions: ctx.Router.DefaultMentions, 169 }, 170 } 171 172 if len(embeds) != 0 { 173 data.Data.Embeds = &embeds 174 } 175 if content != "" { 176 data.Data.Content = option.NewNullableString(content) 177 } 178 179 err = ctx.State.RespondInteraction(ctx.InteractionID, ctx.InteractionToken, data) 180 return 181 } 182 183 // SendfX ... 184 func (ctx *SlashContext) SendfX(format string, args ...interface{}) (err error) { 185 return ctx.SendX(fmt.Sprintf(format, args...)) 186 } 187 188 // SendFiles sends a message with attachments 189 func (ctx *SlashContext) SendFiles(content string, files ...sendpart.File) (err error) { 190 data := api.InteractionResponse{ 191 Type: api.MessageInteractionWithSource, 192 Data: &api.InteractionResponseData{ 193 AllowedMentions: ctx.Router.DefaultMentions, 194 }, 195 } 196 197 if len(files) != 0 { 198 data.Data.Files = files 199 } 200 if content != "" { 201 data.Data.Content = option.NewNullableString(content) 202 } 203 204 err = ctx.State.RespondInteraction(ctx.InteractionID, ctx.InteractionToken, data) 205 return 206 } 207 208 // SendEphemeral sends an ephemeral message. 209 func (ctx *SlashContext) SendEphemeral(content string, embeds ...discord.Embed) (err error) { 210 data := api.InteractionResponse{ 211 Type: api.MessageInteractionWithSource, 212 Data: &api.InteractionResponseData{ 213 AllowedMentions: ctx.Router.DefaultMentions, 214 Flags: api.EphemeralResponse, 215 }, 216 } 217 218 if len(embeds) != 0 { 219 data.Data.Embeds = &embeds 220 } 221 if content != "" { 222 data.Data.Content = option.NewNullableString(content) 223 } 224 225 err = ctx.State.RespondInteraction(ctx.InteractionID, ctx.InteractionToken, data) 226 return 227 } 228 229 // Original returns the original response to an interaction, if any. 230 func (ctx *SlashContext) Original() (msg *discord.Message, err error) { 231 url := api.EndpointWebhooks + ctx.Router.Bot.ID.String() + "/" + ctx.InteractionToken + "/messages/@original" 232 233 return msg, ctx.State.RequestJSON(&msg, "GET", url) 234 } 235 236 // EditOriginal edits the original response. 237 func (ctx *SlashContext) EditOriginal(data api.EditInteractionResponseData) (*discord.Message, error) { 238 return ctx.State.EditInteractionResponse(discord.AppID(ctx.Router.Bot.ID), ctx.Event.Token, data) 239 } 240 241 // GetGuild ... 242 func (ctx *SlashContext) GetGuild() *discord.Guild { return ctx.Guild } 243 244 // GetChannel ... 245 func (ctx *SlashContext) GetChannel() *discord.Channel { return ctx.Channel } 246 247 // GetParentChannel ... 248 func (ctx *SlashContext) GetParentChannel() *discord.Channel { return ctx.ParentChannel } 249 250 // User ... 251 func (ctx *SlashContext) User() discord.User { return ctx.Author } 252 253 // GetMember ... 254 func (ctx *SlashContext) GetMember() *discord.Member { return ctx.Member } 255 256 // Send ... 257 func (ctx *SlashContext) Send(content string, embeds ...discord.Embed) (msg *discord.Message, err error) { 258 err = ctx.SendX(content, embeds...) 259 if err != nil { 260 return 261 } 262 263 return ctx.Original() 264 } 265 266 // Sendf ... 267 func (ctx *SlashContext) Sendf(tmpl string, args ...interface{}) (msg *discord.Message, err error) { 268 err = ctx.SendfX(tmpl, args...) 269 if err != nil { 270 return 271 } 272 273 return ctx.Original() 274 } 275 276 // GuildPerms returns the global (guild) permissions of this Context's user. 277 // If in DMs, it will return the permissions users have in DMs. 278 func (ctx *SlashContext) GuildPerms() (perms discord.Permissions) { 279 if ctx.Guild == nil || ctx.Member == nil { 280 return discord.PermissionViewChannel | discord.PermissionSendMessages | discord.PermissionAddReactions | discord.PermissionReadMessageHistory 281 } 282 283 if ctx.Guild.OwnerID == ctx.Author.ID { 284 return discord.PermissionAll 285 } 286 287 for _, id := range ctx.Member.RoleIDs { 288 for _, r := range ctx.Guild.Roles { 289 if id == r.ID { 290 if r.Permissions.Has(discord.PermissionAdministrator) { 291 return discord.PermissionAll 292 } 293 294 perms |= r.Permissions 295 break 296 } 297 } 298 } 299 300 return perms 301 } 302 303 // SendComponents sends a message with components 304 func (ctx *SlashContext) SendComponents(components []discord.Component, content string, embeds ...discord.Embed) (*discord.Message, error) { 305 data := api.InteractionResponse{ 306 Type: api.MessageInteractionWithSource, 307 Data: &api.InteractionResponseData{ 308 AllowedMentions: ctx.Router.DefaultMentions, 309 Content: option.NewNullableString(content), 310 Embeds: &embeds, 311 Components: &components, 312 }, 313 } 314 315 err := ctx.State.RespondInteraction(ctx.InteractionID, ctx.InteractionToken, data) 316 if err != nil { 317 return nil, err 318 } 319 320 return ctx.Original() 321 }