diff --git a/gateway/events.go b/gateway/events.go index f35032c..472a4b6 100644 --- a/gateway/events.go +++ b/gateway/events.go @@ -1,10 +1,13 @@ package gateway import ( + "encoding/json" + "errors" "strconv" "strings" "github.com/diamondburned/arikawa/v3/discord" + "github.com/diamondburned/arikawa/v3/utils/json/option" "github.com/diamondburned/arikawa/v3/utils/ws" ) @@ -69,19 +72,52 @@ type ResumeCommand struct { // https://discord.com/developers/docs/topics/gateway#connecting-and-resuming type InvalidSessionEvent bool -// RequestGuildMembersCommand is a command for Op 8. +// RequestGuildMembersCommand is a command for Op 8. Either UserIDs or (Query +// and Limit) must be filled. type RequestGuildMembersCommand struct { - // GuildIDs contains the ids of the guilds to request data from. Multiple + // GuildIDs contains the IDs of the guilds to request data from. Multiple // guilds can only be requested when using user accounts. - GuildIDs []discord.GuildID `json:"guild_id"` - UserIDs []discord.UserID `json:"user_ids,omitempty"` + GuildIDs []discord.GuildID `json:"guild_ids"` + + // UserIDs contains the IDs of the users to request data for. If this is + // filled, then the Query field must be empty. + UserIDs []discord.UserID `json:"user_ids,omitempty"` + + // Query is a string to search for matching usernames. If this is filled, + // then the UserIDs field must be empty. If Query is empty, then all members + // are filled for bots. + Query option.String `json:"query,omitempty"` + // Limit is used to specify the maximum number of members to send back when + // Query is used. + Limit uint `json:"limit,omitempty"` - Query string `json:"query,omitempty"` - Limit uint `json:"limit,omitempty"` Presences bool `json:"presences"` Nonce string `json:"nonce,omitempty"` } +// MarshalJSON marshals a RequestGuildMembersCommand. +func (c *RequestGuildMembersCommand) MarshalJSON() ([]byte, error) { + type raw RequestGuildMembersCommand + + if c.UserIDs != nil && c.Query != nil { + return nil, errors.New("neither UserIDs nor Query can be filled") + } + + var marshaling interface{} = (*raw)(c) + if c.Query != nil { + // Force the Limit field to be present if Query is present. + marshaling = struct { + *raw + Limit uint `json:"limit"` + }{ + raw: (*raw)(c), + Limit: c.Limit, + } + } + + return json.Marshal(marshaling) +} + // UpdateVoiceStateCommand is a command for Op 4. type UpdateVoiceStateCommand struct { GuildID discord.GuildID `json:"guild_id"` diff --git a/gateway/events_test.go b/gateway/events_test.go new file mode 100644 index 0000000..1217b53 --- /dev/null +++ b/gateway/events_test.go @@ -0,0 +1,89 @@ +package gateway + +import ( + "context" + "encoding/json" + "reflect" + "strings" + "testing" + + "github.com/diamondburned/arikawa/v3/discord" + "github.com/diamondburned/arikawa/v3/utils/json/option" + "github.com/diamondburned/arikawa/v3/utils/ws" +) + +func TestRequestGuildMembersCommand(t *testing.T) { + assert := func(cmd Event, data map[string]interface{}) { + cmdBytes, err := json.Marshal(cmd) + if err != nil { + t.Fatal("failed to marshal command:", err) + } + + var cmdMap map[string]interface{} + if err := json.Unmarshal(cmdBytes, &cmdMap); err != nil { + t.Fatal("failed to unmarshal command:", err) + } + + if !reflect.DeepEqual(cmdMap, data) { + t.Fatalf("mismatched command, got %#v", cmdMap) + } + } + + t.Run("userIDs", func(t *testing.T) { + cmd := RequestGuildMembersCommand{ + GuildIDs: []discord.GuildID{123}, + UserIDs: []discord.UserID{456}, + } + + assert(&cmd, map[string]interface{}{ + "guild_ids": []interface{}{"123"}, + "user_ids": []interface{}{"456"}, + "presences": false, + }) + }) + + t.Run("query_empty", func(t *testing.T) { + cmd := RequestGuildMembersCommand{ + GuildIDs: []discord.GuildID{123}, + Query: option.NewString(""), + } + + assert(&cmd, map[string]interface{}{ + "guild_ids": []interface{}{"123"}, + "query": "", + "limit": float64(0), + "presences": false, + }) + }) + + t.Run("query_nonempty", func(t *testing.T) { + cmd := RequestGuildMembersCommand{ + GuildIDs: []discord.GuildID{123}, + Query: option.NewString("abc"), + } + + assert(&cmd, map[string]interface{}{ + "guild_ids": []interface{}{"123"}, + "query": "abc", + "limit": float64(0), + "presences": false, + }) + }) + + t.Run("both", func(t *testing.T) { + cmd := RequestGuildMembersCommand{ + GuildIDs: []discord.GuildID{123}, + UserIDs: []discord.UserID{456}, + Query: option.NewString("abc"), + } + + // Gateway should never be touched when Marshal fails, so we can just + // create a zero-value. + var gateway ws.Gateway + + err := gateway.Send(context.Background(), &cmd) + if err == nil || !strings.Contains(err.Error(), "neither UserIDs nor Query can be filled") { + t.Fatal("unexpected error:", err) + } + }) +}