diff --git a/README.md b/README.md index 44df488..1873269 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,10 @@ registered on the default guild — instant, one server — or on the applicatio with `WithGlobalCommands`, which reaches every server the bot is in and takes up to an hour to propagate. +`Interaction.UserID()` is who invoked it, from `member.user` in a guild and from +`user` in a DM — read either field alone and permission checks see an empty id in +the other context. + Path segments are percent-escaped and queries built with `url.Values`. `Webhook.Token` and `Interaction.Token` are `Secret` — `[REDACTED]` in logs and JSON, `.Reveal()` to read. diff --git a/interactions.go b/interactions.go index 321c43e..c434986 100644 --- a/interactions.go +++ b/interactions.go @@ -25,6 +25,7 @@ type Interaction struct { GuildID string `json:"guild_id"` ChannelID string `json:"channel_id"` Member Member `json:"member"` + User Author `json:"user"` Data InteractionData `json:"data"` } @@ -33,6 +34,17 @@ type Member struct { User Author `json:"user"` } +// UserID is who invoked the interaction, wherever Discord put them: a guild +// interaction carries the caller under `member.user`, a DM under `user`. Reading +// only one of the two makes every permission check in the other context see an +// empty id. +func (i Interaction) UserID() string { + if id := i.Member.User.ID; id != "" { + return id + } + return i.User.ID +} + // InteractionData is the invoked command + its options. For a component // interaction (type 3) the command fields are empty and CustomID/Values carry // the clicked component's id and the selected value(s) instead. diff --git a/interactions_autocomplete_test.go b/interactions_autocomplete_test.go index 8ddd5da..36b859d 100644 --- a/interactions_autocomplete_test.go +++ b/interactions_autocomplete_test.go @@ -34,6 +34,22 @@ func TestFocusedNoneWhenAbsent(t *testing.T) { } } +// Discord puts the caller under `member.user` in a guild and under `user` in a +// DM. A permission check that reads only one sees an empty id in the other. +func TestInteractionUserIDCoversGuildAndDM(t *testing.T) { + guild := Interaction{Member: Member{User: Author{ID: "u1"}}} + if got := guild.UserID(); got != "u1" { + t.Fatalf("guild UserID() = %q, want u1", got) + } + dm := Interaction{User: Author{ID: "u2"}} + if got := dm.UserID(); got != "u2" { + t.Fatalf("dm UserID() = %q, want u2", got) + } + if got := (Interaction{}).UserID(); got != "" { + t.Fatalf("empty UserID() = %q, want empty", got) + } +} + func TestInteractionsRespondNoAllowedMentions(t *testing.T) { s := transport.NewStub() in := &Interactions{rt: s, def: &defaults{guilds: &Guilds{rt: s}}}